正規表現 FizzBuzz メモ

wake と sed のセパレータがちょっと違う場所があったのを適当に修正。

\%!; until (/;/) {        #|fizzbuzz: l1,1       #%! { : }
\%!;  $\=$/;        #\(\) #|k(.);.*\1(.).*:"$2"  #%! s/$/;0123456789;/
\%!;  s/$/;0123456789;/;  #|j(.):k$+;0123456789  #%! s/9;\(.*;\)/;\10/
\%!;  s/9;(.*;)/;${+}0/;  #|i:"1"                #%! s/\(^\|#\);/\10;/
\%!;  s/(^|#);/${+}0;/;   #|i(.*)9:i$+ "0"       #%! s/\(.\);[^;]*\1/;/
\%!;  s@\(\)|(.);[^;]*\1| #|i(.*)(.):"$2" j$3    #%! s/;\(.\).*;/\1/
\%!;   @;@;               #|o.*[50]:"Buzz\n"     #%! s/^/#/
\%!;  s/;(.).*;/$1/;      #|o(.*): "$+\n"        #%! h
\%!;  s/^/#/;             #|f.*[50]:"FizzBuzz\n" #%! x
\%!;  $n=$_;              #|f.*:"Fizz\n"         #%! s/^\(###\)*/Fizz/
\%!;  s/^(###)*/Fizz/;    #|n(.*),(.):l$(i$2),$3 #%! s/.*#//
\%!;  s/.*#//;            #|l101,\d:             #%! s/[0-9;]*[05]$/Buzz/
\%!;  s@[0-9;]*[05]$|     #|l(.*),0:f$+ n$+,1    #%! s/z[0-9]*$/z/
\%!;   @Buzz@;            #|l(.*),1:o$+ n$+,2    #%! p
\%!;  s/z[0-9]*$/z/;      #|l(.*),2:o$+ n$+,0    #%! x
\%!;  print;              #|#:                   #%! { /;/!b }
\%!;  $_=$n;              #|#:                   #%! d
\%!; }         # (C) 2011 #|#: Shinichiro Hamaji #%i All Rights Reserved

忘れる前にこのコードについてメモ。

http://d.hatena.ne.jp/shinichiro_h/20111210#1323525496

正規表現なら、ってことで Perl, wake, sed あたりがいいだろうと最初に思いました。 wake を混ぜると改行が無いコードは書けないし、まぁそれなら縦に並べるかと思いました。プログラムの規模としては FizzBuzz くらいなら端末の縦幅におさまるし、正規表現しばりで適当に書けば、3言語でだいたい似たサイズになるかなぁと。

となると3つの言語をどう分割するか考えないといけない。 Polyglot ってのは、たいていは言語Aでは普通の式だけど、言語Bでは文字列リテラルに突入する、みたいな方法を考えることになります。例えば Ruby & Python だと、 '''' とかが、 Ruby では空文字列をあらわす式で、 Python では複数行文字列リテラルに突入なので、

''''
puts "Hello, ruby!"
#'''; print "Hello, python!"

のようにして分離できたりします。

で、 Perl, wake, sed だと、これがさくっと思いつきませんでした。こいつらリテラルとか無さすぎ。

wake はパターンの部分は : 以外は割となんでも入るからなんとかなるだろうとして、 Perl & sed を考えてました。 sedリテラルって言うと // だよなぁしかしコレ Perl でもリテラルで困ったなぁと思ってたところ、そういえば sed は \%% (ただし % は同じ文字ならなんでもいい) も正規表現リテラルなんでした、ってことでこれを使うことに。

バックスラッシュってたしか Perl だと参照取るとかだよねーということで、

% perl -le '\%!; print "hello"'

とかやってみて動くことを確認。これで sed が脱落してるので、この中で % を使わずに Perl と wake を書くといいか、と。 wake はどうせパターンの部分なんで、 : を使ってなくて、正規表現として invalid でない限りは何書いてもよいので、先に Perl をかたして、次 wake、最後にリテラルから復帰して sed 、って順序にしました。 wake は sed の右っかわに置くことはできなくて、これは sed が : を使うから。

でできたのが、

\%!; print "Hello, perl!\n" #|hello: "Hello, wake!\n" #%! iHello, sed!

という形。 wake の手前の #| は、 # で Perl はこれ以降完全にコメントになって、 | で wake は今までの文字列を無視します。無視っていうか、一応左っかわが正規表現として valid でないといけないけど、適当な Perl コードがマッチするわけないので、 | の右っかわだけが本質的な正規表現になる感じです。

wake と sed の境界の #%! は、 # で wake をコメントに突入させて、 % で sed正規表現リテラルを脱出、ここまでの Perl プログラムと wake プログラムが正規表現としてマッチするわけないので、 ! をつけて、マッチしなければ以下の文を実行、って感じでやってます。

あとは実際のコードで例外的な部分。

2行目の

\%!;  $\=$/;        #\(\) #|k(.);.*\1(.).*:"$2"  #%! s/$/;0123456789;/

Perl のコードの末尾に奇妙な #\(\) がありますが、これは wake の \1 に対応する後方参照が無いと wake がさわぐので、 Perl のとこに隙間を用意して特に意味のない後方参照を追加してあります。

6,7行目

\%!;  s@\(\)|(.);[^;]*\1| #|i(.*)(.):"$2" j$3    #%! s/;\(.\).*;/\1/
\%!;   @;@;               #|o.*[50]:"Buzz\n"     #%! s/^/#/

wake の後方参照が、本来は $1 $2 のはずが $2 $3 になってます。これは Perl の方のコードに () が一度使われてるため。というわけで wake のコードだけひっこぬいてもここを変えないと動かない感じになってます。同じことが 11 行目でも起きています。

Perl正規表現は2行に渡ってるので、 / を使うと sed が勝手に閉じちゃうので、 @ を使っています。2行に渡った正規表現はゴミが大量についてるので、 | で分離してます。 13,14行目も同じです。 \1 を使ってるから意味のない \(\) がついてるってのは2行目と同じです。

コード自体は、 sed複数行に渡る正規表現が書けないから、横幅 20 文字制限が結構きびしくて、色々分割したりしました。で、これが割と Perl でもそのまま動くし横幅も小さいし、ってことで、最初書いてた Perl コードは捨てて、 Perl はほとんど sed と同じロジックになってます。

wake は配布パッケージに入れてあった test/fizz.wake を横幅ゴルフしたもので、 l がメインループ、 n がメインループを次に進める関数、 f が 3 の倍数時の出力処理、 o が 3 の倍数でない時の出力、 i がインクリメントで j が 1 文字だけのインクリメント、 k はそのヘルパです。

最後のコピーライトは、 Perl はコメント、 wake は実行されない命令、 sed は実行されない命令 i の引数、って感じです。

なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h