PowerShellのパイプラインを遅延評価にしてみる

パイプラインからの入力をスクリプトブロックに包んで返せば、遅延評価にできるのでは? という思い付きのネタです。

例題

1から10までの整数列から偶数を抽出してその値を2乗するだけの簡単な処理を例に見てみます。

C#で書くとこんな感じ

var query = Enumerable.Range(1,10)
    .Where(n=> (n % 2) == 0)
    .Select(n=> n*n);
foreach(var n in query)
{
    Console.WriteLine(n);
}

/** output **
4
16
36
64
100
*/

PowerShell では以下のように書けます。

C#Select()に相当する処理はSelect-Object ではなくForEach-Objectを使いました。
また、このForEach-Objectは出力時のforeach ステートメントも兼ねています。

この一連の処理は遅延評価はされず、各コマンドレットへ値が渡った時点で即時実行されます。

PS C:\> 1..10 |
  Where-Object {($_ % 2) -eq 0 } |
  Foreach-Object { $_*$_ }

4
16
36
64
100

遅延評価版 Where、Select、Foreach

では、遅延評価のためのコマンドレットを作ってみます。
各コマンドレットの名前は Where-Block 、Select-Block、Foreach-Block とします。

Where-Block 、Select-Block はほぼ同じコードで、以下のような処理を行っています。

  • Where-Block では $Predicate パラメータに bool値を返すスクリプトブロックを指定します。
  • Select-Block では $Selector パラメータに 任意のオブジェクトを返すスクリプトブロックを指定します。
  • Process{}では、スクリプトブロックを返却します
  • スクリプトプロック内では以下の処理を行います
    • 入力がスクリプトブロックであれば、それを実行後、その戻り値を引数に$Predicate または $Selector を実行します。
    • 入力がスクリプトブロック以外であれば、その値を引数にPredicate または $Selector を実行します。
    • Where-Block では、$Predicate の戻り値が$true の場合のみ値を出力します。
    • Select-Block では、$Selector の戻り値をそのまま出力します。

Where-Block、Select-Block とコマンドをつなげて実行すると、つなげたコマンドの数だけネストしたスクリプトブロック(の配列)が得られます。 これを Foreach-Block で 一気に実行する、という仕組みです。

実行してみる

$Predicate,$Selector に渡ってくる引数はParam($x)でアクセスします。

PS C:\> $query = 1..10 | 
  Where-Block -Predicate {Param($x) ($x % 2) -eq 0} |
  Select-Block -Selector {Param($x) $x*$x}


PS C:\> $query | Foreach-Block
4
16
36
64
100

期待通りの結果にはなりました。が、本当に遅延評価になっているのかよくわかりません。
各コマンドレットに Write-Verbose を仕込んで確認してみましょう。

(Write-Verbose版のスクリプトはこちら https://gist.github.com/pierre3/c62db52d977bc2841ff4)

PS C:\> $query = 1..10 | 
  Where-Block -Predicate {Param($x) ($x % 2) -eq 0} -Verbose |
  Select-Block -Selector {Param($x) $x*$x} -Verbose


PS C:\> $query | Foreach-Block -Verbose
詳細: [Where] Input Data = 1
詳細: [Where] Predicate(1)
詳細: [Where] Input Data = 2
詳細: [Where] Predicate(2)
詳細: [Where] return 2
詳細: [Select] Predicate(2)
詳細: [Select] return 4
詳細: [Foreach] return 4
4
詳細: [Where] Input Data = 3
詳細: [Where] Predicate(3)
詳細: [Where] Input Data = 4
詳細: [Where] Predicate(4)
詳細: [Where] return 4
詳細: [Select] Predicate(4)
詳細: [Select] return 16
詳細: [Foreach] return 16
16
詳細: [Where] Input Data = 5
詳細: [Where] Predicate(5)
詳細: [Where] Input Data = 6
詳細: [Where] Predicate(6)
詳細: [Where] return 6
詳細: [Select] Predicate(6)
詳細: [Select] return 36
詳細: [Foreach] return 36
36
詳細: [Where] Input Data = 7
詳細: [Where] Predicate(7)
詳細: [Where] Input Data = 8
詳細: [Where] Predicate(8)
詳細: [Where] return 8
詳細: [Select] Predicate(8)
詳細: [Select] return 64
詳細: [Foreach] return 64
64
詳細: [Where] Input Data = 9
詳細: [Where] Predicate(9)
詳細: [Where] Input Data = 10
詳細: [Where] Predicate(10)
詳細: [Where] return 10
詳細: [Select] Predicate(10)
詳細: [Select] return 100
詳細: [Foreach] return 100
100

1行目の実行時には何も表示されず、Foreach-Block を実行した時点で一度に表示されました。
スクリプトブロックが順番に実行される様子も良く分かります。

スクリプトブロックにまとめる段階で一度 配列(コレクション)が展開されてしまうのは、ショウガナイ?