FizzBuzz - Golf Challenge(by harupiyo)

FizzBuzz Golf Challenge という、Perl で如何に短く頑張れるか!というのがはやってるみたいです。

FizzBuzz – Golf Challenge | TAKESAKO @ Yet another Cybozu Labs

を見て面白そうなので私も作ってみた。
"フツーの人が頑張って作ってみたよ!記"みたいなノリでまとめてみます。
Perl の初心者向けに詳しく思考のプロセスを書き起こしてみたので短くするメカニズムを知りたい方は読んでみてください。

<仕様>
1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。

まずはとにかく動作するものを作ってみる。

for(1..100){
$a = !($_%3)?'Fizz':'';
$a .= !($_%5)?'Buzz':'';
print $a || $_ , "\n";
}

ポイント

  • for の中身を1..100のように書けるのはPerl の好きなところ。美し。
  • use strict; してないので、変数を宣言せずに使って、コード字短。(普段これは当然やらないので、コレだけでも新鮮に思いました)
  • FizzやBuzz を出力したら数字は出力しないようにするので、|| 演算子を使ってどっちかを表示というのをやっています。

これを作った後で、もっと短くできるのでは?と感じたのは、

  • TAKESAKO さんの強烈なのを見たので、当然1行にはできるはず(自分でできるのかなぁ…)
    • $a という一時変数を使っているのがばっちい。これを取れるように工夫すると自然と1行になるんじゃないかな?
    • Perl では、for(){***} という書き方だけでなく、1行であれば *** for() と書けるので、*** を一行にしさえすれば、全体を1行にまとめられるはず

です。

何とか1行で書けないかなと思って頑張ってみました。

for(1..100){
print join '',(!($_%3)?'Fizz':'',!($_%5)?'Buzz':'',$_,"\n");
}

このプログラムは、print join '',(データ配列)という構造をもっています。
さらにデータ配列の部分は、( Fizz を出す条件, Buzz を出す条件, 行番号, 改行 )のような形で、Fizz/Buzz を出す条件のところは出力する文字があってもなくても後でjoin '' してしまうので問題ありません。

とにかくこれで1行にできる気がしたんですが、動かしてみると行番号($_) が必ず出力されてしまうため、単なるbug 版にすぎないものでした。

なんとか行番号を、Fizz/Buzz の出力とは排他にできないか、頑張ってみました。

for(1..100){
print join '',($_%3?'':Fizz).($_%5?'':Buzz)||$_,"\n";
}

先ほどデータ配列の中身を入れ替えてみることでできました。
前回の
( Fizz を出す条件, Buzz を出す条件, 行番号, 改行 )
ではなく、
( ((Fizz を出す条件, Buzz を出す条件)もしくは 行番号), 改行 )
というように、データの数をメッセージ本体と改行の2つにし、メッセージ本体の中身で賢く振舞うようにしました。

その他のポイントとしては、

  • ! $_ % 3 ? 'Fizz' : '' と書くのではなく、Fizz と'' の場所を入れ替えることで!(not) を削除することができます
  • use strict; しないPerl では'Fizz' ではなく、裸のワードを使ってFizz と書けます。


これができたおかげで1行が実現できます。
すなわち、*** for() のスタイルを使い、次と同じにかけます。

print join '',($_%3?'':Fizz).($_%5?'':Buzz)||$_,"\n" for(1..100);

もうちょっと欲張ってみる。姑息なチャレンジが続きます。

まずはこちら。

warn join '',($_%3?'':Fizz).($_%5?'':Buzz)||$_,"\n" for(1..100);

print の代わりにwarn を使うことで、1 文字減らしています。

結局、このような形になりました。

die map{($_%3?'':Fizz).($_%5?'':Buzz)||$_,"\n"}(1..100)

(55文字)

ポイント

  • warn よりも更に短いdie を使っている(w)
  • die を使う場合はその場でプログラムの実行が終了してしまうので、for ループが使えない。
    • よって、die (表示リスト); のような構造とし、表示リストの部分に出したい全データを作るようにする。
    • 表示リストを作るにはmap 関数を使い、(1..100)の初期文字列をエサに、適宜に加工し作っている。
  • 一行だけなので、; も取ることができた。

map の動きを知るのが一番のキモで、これによりfor 文も省略でき、join の'' も省略でき、die も使えるという相乗効果で10文字減らすことができました。


まだ、いろいろ工夫できるところはあるかと思って下にTODO を残しますが、今の私の力ではここまででした。

  • (1..100)を何とか減らせないか?
    • 1から100まで書いたファイルを用意し、それを読み込んで処理するようにする
    • (こんな感じで実行)perl -n fb.pl 100.txt
    • プログラムコードは

warn join '',($_%3?'':Fizz).($_%5?'':Buzz)||$_,"\n";

    • ただ、これでは不要な改行が勝手に入ってしまう問題がありました orz
  • "\n" って書きたくないけどどうにかできるのかな?
  • なんとか三項演算子を使わないようにしてエレガントにいけないかな


ここまでやってみて、改めてTAKESAKO さんのものを見るとスゴイ!!
まだまだぜい肉ありますね〜〜。

とにかく、ここまで楽しんでやりました。