学生時代に情報科学科の同級生たちと「FizzBuzz 問題というものがあるらしいが、この程度の基礎ができないプログラマなどいるはずがない」と会話していた。
それから四半世紀。小学生にはプログラミングの授業があり、非 IT 企業の一般社員にも「Python 書いて RPA しろ、HTML/CSS/JavaScript で社内サイト作って情報発信しろ」という圧が掛かってくる DX 社会。DX とはデジタル革命, IT, Web2.0, ICT に続く "企業経営者向け IT 売り込みバズワード" だと思っていたが、一般の人々にまでやれ!やれ!と煽り立てる世の中の圧が今回はすごい。
猫も杓子もコードを書けという時代になった *1 せいか、Quora などでも FizzBuzz 書けない問題をよく目にする。
ということで、自分なら FizzBuzz どう書くか、C# と Perl でやってみた。
C# 9.0 (標準解)
Enumerable.Range(1, 30)
.Select( i => (i % 3, i % 5) switch {
(0, 0) => "FizzBuzz",
(0, _) => "Fizz" ,
(_, 0) => "Buzz" ,
(_, _) => "" + i ,
} )
.ToList().ForEach( x => Console.WriteLine(x) );
所要 1 分半。using, namespace, class, Main() メソッドの宣言が不要な C# スクリプトとして記述した。
普段使いの C# のため、コアロジックのコーディングに悩むこともリファレンスを参照することもなく、呼吸するようにタイピングする。純粋関数 (LINQ によるデータ変換) と I/O (WriteLine() による出力) をきれいに分離した上で後者をどう書くかというのが C# LINQ を使う上で最も時間を費やす悩みどころ。結局、WriteLine() を交えるなら List の .ForEach() を使うのが (開発効率的に) 早いだろうということで、メモリ効率を犠牲にしてこの形に。
数列のサイズが大きいか未知である場合は、メモリ効率を勘案し "データ変換 + 出力" を単位として遅延評価にする。その場合、最後を foreach {} 制御に置き換えるところなのだろうが、カーソル移動とブロックインデントが面倒なため、LINQ メソッドチェーンで次のように書いてしまう。ステートメント混じりである点とダミーデータを作って捨てるムダが美しくないが、デバッグなどではよく使う。
.Select( x => { Console.WriteLine(x); return 1; } ).ToList();
map do {
my $s = ($_ % 3 ? '' : 'Fizz') . ($_ % 5 ? '' : 'Buzz');
print $s ? $s : $_, "\n";
}, (1 .. 30);
所要 3 分。同一内容の記述であるが、こちらの方がコーディングの所要時間が長くなった。
Perl は得意ではあるもののたまにしか使わないため、いかに簡便に書けるか条件判定と演算子との組み合わせ *2 を改めて意識する必要があり、また、map の対象をステートメントブロックにするか式にして手続を外に追い出すか試行錯誤したため、そこに時間を取られる。
print map{(($s=($_%3?'':'Fizz').($_%5?'':'Buzz'))?$s:$_)."\n"}(1..30);
どれだけ短く書けるか競争なら、こうかな? 70 文字。読みにくい...
三項演算子を用いている部分をより短く書けないか *3 と久しぶりにリファレンスを参照してみて、 // 演算子や given 文といったものが Perl 5 に追加されていることを知った。given が文でなく式なら嬉しいのだが。
C# 9.0 (別解)
Enumerable.Range(1, 30)
.Select( i => (i % 3 == 0 ? "Fizz" : "") + (i % 5 == 0 ? "Buzz" : "") is var s && s.Length > 0 ? s : "" + i )
.ToList().ForEach( x => Console.WriteLine(x) );
標準解は素直な書き方だが、3, 5, 15 の倍数での場合分けと行数が多くなってしまうところに若干の負けた感が漂うため、別解も用意してみた。作成した Fizz/Buzz 文字列を (is var パターンマッチのちょっとした濫用により) 変数 s で受け、ワンラインの式としている。
あるいは次の通り。switch と () が増える分だけ長くなるかと思いきや同じコード長。ポイントはやはり、代入文を使わず、式の内部で変数 s を受けるというところ *4。
Enumerable.Range(1, 30)
.Select( i => ((i % 3 == 0 ? "Fizz" : "") + (i % 5 == 0 ? "Buzz" : "")) switch { "" => "" + i, var s => s } )
.ToList().ForEach( x => Console.WriteLine(x) );
コード長 + 可読性 の観点からはこれが、実行速度 + 可読性の観点からは標準解が C# の最適解 *5 になるだろうか。
パラダイムがオブジェクト指向から関数型プログラミングに移りつつあってプログラミング言語が乱立する時代でもあり、たまにこういう基礎問題に立ち返るのも悪くない。
関数型が志向されるようになると式をワンラインで記述することも許容されやすくなるだろう。ワンライナーが嫌われるのは、意味単位の異なる複数ステートメントを無理やり1行に収めようとするからだと思っている。
おまけ
using System.Text.RegularExpressions;
var dic = new [] {
"/じゅう/にじゅう/さぁ~んじゅう/よんじゅう" ,
"/いち/に/さぁ~ん/し/ご/ろく/しち/はち/きゅう",
}
.Select( str =>
str.Split("/")
.Select( (x, i) => new { Key = i, Value = x } )
.ToDictionary( x => x.Key, x => x.Value ) )
.ToList();
Enumerable.Range(1, 40)
.Select( i => i % 3 == 0 || Regex.IsMatch($"{i}", "3") ? dic[0][i / 10] + dic[1][i % 10] : $"{i}" )
.Append("オモロ~")
.ToList().ForEach( x => Console.WriteLine(x) );