Perl Perl_5
Perl クロージャによる永続的なプライベート変数 closure, state (0x26c)

目次 - Perl Index
Theme
Perl について、復習を兼ねて断片的な情報を掲載して行く連載その 0x26c 回。
Perl で、永続的なプライベート変数を設定する「 クロージャ 」( closure ) と、レキシカルな永続的な変数を宣言出来る演算子「 state 」を確認する。
変数のスコープ
Perl を含む多くのプログラミング言語の変数 ( variable ) には、スコープという有効範囲が設定されています。
(0x32) 等で確認した通り、一般に、最も大きなスコープを持つ変数を「 グローバル変数 」と呼び、より小さなスコープを持つ変数を「 ローカル変数 」や「 プライベート変数 」と呼びます。
Perl では、最も小さいプライベート変数を「 レキシカル変数 」と呼び、演算子「 my 」で宣言します。
レキシカルスコープを使ったカウント
演算子「 my 」で宣言したカウンタ用途の変数を利用してカウントアップするサブルーチンを書いた場合に、次のように変数を宣言するブロック ( スコープ ) を誤ると、サブルーチンはカウンタとして機能しません。
sub counter {
my $count;
return ++$count;
}
foreach (0..4) {
print "$_: ", counter(), "\n";
}
上記を実行すると、次のような結果になります。
0: 1
1: 1
2: 1
3: 1
4: 1
なぜなら、サブルーチン「 counter() 」を呼び出すたびにスカラ変数「 $count 」の値が初期化されているからです。
ですから、カウントアップを適切に行うには、次のように変数の初期化をサブルーチンのブロック ( スコープ ) の外部で行います。
my $count;
sub counter {
#my $count;
return ++$count;
}
foreach (0..4) {
print "$_: ", counter(), "\n";
}
上記を実行すれば、次の結果が得られます。
0: 1
1: 2
2: 3
3: 4
4: 5
カウンタ用途のサブルーチン「 counter() 」が意図通りに機能しています。
問題点
前項で意図通りに機能したカウンタ用途のサブルーチンには、問題点があります。
それは、カウンタ用のスカラ変数「 $count 」がグローバル変数になったため、すべてのスコープからアクセス出来てしまうことです。
次の例では、foreach 文のブロック内でわかりやすく変数「 $count 」にアクセスしていますが、プログラムコードの量が増えれば増えるほど、意図しない場所から意図しないアクセスが発生するリスクが高くなります。
my $count;
sub counter {
return ++$count;
}
foreach (0..4) {
print "$_: ", counter(), "\n";
# Oops!
++$count;
}
上記を実行すると、結果は次のようになります。
0: 1
1: 3
2: 5
3: 7
4: 9
foreach 文のブロック内で変数「 $count 」をカウントアップしているため、サブルーチン「 counter() 」は、図らずも奇数カウンタになってしまっています。
クロージャによって「 永続変数 」化する
perlsub - perldoc.jp によれば、単純に「 { } 」ブロックを利用して次のようにスコープを閉じる ( close ) することで、前項の問題点を回避出来るといいます。
# $count のスコープを閉じる
{
my $count;
sub counter {
return ++$count;
}
}
foreach (0..4) {
print "$_: ", counter(), "\n";
# Error by strict paragma
++$count;
}
「 { } 」ブロックのスコープに「 $count 」を閉じ込めて、スコープ外に存在する foreach ブロック内からの「 $count 」の呼び出しを閉め出してあります。
これによって、サブルーチン「 counter() 」内の変数「 $count 」の値が保護されます。
perlsub によれば、この時の変数「 $count 」を「 クロージャによる永続変数 」と呼ぶそうです。また、この時の「 クロージャ 」はサブルーチン「 counter() 」です。
なお、上記コードを、プラグマ「 warnings 」(0x15) を use した状態で実行するとメッセージ「 Name "main::count" used only once: possible typo at ... 」といった警告が発生します。
プラグマ「 strict 」(0x35) を use した状態では、「 Global symbol "$count" requires explicit package name at ... 」によるエラーが発生してプログラムは実行出来ません。
クロージャ
現在の僕は、前項のスカラ変数「 $count 」は任意のローカルスコープに閉じられた ( closed ) 変数で、サブルーチン「 counter() 」は、その閉じられた変数の操作を行える「 クロージャ 」( closure ) だと理解しています。
「 クロージャ 」については [JavaScript] 猿でもわかるクロージャ超入門 まとめ - DQNEO 起業日記 を読むとよく理解出来ます。
著者の DOQNEO さんは YAPC::Asia 2015 のライトニングトークで「 Git の作り方 」というネタを披露されています。Twitter のアイコンから猛々しい DQN めいた方を想像していましたが、ブログの語り口同様の優し気な方でした。
せっかくなので「 猿でもわかるクロージャ超入門 5 」のクロージャを Perl で実装してみます。
次のコードでは、サブルーチン「 outer() 」の中の無名サブルーチンが「 クロージャ 」になっていて、「 outer() 」を呼び出すことでその「 クロージャ 」の「 コードリファレンス 」( コードレフ ) を取得します。
クロージャを実行するには、クロージャがコードレフであることから「 デリファレンス 」してあげる必要があります。
#!/usr/bin/perl
use strict;
use warnings;
# 戻り値としてコードレフのクロージャを返す
sub outer {
my $count = 1;
# クロージャ ( 無名サブルーチン )
sub {
print "$count\n";
$count = $count + 1;
};
}
# outer() を呼び出してクロージャ ( コードレフ ) を格納
my $x = outer();
my $y = outer();
# デリファレンスで $x からクロージャを呼び出して実行
foreach (0..2){
$x->();
}
print "---\n";
# デリファレンスで $y からクロージャを呼び出して実行
foreach (0..2){
$y->();
}
上記コードを実行すると、次の出力が得られます。上段が「 $x->() 」で下段が「 $y->() 」の出力です。
1
2
3
---
1
2
3
なお、Perl の「 リファレンス 」や「 デリファレンス 」という考え方や使い方は「 続・初めての Perl 」で紹介されることになります。
Perl のクロージャについては、第20回 リファレンス入門(3)- Perl Hackers Hub も参考になります。こちらの著者 近藤 嘉雪 さんは、この blog でお世話になっている書籍「 初めての Perl 」を翻訳されている方です。
演算子「 state 」
Perl 5.10 から、永続的なレキシカル変数を宣言出来る演算子「 state 」が導入されています。
この演算子を利用して宣言されたレキシカル変数は、次のように記述しても再初期化されることがありません。
use 5.010;
sub counter {
state $count;
++$count;
}
foreach (0..2) {
print counter(), "\n";
}
つまり、上記コードを実行すると次の結果が得られます。
1
2
3
サブルーチン内のスカラ変数「 $count 」が、再初期化されることなく加算されていることがわかります。
演算子「 state 」を利用したクロージャの失敗
演算子「 state 」を利用すれば、外側のルーチンを書くことなくクロージャのような機能が出来るかもしれない。と思い立ち、次のコードを書いてみましたが結果は意図とは異なるものでした。
use 5.010;
my $code_ref = sub {
state $count = 1;
print "$count\n";
$count++;
};
my $x = $code_ref;
my $y = $code_ref;
foreach (0..2) {
$x->();
}
print "---\n";
foreach (0..2) {
$y->();
}
実行結果は次の通りです。
1
2
3
---
4
5
6
次のように、単純に my の代わりとして「 state 」を利用しましたが、結果は上記と同じでした。
use 5.010;
sub outer {
state $count = 1;
sub {
print "$count\n";
$count++;
};
}
演算子「 state 」の機能を考えれば当然の結果ですが、この失敗によって「 クロージャ 」の理解が進んだように思えます。
クロージャを利用したい場合は、外側のサブルーチン「 outer 」が呼び出されるごとに「 $count 」が初期化されることと、それがクロージャ用のサブルーチンに封じ込められる必要があるわけですね。
0x26c -> 0x26d へ
次回は、リストと配列の「 スライス 」という機能を確認します。
参考情報は書籍「 初めての Perl 第 6 版 」を中心に perldoc, Wikipedia および各 Web サイト。それと詳しい先輩。
目次 - Perl Index
Perl mp2 翻訳 Web コンテンツ圧縮の FAQ (d228)
Perl mp2 翻訳 既知のブラウザのバグの回避策をいくつか (d227)
Perl mp2 翻訳 Perl と Apache でのキュートなトリック (d226)
Perl mp2 翻訳 テンプレートシステムの選択 (d225)
Perl mp2 翻訳 大規模 E コマースサイトの構築 (d224)
Perl mp2 翻訳 チュートリアル (d223)
Perl mp2 翻訳 既知のブラウザのバグの回避策をいくつか (d227)
Perl mp2 翻訳 Perl と Apache でのキュートなトリック (d226)
Perl mp2 翻訳 テンプレートシステムの選択 (d225)
Perl mp2 翻訳 大規模 E コマースサイトの構築 (d224)
Perl mp2 翻訳 チュートリアル (d223)