qiita.com
こんなワンライナーがあります:
% perl -E 'say$!=24'
Too many open files
これの正体に迫っていきたいと思います。
どのように解釈されるのか
まずはこれがいったいどのようにParseされるのかを知るために、
-MO=Deparse
を付けて実行してみましょう。
% perl -MO=Deparse -E 'say$!=24'
use feature 'current_sub', 'evalbytes', 'fc', 'say', 'state', 'switch', 'unicode_strings', 'unicode_eval';
say $! = 24;
-e syntax OK
say $! = 24
キモはこれです。
$!
という変数に 24
を代入して、その式の結果を say
が受け取っています。
Perlの代入式は左辺値の値が取られるため、これは以下のコードと等価です。( 参考: perlop - Perl の演算子と優先順位 - perldoc.jp )
$! = 24;
say $!;
24
を代入したのに文字列が出てきた!?バグか!?と驚くかもしれません。
しかし、これは実はちゃんとPerlのドキュメントに明記されている挙動です。
特殊変数
Perlにおいては $!
のようにシジル($
など)のあとの変数名が記号で始まるものは特殊変数と呼ばれ、処理系によって特殊な扱われ方をする変数になっています。
特定の特殊変数がどのようなものかは perldoc -v '$!'
などとしたりすることで調べられます。
$!
とはなんなのかを調べてみましょう。perldoc.jp でも簡単に調べられます。
Perlの組み込み変数 $! の翻訳 - perldoc.jp
$!
はerrnoを扱うための特殊変数だということがわかります。
こんな記述があります:
参照されると、$! は C の errno 正数変数の現在の値を取得します。 $! に数値が代入されると、その値は errno に補完されます。 文字列として参照されると、$! は errno に対応するシステムエラー 文字列を返します。
つまり、そういう挙動をする特殊変数なので、そうなる。という感じです。
インターフェースに対する考察
でも、「文字列として参照されると」とは、どういうことなのか?
なんで、こんな不自然に見えるインターフェースになっているのか?
これにはPerl特有のコンテキストという考え方が関わってきます。
コンテキスト
Perlは文脈的多態言語 (Contextually Polymorphic Language)として作られています。
これは要するに、データ表現を単一のスカラにまとめ、そしてそのスカラを評価した文脈によってデータ表現を変えることによって、同一の概念を1つの変数で簡便に扱うための概念です。
参考資料:
http://perldoc.jp/docs/perl/5.26.1/perldata.pod#Scalar32values
https://www.slideshare.net/karupanerura/ss-98704540
https://www.slideshare.net/KondoYoshiyuki/yapc2012-20120929
このコンテキストを実現するための仕組みとして、Perlのスカラ変数は同一の値に対する複数の型のデータ表現を持つSV構造体として表現されます。
具体的にはPV/IV/NVという3つの値をSVに同時に持つことができます。
参考資料:
http://xsubtut.github.io/
https://metacpan.org/pod/B#OVERVIEW-OF-CLASSES
通常はこれらのIV/PVなどには同一のデータを単に型変換したデータが保持されます。
use Devel::Peek;
my $age = 17;
Dump $age;
print "私は永遠の $age 歳です\n";
Dump $age;
実行してみるとこのとおりです:
SV = IV(0x7ffe9080dd88) at 0x7ffe9080dd98
REFCNT = 1
FLAGS = (PADMY,IOK,pIOK)
IV = 17
私は永遠の 17 歳です
SV = PVIV(0x7ffe9080b820) at 0x7ffe9080dd98
REFCNT = 1
FLAGS = (PADMY,IOK,POK,pIOK,pPOK)
IV = 17
PV = 0x7ffe8f402de0 "17"\0
CUR = 2
LEN = 10
PV値に "17"
という値が増えて、FLAGS
に POK
が増えています。(PVはPointer Valueなのでその名の通り、実際にはその文字列へのポインタです)
内部的にはこのようにして同一の値に対する異なる表現のデータを持ち、コンテキストでその評価を変えるのがPerlのContextually Polymorphic Languageたる世界観なのです。
ソフトウェア工学的に正しいかどうかはさておき、データを直感的に扱うためのアプローチとしてはユニークで面白い概念なのではないかと個人的には思います。
dualvar
さて、これを見た賢い人は「つまり単一のスカラに違うデータを入れることも可能なのでは?」と考えると思います。
まさにそのとおりで、標準モジュールのScalar::Utilにdualvarという機能があります。
https://metacpan.org/pod/Scalar::Util#dualvar
これを使うとこういったことができます:
use Devel::Peek;
use Scalar::Util qw/dualvar/;
my $age = dualvar 17, 'seventeen';
Dump $age;
print "I'm $age years old.\n";
print "Next age: ", $age + 1, "\n";
結果としてはこうです:
SV = PVNV(0x7fa429802150) at 0x7fa42981da58
REFCNT = 1
FLAGS = (IOK,POK,IsCOW,pIOK,pPOK)
IV = 17
NV = 0
PV = 0x7fa42b200570 "seventeen"\0
CUR = 9
LEN = 11
COW_REFCNT = 1
I'm seventeen years old.
Next age: 18
IV/PVに別々の値が入っており、それがそれぞれのコンテキストに応じて使われている事がわかります。
即ち、Perlではerrnoとそのエラーメッセージは表裏一体の同一概念として扱えるべきだと考えて $!
をdualvarのようなスタイルで実装していると考えられます。
ちなみに、一応書いておくと、dualvarを通常のソフトウェア開発で使うべき場面は基本的には存在しないと思います。
$!の正体に迫る
ここまでの知識を前提知識として知っていると $!
はdualvarのようにして実装されているのでは?と想像できると思います。
実際に実行してみましょう:
use Devel::Peek;
$! = 24;
say $!;
Dump $!;
やはりIVにはerrnoが、PVにはそのエラーメッセージが格納されています。
Too many open files
SV = PVMG(0x7fb7ce827140) at 0x7fb7ce806ff8
REFCNT = 1
FLAGS = (GMG,SMG,NOK,POK,pNOK,pPOK)
IV = 24
NV = 24
PV = 0x7fb7cdc07b00 "Too many open files"\0
CUR = 19
LEN = 21
MAGIC = 0x7fb7cdc08ec0
MG_VIRTUAL = &PL_vtbl_sv
MG_TYPE = PERL_MAGIC_sv(\0)
MG_OBJ = 0x7fb7ce807058
MG_LEN = 1
MG_PTR = 0x7fb7cdc028d0 "!"
では $!
をsay
の文字列コンテキストで評価する前に参照してみるとどうでしょうか?
use Devel::Peek;
$! = 24;
Dump $!;
なんとPVは0です:
SV = PVMG(0x7fc936815b40) at 0x7fc93581d9f8
REFCNT = 1
FLAGS = (GMG,SMG,IOK,pIOK)
IV = 24
NV = 0
PV = 0
MAGIC = 0x7fc93570a450
MG_VIRTUAL = &PL_vtbl_sv
MG_TYPE = PERL_MAGIC_sv(\0)
MG_OBJ = 0x7fc93581da58
MG_LEN = 1
MG_PTR = 0x7fc93570a480 "!"
$!
を評価したときに何かが起きて、IVに応じたPVがセットされた。ということがわかります。
これを実行しているのがMAGICと呼ばれる仕組みです。
MAGIC
MAGICは変数の操作に対してフックするための仕組みです。
以下のようなことができます:
関数ポインタ その振る舞い
---------------- ------------
svt_get SV の値が取得された前に何かを行う。
svt_set SV に値を代入した後で何かを行う。
svt_len SV の長さを報告する。
svt_clear SV が表わしているものををクリアする。
svt_free SV に結び付けられてい領域を解放する。
svt_copy tie された変数のマジックを tie された要素にコピーする
svt_dup スレッドのクローン化中にマジック構造体を複製する
svt_local 'local' 中にマジックをローカル変数にコピーする
出展: https://perldoc.jp/docs/perl/5.20.1/perlguts.pod#Magic32Variables
$!のMAGIC
Devel::Peekによると MG_TYPE = PERL_MAGIC_sv
とあるので先のドキュメントと照らし合わせるとやはり特殊スカラ変数向けのMAGICが内部的に定義されていることが分かります。
mg_type
(old-style char and macro) MGVTBL Type of magic
-------------------------- ------ -------------
\0 PERL_MAGIC_sv vtbl_sv 特殊スカラ変数
該当箇所のソースを追うと、 mg_vtable.hでこれが定義されており Perl_magic_set
と Perl_magic_get
によって実際の処理が行われていることが分かります。
https://github.com/Perl/perl5/blob/v5.30.0/mg_vtable.h#L191
$! = 24
のように代入されるタイミングで Perl_magic_set
が実行され、 MG_PTR = 0x7fc93570a480 "!"
なので以下の分岐に入ってきます。
https://github.com/Perl/perl5/blob/v5.30.0/mg.c#L3074-L3089
代入元のスカラを数値コンテキストで評価したときと同じ値を、
SETERRNOマクロで一般的な errno.h
の提供するグローバル変数のerrnoにもセットしています。VMSの場合は挙動が少し違うようです。
https://github.com/Perl/perl5/blob/v5.30.0/perl.h#L1330
そして $!
が評価されるタイミングで Perl_magic_get
が実行され、errnoをSVのIVスロットにセットし直して最終的にはstrerror関数によってerrnoのエラーメッセージを取得してPVスロットにセットしています。
https://github.com/Perl/perl5/blob/v5.30.0/mg.c#L1003-L1032
https://github.com/Perl/perl5/blob/v5.30.0/embed.h#L843
https://github.com/Perl/perl5/blob/v5.30.0/mg.c#L883-L897
こうして $!
の挙動がどのように実現されていることが分かりました。
まとめ
$!
はPerlの特殊変数でerrnoを扱います。その挙動はMAGICによって実装されており、dualvarのように振る舞います。
$!
を使うときはコンテキストによって表現を変えるのでコンテキストを適切にコントロールしましょう。