第2回デスマコロシアム Perlの42文字のコード
はじめに
CodeIQ で出題された『第2回デスマコロシアム』に参加しました。
大雑把に説明すると、
- 問題文で与えられたある文字列を出力するコード(プログラム)を提出する。
- 言語は ideone で利用可能なものから選ぶ。
- コードのサイズが短いほどよい。(1文字につき1点減点)
- 同じ言語を選択した人が少ないほどよい。(1人につき10点減点)
- その他、コーディングとは無関係な運の要素も加えて、トーナメントで競う。
といった感じです。要するに、コードゴルフに運の要素を加えたトーナメントです。具体的な出力の文字列などについては、出題者様によるまとめ記事をご参照ください。
出題者様によるまとめ記事:
http://d.hatena.ne.jp/tbpg/20140517/1400291776
私は、途中まで Perl (42文字)でエントリーし、その後 Perl 6 (32文字)に移行しました。結果、準々決勝で敗退しましたが、その Perl 6 のコードが全言語を通して最短(3名タイ)ということで、「最短賞」をいただきました。
結果発表(CodeIQ Magazine):
https://codeiq.jp/magazine/2014/05/9744/
今回は、その全言語最短の Perl 6 のコードはとりあえず置いておいて、途中で提出した Perl の42文字のコードについて「見たい」という要望をいただきましたので、解説することにいたします。
そういえば、前回のデスコロで、Perlで42を出された方の解答が見たい。 #デスマコロシアム #codeiq
— あじ (@Azicore) May 16, 2014
Perl の42文字のコードの解説
途中で提出した Perl の42文字のコードは、次の通りです。
print chr$_++for(unpack CU7,"aAあアアあAa")x26
(ideone で実行 → http://ideone.com/aJV7I5)
全体は、式が for
で後置修飾された文の形になっています(最後の ;
が省略されています)。 for
の右側のリストの要素が1つずつ $_
にセットされながら for
の左側の式が評価されるようなイメージです。
まず、
unpack CU7,"aAあアアあAa"
の部分を見てみましょう。文字数を節約するために、関数呼び出しの括弧と文字列のクォートを省略してありますが、
unpack("CU7","aAあアアあAa")
ということです。 unpack()
は、Perl の組み込み関数で、テンプレート("CU7"
)に従って文字列("aAあアアあAa"
)をリストに展開します。これで、8文字の文字コードのリストが得られます。テンプレートの 「C
」は符号なし1バイト、「U
」はUnicodeの1文字のコードポイント、「7
」は「U
」を7回、という意味です。一見、 "U8"
でも良さそうな気もしますが、じつは pack()
/ unpack()
には、テンプレートの文字列が「U
」で始まる場合には Unicode を文字単位でなくバイト単位で扱うという不可解な仕様があり、これを回避するために1文字消費して "CU7"
としてあります。
次に、その外側の
(
……)x26
の部分ですが、この「x
」はリストを繰り返す演算子です。例えば (1,2,3)x2
なら (1,2,3,1,2,3)
と同じ意味になります。これによって、8文字の文字コードのリストの26回の繰返しが得られます。
さて、これで for
の左側の式の $_
にセットする値のリストができました。ではその式を見てみましょう。
print chr$_++
ですね。これもちゃんと書くと
print(chr($_++))
ということです。後置 ++
はポストインクリメント、 chr()
は文字コードに対応する文字を返す組み込み関数、 print()
は引数を出力する組み込み関数です。さあこれで、問題文で与えられた文字列が出力されますね…? いや、何かおかしいですね。インクリメントの効果が、後ろの文字に蓄積していっている!? しかも、ちゃんと8文字ごとに1ずつ増えるように? ループには毎回 "aAあアアあAa"
を展開した文字コードが渡っているはずなのに、なぜそのようなことになるのでしょう。
(ここから先は、あくまで私の経験上の理解によるもので、必ずしも正確であるとは限りませんのでご了承ください。)
じつは、 Perl の for
は、リストの各要素の値を $_
にセットするというより、むしろリストの各要素自体への参照を $_
にセットするようなイメージなのです。例えば、
print(++$_) for $a,$b;
というコードを実行すると、変数 $a
と $b
がそれぞれインクリメントされて出力されます。もちろん、インクリメントの効果は、ループを抜けた後にも残ります。これは x
演算子でリストを繰り返した場合も同様で、
print(++$_) for ($a,$b) x 3;
では、変数 $a
と $b
がそれぞれ3回ずつインクリメントされます。また、
print(++$_) for (1,2,3);
とすると、定数(read-only value)を変更しようとしたとして、実行時エラーになります。
イメージできますか? もう少し複雑な例を見てみましょう。
print(++$_) for (cos(0)) x 3;
これは、やはり定数(1)を変更しようとしたことになり、実行時エラーです。ところが、
$a=0; print(++$_) for (cos($a)) x 3;
これは実行できてしまいます。この場合、 cos($a)
は定数ではなく、実行時に計算されます。計算結果は、一時的な内部変数(のような何か)に格納され、その内部変数への参照が $_
に設定されるのです。その結果、内部変数は順次インクリメントされて 2 と 3 と 4 を出力することになります。
(なお、このことは for
に限ったことではなく、 map
や grep
でも同様の挙動になります。)
これさえ理解できれば、あの42文字のコード
print chr$_++for(unpack CU7,"aAあアアあAa")x26
がなぜ正しい出力をするのか、納得できますね!
めでたし、めでたし。
(Perl の42文字のコードの解説に絞って全体を書き直しました。)