2012年6月11日月曜日

Powershellのスクリプトブロックと変数スコープに関する疑問

PowerShell覚え書き

背景

 先日PowerShellのクロージャを試してみましたが、結局スクリプトブロック内外の変数のスコープがいまひとつ腑に落ちなかったので少し検証してみました。結論は特にないのですが、自分のメモとして残します。

1.再帰呼出しでの階乗関数

function factorial1([int]$n)
{
    if ( $n -eq 1 ) {return $n}
    $n * (factorial1 ($n-1))
}

factorial1 5      # => 120
factorial1 100    # => 9.33262154439441E+157
factorial1 170    # => 7.25741561530799E+306
factorial1 171    # => +∞
factorial1 300    # => +∞
factorial1 998    # => +∞
factorial1 999    # => 呼び出しの深さのオーバーフローのため、スクリプトが失敗しました。
 ごく普通の再帰呼び出し関数です。(末尾再帰になってません。)170!までは計算できましたが、[double]の精度を超えると無限大になりました。また、関数呼び出しの深さの最大値が1000までという制約があるために、998!より大きくなるとエラーが発生しました。

2.スクリプトブロックを使用した再帰呼出しの階乗関数

function factorial2_helper([int]$n)
{
    $block={
        param($i)
        if ( $i -eq 1) {return $script:f}
        $script:f = $script:f * $i
        factorial2_helper ($i-1)
    }
    &$block $n
}

function factorial2([int]$n)
{
    $script:f=1
    factorial2_helper $n
}

factorial2 5      # => 120
factorial2 100    # => 9.33262154439441E+157
factorial2 170    # => 7.25741561530799E+306
factorial2 171    # => +∞
factorial2 300    # => +∞
factorial2 498    # => +∞
factorial2 499    # => 呼び出しの深さのオーバーフローのため、スクリプトが失敗しました。
 スクリプトブロックにして、結果を保持する変数fを使うようにしました。関数をまたがって参照するので、変数fはスクリプトスコープです。また、関数呼び出しはスクリプトブロック化することで2倍に増えています。

3.クロージャに変更

function factorial3_helper([int]$n)
{
    $block={
        param($i)
        if ( $i -eq 1) {return $script:f}
        $script:f = $script:f * $i
        factorial3_helper ($i-1)
    }.GetNewClosure()
    &$block $n
}

function factorial3([int]$n)
{
    $script:f=1
    factorial3_helper $n
}

factorial3 5      # => $null
factorial3 100    # => $null
factorial3 170    # => $null
factorial3 171    # => $null
factorial3 300    # => $null
factorial3 498    # => $null
factorial3 499    # => 呼び出しの深さのオーバーフローのため、スクリプトが失敗しました。
 単純にクロージャにしたところご覧の通りエラーとなります。直接の理由はスクリプトスコープである変数fが参照できないためなんでしょうが、なぜクロージャの場合には同一スクリプト内なのに変数参照ができないのか不明です。

4.グローバルスコープに変更

function factorial4_helper([int]$n)
{
    $block={
        param($i)
        if ( $i -eq 1) {return $global:f}
        $global:f = $global:f * $i
        factorial4_helper ($i-1)
    }.GetNewClosure()
    &$block $n
}

function factorial4([int]$n)
{
    $global:f=1  #ここはscriptスコープでも結果は同じ
    factorial4_helper $n
}

factorial4 5      # => 120
factorial4 100    # => 9.33262154439441E+157
factorial4 170    # => 7.25741561530799E+306
factorial4 171    # => +∞
factorial4 300    # => +∞
factorial4 498    # => +∞
factorial4 499    # => 呼び出しの深さのオーバーフローのため、スクリプトが失敗しました。
 変数fのスコープをグローバルにしたらさすがに動きましたが、クロージャにしてもグローバル変数を直接さわっているのでナンセンスです。(もともとあまり意味はないんですけど。)

5.結果を持つ引数の追加

function factorial5_helper($i,$res)
{
    if ( $i -eq 1) {return $res}
    $res = $res * $i
    factorial5_helper ($i-1) $res
}

function factorial5($n)
{
    factorial5_helper $n 1
}

factorial5 5      # => 120
factorial5 100    # => 9.33262154439441E+157
factorial5 170    # => 7.25741561530799E+306
factorial5 171    # => +∞
factorial5 300    # => +∞
factorial5 997    # => +∞
factorial5 998    # => 呼び出しの深さのオーバーフローのため、スクリプトが失敗しました。
 スクリプトブロックをやめて末尾再帰になるようにしましたが、実測した感じではどうも末尾再帰の実装(ジャンプ)にはなってなさそうな雰囲気でした。

結論

 クロージャから参照・更新可能な変数スコープはやっぱりわかりません。スクリプトスコープの変数でも参照できないことがあることということで、難しいですね。

0 件のコメント:

コメントを投稿