読者です 読者をやめる 読者になる 読者になる

第2回デスマコロシアム Perlの42文字のコード

CodeIQ

はじめに

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文字のコードの解説

途中で提出した 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" を展開した文字コードが渡っているはずなのに、なぜそのようなことになるのでしょう。

(ここから先は、あくまで私の経験上の理解によるもので、必ずしも正確であるとは限りませんのでご了承ください。)

じつは、 Perlfor は、リストの各要素の$_ にセットするというより、むしろリストの各要素自体への参照$_ にセットするようなイメージなのです。例えば、

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 に限ったことではなく、 mapgrep でも同様の挙動になります。)

これさえ理解できれば、あの42文字のコード

print chr$_++for(unpack CU7,"aAあアアあAa")x26

がなぜ正しい出力をするのか、納得できますね!

めでたし、めでたし。

 

Perl の42文字のコードの解説に絞って全体を書き直しました。)