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