20
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

powershellのクロージャを理解する

Last updated at Posted at 2015-12-01

powershellには、無名関数の代わりにスクリプトブロック{ .. }がある。
無名関数といえば、自分の経験では普通クロージャなのだが、powershellのそれはなぜかクロージャではない。

foo.ps1
function foo {
  $n = 1 # (1)
  {
    $n = $n + 1
    $n
  }
}

$f = foo

$n = 0 # (2)

# 以下、クロージャであれば関数fooの変数$nがクロージャに束縛され、2, 3, 4 がそれぞれ出力される
& $f # => 1が出力される。期待するのは2
& $f # => 1が出力される。期待するのは3
& $f # => 1が出力される。期待するのは4

powershellのスクリプトブロックの挙動は以下のとおりである。

  • スクリプトブロック { .. } は変数を束縛しない
  • powershell は変数のスコープの解決をレキシカルには行わない(ダイナミックスコープという。$f は、関数 foo の変数(1)ではなく、$f を実行した時点で定義してある変数(2)を参照している)

しかし、powershellにはversion2から本物のクロージャが導入されている。当初、このクロージャの動作がなかなか理解できなかったがどうやら以下の様な挙動をするらしいことがわかった。

(a) スクリプトブロックに対して GetNewClosure() を呼び出すとスクリプトブロックをクロージャに変換して返す
(b) クロージャは変数の「コピー」を束縛する
(c) 束縛した変数にアクセスするには変数に script: 修飾子を加える必要がある(少なくとも代入時にはscript:修飾子がないとローカルスコープの変数を作ってしまう)クロージャ内でscript:修飾した変数は本来のスクリプトスコープの変数ではなく、クロージャ内のスコープを表している(ここがなかなか理解できない点であった)

※ 上記は、Windows Powershell In Action, 2nd Edition を見る機会があり、拾い読みして理解した内容である。

この規則に倣って、先のスクリプトを書き直すと以下のようになる

foo-closure.ps1
function foo {
  $n = 1
  {
    $script:n = $n + 1    # (c)
    $n
  }.GetNewClosure()       # (a)
}


$f = foo

$n = 0

& $f # => 2
& $f # => 3
& $f # => 4

期待通りの動作になった。ただ、(b)の挙動については確認できていない。これは以下のような例で確認できる。

bar.ps1
function bar {
  $n = 1                # (1)

  $f = {
    $script:n = $n + 1  # (2)
    $n
  }.GetNewClosure()

  $n = 9                # (3)

  $f
}

$f = bar

$n = 0                  # (4)

& $f # => 2が出力される。期待するのは10
& $f # => 3が出力される。期待するのは11
& $f # => 4が出力される。期待するのは12

期待するのは、(1), (2), (3) は同じ変数で、(4) は異なるスコープの変数である。従って $f を実行するときには、9 からインクリメントして欲しい。しかし、実際には(b)の規則により(1)のコピーが(2)となっており、1からインクリメントされている。

これは、以下の様な2つのクロージャで変数を共有することができないことを意味する。

baz.ps1
function baz {
  $n = 1                  # (1)
  $f1 = {
    $script:n = $n + 1    # (2)
    $n
  }.GetNewClosure()

  $f2 = {
    $script:n = $n + 1    # (3)
    $n
  }.GetNewClosure()

  $f1, $f2
}


$f1, $f2 = baz

$n = 0                    # (4)

& $f1 # => 2が出力される。
& $f2 # => 2が出力される。期待するのは3
& $f1 # => 3が出力される。期待するのは4
& $f2 # => 3が出力される。期待するのは5

この例も、期待するのは (1), (2), (3) は同じ変数で、(4) は異なるスコープの変数なのだが、実際は(2), (3) は (1) を個々にコピーした別々の変数になっている。

このpowershellの挙動について、困るのは以下の点である。

  • レキシカルに同じ変数に見えるものが実際には別々のスコープの変数である(そのため、ソースの見た目でスコープがわかりにくい)

もう少し、有り得そうな例で言うと

qux-closure.ps1
function qux {
  param($f)

  & $f
}

$n = 1
qux { $script:n = $n + 1 }.GetNewClosure()
$n  # => 1が出力される。期待するのは2

上記例で、スクリプトブロックが外のスコープを変更することができないのである。以下で変更できるにも関わらず!

$n = 1
ForEach-Object { $n = $n + 1 }
$n  # => 2

以下、参考までに上記の例と同じクロージャ定義をjavascriptとrubyで記述した。もちろんこれらの言語の挙動が絶対的に正しいというわけではないかもしれない(こんな話もある)。しかし、これらの言語の挙動に慣れていると裏切られ、理解し難い気持ちになるので最初から powershell は違うものだと覚悟しておく必要がある。

Javascriptの場合

foo.js
function foo() {
  var n = 1;
  return function() {
    n = n + 1;
    return n;
  }
}

var f = foo();

var n = 0;

console.log(f()); // => 2
console.log(f()); // => 3
console.log(f()); // => 4
bar.js
function bar() {
  var n = 1;

  var f = function() {
    n = n + 1;
    return n;
  }

  n = 9;

  return f;
}

var f = bar();

var n = 0;

console.log(f()); // => 10
console.log(f()); // => 11
console.log(f()); // => 12
baz.js
function baz() {
  var n = 1;
  var f1 = function () {
    n = n + 1;
    return n;
  }

  var f2 = function () {
    n = n + 1;
    return n;
  }

  return [f1, f2];
}


var f = baz();
var f1 = f[0];
var f2 = f[1];

var n = 0

console.log(f1()); // => 2
console.log(f2()); // => 3
console.log(f1()); // => 4
console.log(f2()); // => 5
qux.js
function qux(f) {
  f();
}

var n = 1
qux(function () { n = n + 1 });
console.log(n); // => 2

Rubyの場合

foo.rb
def foo
  n = 1
  lambda {
    n = n + 1
  }
end

f = foo

n = 0

p f.call # => 2
p f.call # => 3
p f.call # => 4
bar.rb
def bar
  n = 1

  f = lambda {
    n = n + 1
  }

  n = 9

  f
end

f = bar

n = 0

p f.call # => 10
p f.call # => 11
p f.call # => 12
baz.rb
def baz
  n = 1
  f1 = lambda {
    n = n + 1
  }

  f2 = lambda {
    n = n + 1
  }

  [f1, f2]
end


f1, f2 = baz

n = 0

p f1.call # => 2
p f2.call # => 3
p f1.call # => 4
p f2.call # => 5
qux.rb
def qux
  yield
end

n = 1
qux { n = n + 1 }
p n # => 2
20
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?