Functional PowerShell
PowerShellで色々書いてて、折角Foreach-ObjectやWhere-Objectがあるので、もっと関数型言語っぽく書けないものかと思って、仕様をいくつか調べてみた。
if式
$(...)で解決する。$()の括弧内に記載した要素の出力結果を値に変換できる。
条件演算子が無くてもこれで大丈夫。
# 利用例 $count += $(if ($num -lt 10) { 1 } else { 0 }) # 1,0,1,0,... 1..10 | %{ $(if ($_%2 -eq 0) { 0 } else { 1 }) }
場合によっては、switchを$()で囲めばパターンマッチっぽいものもできそう。
だが、switchにはbreakが必要なのが残念である。
Tuple
配列をカンマ区切りで書ける。
アクセスは[0], [1]とかでできるので、必要に応じてfst, sndなどを実装すればよいかな。
# 利用例 $x,$_ = 1,2 $x # 1が入る
Lambda式(無名関数)
ScriptBlockを無名関数のように使える。
評価する場合、ScriptBlockの先頭に&を付ける。
ブロック内で引数を取る場合、暗黙変数$argsか、param構文を利用できる。
# 利用例 &{ $args[0] + $args[1] } 1 2 # 3 $add = { $args[0] + $args[1] } &$add 1 2 # 3 $add = { param($x, $y) $x+$y } &$add 1 2 # 3
高階関数 (簡単な場合)
関数を返す関数。ScriptBlockを返せばよい。
使う時には&で解釈する必要がある。
# 利用例 function getAdd { { $args[0] + $args[1] } } $add = getAdd &$add 1 2
クロージャ(変数を束縛する高階関数)
以下のように書くと上手くいきそうだが、PowerShellではブロック内のローカル変数を保持してくれない。
# 失敗例 ($nが保持されない) $add = { $n = $args[0]; { $args[0] + $n } } $add2 = &$add 2 &$add2 3 # 実行時に変数参照するので、グローバル変数を使うならまあできる $g_add = { $global:n = $args[0]; { $args[0] + $global:n } } $g_add2 = &$g_add 2 # 2をグローバル変数に保持して部分適用(?) &$g_add2 3 # 5 $global:n # 2
問題はブロック内の一次変数を保持してくれないことなので、ブロックを文字列で作り、
それをブロックに変換すればよい。
[scriptblock]::Createが用意されているので、それを利用する。
# 利用例 $add = { $block = '$args[0] + {0}' -f $args[0]; [scriptblock]::Create($block) } $add2 = &$add 2 &$add2 3 # 5 # まとめて適用する場合、&を書くのが手間ではある &(&$add 1) 2 # 3 # 適用関数を作っておいてもよいかも function apply ($op, $left, $right) { &(&$op $left) $right } apply add 1 2 # 3
GetNewClosure
牟田口さん(@mutaguchi)さんから、PowerShellのv2以上であればScriptBlock#GetNewClosureが使えると教えて頂いた。
試してみた。できた!
# クロージャの例 $add = { $x = $args[0]; { $x + $args[0] }.GetNewClosure() } &(&$add 1) 2 # 3
map
Foreach-Object (foreach, %)を利用する。
mapなのにforeachと呼ぶのが気に入らないので、%を多用してしまう……
# 利用例 1..10 | %{ $_ * 2 } # 無名関数の利用 1..10 | % { & {$args[0] * 2} $_ }
filter
Where-Object (where, ?)を利用する。
# 利用例 1..10 | ? { $_%2 -eq 0 } # 関数の利用 $even = { $args[0]%2 -eq 0 } 1..10 | ? { &$even $_ }
まとめ
なんとかなりそうな力が備わっているので、頑張ってもっとFunctionalにPowerShellを書こう*1。
あ、効率とかは全く測定してないですが、なんとなくあまりよくなさそうな気がします。
*1:#joke=on