しかし (毎度のことながら)、PowerShell の柔軟さを持ってすれば、クロージャを実現することだって可能です。
今回は closure という名前の関数を作りました。この関数の引数に、クロージャとして機能させたいスクリプトブロックを渡せば、スクリプトブロックをクロージャ化できます。
例えば次のような使い方ができます。 (Wikipedia に掲載されている JavaScript のクロージャサンプルを移植)
function NewCounter
{
$i = 0;
return closure {
$i++;
return $i;
};
}
$counter = NewCounter;
&$counter; # 1
&$counter; # 2
&$counter; # 3
次のような使い方もできます。
function NewDecorator
{
param ([string]$decoration)
return closure {
param ([string]$text)
return $decoration + $text + $decoration;
};
}
$sharpDecorator = NewDecorator '#';
&$sharpDecorator "hoge"; # #hoge#
&$sharpDecorator "fuga"; # #fuga#
&$sharpDecorator "piyo"; # #piyo#
[ コード ]
クロージャを実現する closure 関数は次のようになっています。
この関数は更に InvokeClosureScript という関数を使用します。(正確には closure 関数が生成するスクリプトブロックの内部で使用します。)
closure 関数 + InvokeClosureScript 関数
function global:closure
{
param ([ScriptBlock]$private:script)
trap { break; }
# 引数の妥当性検証
if ($() -eq $script) { throw '引数 script が null です。' }
# 全てのクロージャを保存するクロージャストアを作成 (ハッシュテーブル)
if ($() -eq $global:ClosureStore)
{
Set-Variable 'ClosureStore' @{} -Scope 'global' -Option 'Constant, AllScope';
}
# GC に回収されているクロージャ (を格納しているハッシュテーブル要素) は、クロージャストアから削除
($ClosureStore.GetEnumerator() | ? { !$_.Value.IsAlive; }) | ? { $() -ne $_; } | % { $ClosureStore.Remove($_.Key); };
# 子スコープで環境 (自動変数を除く全ての変数) を取得し保存
$autoVariableNames =
@(
'$', '?', '^', '_', 'args', 'ConfirmPreference', 'ConsoleFileName', 'DebugPreference', 'Error', 'ErrorActionPreference',
'ErrorView', 'ExecutionContext', 'false', 'FormatEnumerationLimit', 'foreach', 'HOME', 'Host', 'input', 'LASTEXITCODE', 'lastWord',
'line', 'Matches', 'MaximumAliasCount', 'MaximumDriveCount', 'MaximumErrorCount', 'MaximumFunctionCount', 'MaximumHistoryCount', 'MaximumVariableCount', 'MyInvocation', 'NestedPromptLevel',
'null', 'OutputEncoding', 'PID', 'PROFILE', 'ProgressPreference', 'PSHOME', 'PWD', 'ReportErrorShowExceptionClass', 'ReportErrorShowInnerException', 'ReportErrorShowSource',
'ReportErrorShowStackTrace', 'ShellId', 'StackTrace', 'switch', 'true', 'VerbosePreference', 'WarningPreference', 'WhatIfPreference'
);
$private:environment = & { return Get-Variable | ? { $autoVariableNames -notcontains $_.Name }; };
# スクリプトと環境を組み合わせてクロージャを表す。
$private:closure =
New-Object 'PSObject' |
Add-Member 'Script' $script -MemberType 'NoteProperty' -PassThru |
Add-Member 'Environment' $environment -MemberType 'NoteProperty' -PassThru;
# GUID をキー、クロージャの弱参照を値とし、クロージャストアに保存
$private:closureId = [Guid]::NewGuid();
$ClosureStore.Add($closureId, [WeakReference]$closure);
# クロージャを実行するスクリプトを動的な文字列操作で構築 (スクリプトに GUID を埋め込むため)
$private:invokerText = "InvokeClosureScript `"$closureId`" `$Args;";
# テキストからスクリプトへ変換し、更に PSObject 化する
$private:invoker = [PSObject](Invoke-Expression "{ $invokerText }");
# 環境をスクリプトに結びつけることでスクリプトと環境の寿命を同一化する
Add-Member -InputObject $invoker -Name 'Closure' -Value $closure -MemberType 'NoteProperty';
return $invoker;
}
function global:InvokeClosureScript
{
param ([Guid]$private:closureId, [Array]$private:Args_)
# 指定した GUID に関連付いているクロージャをクロージャストアから取得 (クロージャは弱参照を使用して格納されている)
$private:closure = $ClosureStore[$closureId].Target;
# Null なら例外
if ($() -eq $closure) { throw '指定した ID に関連付けられたクロージャは存在しません。'; }
# 関数呼び出しを動的な文字列操作で構築 (param キーワードによる引数の受け取りが正常に機能するように)
$private:invokerText = 'param ([ScriptBlock]$private:script, [Array]$private:Args_) .$script';
for ($private:i = 0; $i -lt $Args_.Length; $i++) { $invokerText += " `$Args_[$i]"; }
# テキストからスクリプトへ変換
$private:invoker = Invoke-Expression "{ $invokerText }";
# 環境のロードとクロージャの実行は変数宣言を最小限にした子スコープで
$private:result =
&{
# 環境をロード
$Args[1].Environment | % { trap { continue; } $ExecutionContext.SessionState.PSVariable.Set($_); };
# クロージャを実行
return .$Args[0] $Args[1].Script $Args[2];
} $invoker $closure $Args_;
return $result;
}
[ closure 関数について ]
closure 関数は、受け取ったスクリプトブロックと環境 (自動変数を除く全ての変数) の組み合わせを一つのクロージャとして、クロージャストア (グローバルなハッシュテーブル) に保存します。クロージャストアのキーには、ランダムに生成された GUID を使用します。そして、この GUID を引数として InvokeClosureScript 関数を呼び出すスクリプト (インボーカー) を生成します。closure 関数はこのインボーカーを呼び出し元へと返します。
[ InvokeClosureScript 関数について ]
InvokeClosureScript 関数は、受け取った GUID を元にクロージャストアからクロージャ (スクリプトブロックと環境) を取得します。そして、環境をロードしてスクリプトブロックを実行するのですが、スクリプトブロックは環境がロードされるスコープと同一スコープで実行されます。これにより、本来親スコープで宣言されたはずの変数でもスクリプトブロックから変更することが可能となります。例えば冒頭の一つ目のサンプルコードでは、クロージャ (となるスクリプトブロック) の親スコープで $i が宣言されていますが、クロージャ内から $i を変更 (インクリメント) しています。
(なお、環境のロードとスクリプトブロックの実行は子スコープで行われますが、これは単に、InvokeClosureScript 関数内の変数がスクリプトブロックの実行に影響を与えないようにするためです。)
[ クロージャの寿命について ]
クロージャ (スクリプトブロックと環境) をクロージャストアに保存する際には、弱い参照を保存しています。これにより、クロージャは不要になると GC の対象になることができます。クロージャが GC に回収されただけでは、まだクロージャストアにエントリが残っていますが、次に closure 関数が呼び出された際にエントリも削除されます。
またクロージャは、インボーカーのノートプロパティとしても保存されています。これにより、インボーカーがどこからか参照されている限りは、クロージャは GC の対象になりません。
ただし、一つ注意点があります。インボーカーを変数に保持する代わりに次のように関数として保持してしまうと、(PSObject ではなく生のオブジェクトが保持されるため) ノートプロパティが失われてしまうのです。
$function:func1 = closure {}; # クロージャが GC の対象になってしまう
ノートプロパティが失われてしまうということは、クロージャが GC の対象になってしまうということです。一度変数に保持してから更に関数として保持すればノートプロパティは失われませんが、これも管理が複雑になるのでお勧めしません。
[ インボーカーのノートプロパティについて ]
インボーカーのノートプロパティにクロージャが保存されている理由はクロージャの寿命の制御のためですが、これは嬉しい副作用をもたらします。次のように、インボーカーのノートプロパティを通してスクリプトブロックと環境を確認することができるのです。
$closure1 = closure {};
$closure1.Closure.Script; # スクリプトブロックを確認
$closure1.Closure.Environment; # 環境を確認
【ダウンロード】
自作の PowerShell 関数は、以下の記事からまとめてダウンロードできます。
YokoKen.PowerShell.Scripts
# 変更履歴
2008/06/16
・GC に回収されているクロージャをストアから削除する処理にバグがあったため修正。
トラックバックURL↓
http://csharper.blog57.fc2.com/tb.php/218-06a353c6