20190226のPerlに関する記事は1件です。

[Perl] 初期化と入れ子データ構造のハマりどころ

この記事は、Perl の初期化のおさらいと、Perl でリファレンスを使ってネストしたデータ構造をつくり操作するさい、初期化をめぐってちょっとした落とし穴があるよ、という Tips です。

Perl における変数の初期化

Perl でも、変数を宣言と同時に初期化 (initialize) したり、宣言だけしておいて後で初期化したりすることができます。

my ($var1, $var2) = (23, 42);
my $var3;
...
$var3 = 'foo';

ここまでは、他の一般的な言語と同様です。

初期化しないで使うと...

しかし、Perl では変数を初期化せずとも使えるシーンがいろいろあります。まず配列変数やハッシュ変数については、接頭辞 (sigil) によって、それが配列やハッシュとして初期化されるべきことが明らかなので、わざわざ書く必要がありません。

test1.pl
use 5.28.0;
use warnings;

my (@arr, %hash);
# 明示的に初期化するなら my @arr = (); my %hash = ();

push @arr, 'element';  # 何の問題もない
$hash{key} = 'value';  # 何の問題もない
say "\@arr has $arr[0], and \%hash has $hash{key}.";
# > @arr has element, and %hash has value.

スカラー変数については、初期化せず使うのは、多くの場合望ましいやりかたではありませんが、use warnings;していれば警告がうるさいぐらいの話で、べつに可能です。初期化されていないスカラー変数は、perl1 によって次のように扱われます:

  • undef (未定義値。特殊なコンテクストがなければ)
  • 0 (数値コンテクストで評価されれば)
  • '' (空文字列。文字列コンテクストで評価されれば)

これらは、組込みブール型を持たない Perl において、いずれも「偽」と判定される値です。いちおうサンプルプログラムをどうぞ。

test2.pl
use 5.28.0;
use warnings;

my $var;

say '$var is undefined.' if not defined $var;
# 警告なし
# > $var is undefined.
say '$var is likewise undefined.' if not $var;
# これでも警告なし (defined チェックは要らない)
# > $var is likewise undefined.
say $var + 42;
# > Use of uninitialized value $var in addition (+) at test2.pl line 12.
# > 42
say "foo${var}bar";
# > Use of uninitialized value $var in concatenation (.) or string at test2.pl line 15.
# > foobar
say '$var seems to be 0.' if $var == 0;
# > Use of uninitialized value $var in numeric eq (==) at test2.pl line 19.
# > $var seems to be 0.
say q|$var seems to be ''.| if $var eq '';
# > Use of uninitialized value $var in string eq at test2.pl line 21.
# > $var seems to be ''.

警告の出る書き方については、常用するものではありません (書き捨てのスクリプトは除く)。

コンテクストにご注意

ちなみに、文字列は'''0'以外なら「真」と判定されるので、例えば'hello'はもちろん真です。しかし、どんな文字列でも数値として評価された場合は0になるので、'hello' + 'hello' を判定すれば偽になります (演算子+が両辺に数値コンテクストを与えます)。これも、初心者がハマりやすいポイントかもしれません2
次のコード片では、4つのsayはすべて実行されます:

my $str = 'hello';
say q|$str is true.|       if     $str;
say q|$str + '' is false.| unless $str + '';  # 数値 0 と評価 (偽)
my $num = 0;
say q|$num is false.|      unless $num;
say q|$num . 0 is true|    if     $num . 0;  # 文字列 '00' と評価 (真)

warningsプラグマを有効にしていれば、$str + ''を評価する箇所のみに警告が出ます。$num . 0には、出ません。数値を文字列に昇格 (promote) するのはふつうのことでも、文字列を数値へ降格 (demote) させるシチュエーションは、ちょっと異常だからでしょう。
数値は文字列になっても、さいわいに、その数値が0以外であればやはり真のままです。とくに、真なる数値は真のままです。他方、文字列が数値になると、その文字列が''だろうとなかろうと、関係なく偽になってしまいます。真なる文字列でも偽 (0) になります
よく気をつけましょう (というか、だからuse warnings;しましょう)。

autovivification (自動賦活)

ところで、スカラー変数には、数値や文字列のほかに、(ハード) リファレンスが格納されることがありますね。リファレンスの初期化はどうなっているのでしょうか? Perl には、無名配列コンストラクタ[, ]と、無名ハッシュコンストラクタ{, }が用意されています。従って、$arefという変数を無名配列への参照で初期化したければ、my $aref = [];とすればよいわけです。ところが驚くべきことに、この初期化は必要ありません。

test3.pl
use 5.28.0;
use warnings;

my ($aref, $hashref);  # ただスカラー変数を宣言しただけ
# 明示的に初期化するなら my $aref = []; my $hashref = {};
my $k = 'key';

$aref->[23]    = 'hello';  # 配列としてデリファレンス
$hashref->{$k} = 'world';  # ハッシュとしてデリファレンス
say "$aref->[23], $hashref->{$k}!";
# > hello, world!

say $#{$aref};  # @$aref の最後の要素のインデクス
say "$aref";    # 参照されている無名配列のメモリアドレス
# > 23
# > ARRAY(0x38c428)

$hashref->{foo}->{bar} = 'baz';
say q|%{$hashref} has the key 'foo'.| if exists $hashref->{foo};
# > %{$hashref} has the key 'foo'.

$aref->{$k} = 42;  # ランタイムエラー (強制終了)
# > Not a HASH reference at test3.pl line 22.

上のコードでは、宣言されただけで初期化されていないスカラー変数を、いきなり配列やハッシュとしてデリファレンスしています。何のエラーも出ません。それでいいのです。実は、Perl では autovivification (自動賦活) という機能が標準で利用可能になっており、それがリファレンスであろうとなかろうと、左辺値 (lvalue) として使われた配列のエントリやハッシュのキーは、すべて自動的に存在することになってしまう (生成される) のです。従って、配列変数やハッシュ変数を初期化しないでよいように、無名配列や無名ハッシュへのリファレンスもまた初期化する必要はありません。もちろん、デリファレンスされる前にはundefが格納されています。いったん左辺値として使われると、$arefは長さ24 (最後の要素のインデクスは23) の無名配列を参照するようになり、$aref->[3]$aref->[7]等にはundefが格納されます。その無名配列は、メモリアドレス 0x38c428 に存在している、というわけです (アドレスの生の値は環境によって異なります)。
同様に、$hashref->{foo}->{bar}をひとたびデリファレンスして使えば、exists $hashref->{foo}は「真」の値を返すことになります。keys %$hashrefは、この時点で('key', 'foo')になります。代入によって、無名ハッシュ{bar => 'baz'}が生成され、同時にそのメモリアドレスが$hashref->{foo}に格納されたのです。
最後に、すでに$arefには、メモリアドレス 0x38c428 に存在する無名配列が格納されました。従って、それをハッシュとしてデリファレンスしようとした$aref->{$k}は、実行時エラーを返します。配列はハッシュではないからです。このように、最初はただの初期化されていないスカラー変数だったものが、デリファレンスされることによって無名配列にも無名ハッシュにも姿を変え、しかも明示的に再代入を行なわないかぎり元には戻らないのです。変数の型などというものを超越して、ここに Perl の妙味があります。
なお、このような自動賦活はバグの温床にもなりやすく、no autovivification;を使うことで、perl の挙動を変更することができます。その詳細は、autovivificationとうまく付き合う 等の記事をご覧ください。

入れ子データ構造の罠

ネストなんて簡単だ

Perl で複雑なデータ構造を構築するには、リファレンスのお世話になるしかありません。Perl はリストを平坦化 (flatterize) してしまうので、((10, 20, 30), (40, 50, 60))などとしても、多次元配列を作ることはできません (これは、ただの(10, 20, 30, 40, 50, 60)になってしまいます)。しかし、無名配列コンストラクタを使ってmy @refarr = ([10, 20, 30], [40, 50, 60])などとやると、これは立派な多次元配列で、$refarr[0]->[2]30を参照できます (さらに、$refarr[0][2]と略記もできます)。
スカラー変数は、左辺値としてデリファレンスするだけで、途中に参照されたものはすべて自動賦活されるのでした。

my $rec;
$rec->{foo}->[23]->{bar}->[42]->{baz} = sub { return $_[0] };

従って、こうするだけで、$recはある無名ハッシュを参照し、その無名ハッシュの中の無名配列の中の無名ハッシュの中の無名配列の中の無名ハッシュのキー'baz'が参照するところのメモリアドレスに対して、無名サブルーチンリファレンスが格納されます。続いて、

say $rec->{foo}->[23]->{bar}->[42]->{baz}->('hello, world!');

とやれば、無名サブルーチンリファレンスが参照している無名サブルーチンが実行され、STDOUThello, world!が出力されます。
これは何と素晴らしいことでしょう! これを逐一初期化して、

my $rec = {};
$rec->{foo} = [];
$rec->{foo}->[23] = {};
$rec->{foo}->[23]->{bar} = [];
$rec->{foo}->[23]->{bar}->[42] = {};
$rec->{foo}->[23]->{bar}->[42]->{baz} = sub { return $_[0] };

などと書いたり、シリアライズして、

my $rec = {
    foo => [
        ((undef) x 23), {
            bar => [
                ((undef) x 42), {
                    baz => sub {
                        return $_[0];
                    },
                },
            ],
        },
    ],
};

などと書いていたら、日が暮れてしまいますね。

初期化が必須のこともある

上の例で、$rec->{foo}->[23]->{bar}->[42]以下の無名ハッシュを操作する場合、my $foo23bar42 = $rec->{foo}->[23]->{bar}->[42];などと、いったん作業変数に格納してしまうのがよいでしょう。そうして$foo23bar42->{qux}->{quux} = 'ho!'としてやれば、それは元の$recからも参照されているので、$rec->{foo}->[23]->{bar}->[42]->{qux}->{quux}にも文字列'ho!'が登録されています。これは単に、参照の仕方のエイリアスです。しかし、$foo23bar42$rec->{foo}->[23]->{bar}->[42]で初期化だけして、'ho!'の代入の前に$foo23bar42->{qux}を右辺値で参照したならば、その値はただのundefです。$rec->{foo}->[23]->{bar}->[42]->{qux}のところに無名ハッシュは生成されていませんし、$foo23bar42->{qux}はけして無名ハッシュへの参照などではありません。このことが、ちょっとした落とし穴になります。
つまり、キーに値が登録されていないとき、そのキーを左辺値として無名ハッシュで初期化すれば、そのキーの値には生成された無名ハッシュのメモリアドレスが登録されますが、単に右辺値としてキーが参照されただけでは、値としてundefが返されるだけだ、ということです。わかりにくいので、以下のサンプルプログラムをご解読ください。

test4.pl
use 5.28.0;
use warnings;
use Data::Dumper qw(Dumper);

my $hashref1;
my $hashref2;

$hashref1->{now} = {};  # $hashref1->{now} の値を 無名ハッシュへの参照で初期化する
;                       # $hashref2->{now} の値を初期化しない

my $now1 = $hashref1->{now};
my $now2 = $hashref2->{now};
$now1->{is_morning} = 1;
$now2->{is_morning} = 1;

say $hashref1->{now}->{is_morning} ? 'Good Morning!' : 'Good Evening!';
# > Good Morning!
say $hashref2->{now}->{is_morning} ? 'Good Morning!' : 'Good Evening!';
# > Good Evening!

say Dumper $hashref1;
# > $VAR1 = {
# >           'now' => {
# >                      'is_morning' => 1
# >                    }
# >         };
# >
say Dumper $hashref2;
# > $VAR1 = {
# >           'now' => {}
# >         };
# >

say "\$hashref1->{now}: $hashref1->{now}, \$now1: $now1";
# > $hashref1->{now}: HASH(0x58c428), $now1: HASH(0x58c428)
say "\$hashref2->{now}: $hashref2->{now}, \$now2: $now2";
# > $hashref2->{now}: HASH(0x332650), $now2: HASH(0x332620)

$hashref1->{now} = {};という初期化ステートメントによって、$hashref1->{now}には無名空ハッシュ{}のメモリアドレスが登録されます。それは 0x58c428 です。従って$now1もまた、同じメモリアドレス 0x58c428 を参照し、そこに存在する無名ハッシュを操作します。よって$hashref1->{now}->{is_morning}は、$now1->{is_morning}のエイリアスです。
しかし、$hashref2->{now}は右辺値として参照されているだけであり、単にundefを返します。従って$now2にも (メモリアドレスではなく) undefが格納され、これは初期化されていないスカラー変数と同じことです。$now2->{is_morning}としてデリファレンスされたとき、新しい無名ハッシュが自動賦活され、そのメモリアドレスが 0x332620 でした。これは、後になって$hashref2->{now}->{is_morning}が評価されたその瞬間に生成された、$hashref2->{now}が参照する、メモリアドレス 0x332650 に存在する無名ハッシュと、何の関係もありません。他方、$now1が参照しているメモリ番地 0x58c428 は、$hashref1->{now}が参照しているメモリ番地と同じものであり、$now1->{is_morning}$hashref1->{now}->{is_morning}指しているところのものを指示できるのです。
従って、my $now2 = $hashref2->{now};などと右辺値を参照しただけでは、同じ指示対象を指示するのには不足です。そのためには、あらかじめ$hashref2->{now} = {};などとして、$hashref2->{now}が参照で初期化され、$hashref2->{now}に個別具体的なメモリアドレスが格納され、それが$now2と同じものを指している、という事実が確保されなければならないのです (メモリアドレスが同一かどうかは、if ($now1 == $hashref1->{now}) {...} などとして、テストもできます)。
ロジック風のタームを使えば、このサンプルプログラムで、$now1はのっけから束縛変数として初期化されていますが、$now2はデリファレンスによってアクセスされるまでは、ただの自由変数なのですね。

まとめ

「Perl のリファレンスでは初期化は不要」という曖昧な記憶をしていると、単にそれをリファレンスとして使うだけならば問題ありませんが、特定の「それ」を指示するようなリファレンスとして使いたい場合には、よくよく注意して扱わなければならない、という Tips でした。


  1. 小文字の perl とは、大文字の Perl (=言語仕様) の、唯一の処理系の実装のことを指します。 

  2. 特に、JavaScript では、数値の加法演算子にも文字列の連接演算子にも+を使うので、JavaScript に慣れているとよくハマります。姉妹言語の Perl 6 では、文字列連接演算子に.(ドット) ではなく~(チルダ) が採用されました。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む