ヒアドキュメントの中でPerlの式を書く

初代新幹線「0系」最後のラストランの日に、新幹線N700系のぞみでイーモバイルしながらこの記事を書いているid:TAKESAKOです。

Perlのヒアドキュメントを使うと複数行にわたる文字列を一気に代入したりするときに楽なので、使っている人も多いと思います。

my $foo = "bar";
my $tmp = time(); # ←関数の実行結果
print<<EOF;
  <div class="${foo}1">
    <h1>TIME: $tmp</h1>
  </div>
EOF

しかし、ヒアドキュメントの途中でサブルーチンの実行結果も一緒に埋め込みたいときがでてくるときがあります。

@{[ Perlの式 ]}

そのようなときは、@{[ リスト ]} というイディオムを使うと非常に便利です。

print<<EOF;
  <div class="${foo}1">
    <h1>TIME: @{[ time() ]}</h1>
  </div>
EOF

実行結果:

  <div class="bar1">
    <h1>TIME: 1229258525</h1>
  </div>

@{[ リスト ]} の中にはPerlの式をそのまま書くことができます。

この例では、関数 time() の実行結果がヒアドキュメントの文字列の中に埋め込まれていることがわかります。

リストコンテキストの罠

しかし、このイディオムの式はリストコンテキストで評価されるので、wantarray でサブルーチンの戻り値を切り替えている関数などでは期待通りの結果が得られない場合があります。

print<<EOF;
  <div class="${foo}2">
    <h1>TIME: @{[ localtime() ]}</h1>
  </div>
EOF

実行結果:

  <div class="bar2">
    <h1>TIME: 5 42 21 14 11 108 0 348 0</h1>
  </div>

このような場合は、式が必ずスカラーコンテキストで評価されるように、 "".localtime() もしくは scalar localtime() などと記述すれば大丈夫です。

print<<EOF;
  <div class="${foo}3">
    <h1>TIME: @{[ scalar localtime() ]}</h1>
  </div>
EOF

実行結果:

  <div class="bar3">
    <h1>TIME: Sun Dec 14 21:42:05 2008</h1>
  </div>

"@a\n" と特殊変数 $"

ちなみに、ダブルクォート文字列中に @a を埋め込んだ場合、例えば "@a\n"join($",@a)."\n" と等価となります。

$" は、配列のリストを文字列に変換するときに自動的に要素間に挿入する文字列(デフォルトは空白 " ")を意味する特殊変数です。
一時的に $" の値を書き換えて、要素間に自動挿入する文字列をデフォルトの空白から任意の文字列に変更することもできます。

do {
  local $" = " x ";
  my @a = ('sin', 'cos', 'tan');
  print "@a\n";
};

実行結果:

sin x cos x tan

ここで、"@a\n" の部分を "@{[ リスト ]}\n" に置き換えて、

print "@{[ 'sin', 'cos', 'tan' ]}\n";

と、無名配列のデリファレンス @{[ ]} を使うことによって、配列 @a を使わずに、文字列中に直接リストの値を埋め込むことが可能になります。

このようにヒアドキュメントの他にも、ダブルクォーテーションで囲まれた文字列の中でも @{[ リスト ]} のイディオムを使ってPerlの式を展開することができます。

閑話休題

数日前の復習ですが、Perlは破壊的な正規表現の置換を行なうので、以下のように複数行にわけて書くのが面倒という話がありました。

my $a = "AAA";
my $b = $a;
   $b =~ s/A/B/g;
print "'$a' => '$b',\n";

実行結果:

'AAA' => 'BBB',

これは、$b = $a の代入文を ( ) で括って、置換演算子 =~ の左辺に持ってくることによって1行短縮することができます。

my $a = "AAA";
( my $b = $a ) =~ s/A/B/g;

代入演算子 = は左結合なので、さらに変数 $a の初期化も1行にまとめることができます。

( my $b = my $a = "AAA" ) =~ s/A/B/g;

これは、myの存在しなかったPerl4の時代で基本的なテクニックだったそうです。

@{[ リスト ]} の応用例

ここでmapを使えば一時変数の名前もつけなくてよくなるはず…と思って以下のコードを書いて実行してみます。

my ($b) = map { s/A/B/g; $_ } ("AAA");

実行結果:

Modification of a read-only value attempted at - line 1.

しかし、このコードは Perl に怒られてエラーになってしまいます。

("AAA") としただけでは "AAA" は文字列定数(read-only value)と解釈されるので、値の変更ができないのです。

map の初期値に @{[]}

そこで登場するのが @{[ リスト ]} の応用例です。

my ($b) = map { s/A/B/g; $_ } @{["AAA"]};

["AAA"]で無名配列を作ってすぐに@{}でデリファレンスしてあげれば、 一時的な配列を作成することができます。
この配列の値は破壊的な変更が可能です。

grep の第一引数に s///

ちなみに、map の他に grep を使っても同じようなコードを書くことができます。

my ($b) = grep s/A/B/g || 1, @{["AAA"]};

このとき || 1 の部分を省略してしまうと、 s/// の戻り値がそのまま評価されてしまうので、置換に失敗した要素が取り除かれてしまいます。

print join ":", grep s/A/B/g, @{["AAA", "ABC", "XXX", "A"]};

実行結果:

BBB:BBC:B

s/A/B/ の置換に失敗した "XXX" の値も含めたい場合は、s の後ろに || 1 をつけて、パターンマッチの結果に関わらず常に真になるようにします。

print join ":", grep s/A/B/g || 1, @{["AAA", "ABC", "XXX", "A"]};

実行結果:

BBB:BBC:XXX:B

次は国内滞在説が根強い id:miyagawa さんにお願いしたいと思います。;-)