クロージャーでFizzBuzz
実は、一つ前の投稿は、最初はFizzBuzzを例にして書いていたんですよ。
でも、FizzBuzzだと、クロージャーを使わない途中のバージョンを書くのが異常に難しかったんです。で、それで途中で挫折して、だから、倍数などという実用性が全く無い例で書いてしまいました……。
言い訳。確かに倍数は実用性が無いけれど、社員を名前で検索するような場合にも、同じテクニックが使えると思うんですよ
で、「こんな役に立たない例で話をされても」と思った方は、頭の中で、社員を名前で検索する場合に置き換えてみてください。以下のような感じ。
public IEnumerable<Employee> GetEmployeesByName( IEnumerable<Employee> employees, string name) { return employees.Where(e => e.Name == name); }
ね、クロージャーは便利でしょ?
ここから本題。FizzBuzzの「非」関数型バージョン
さて、本題のFizzBuzzです。まずは、関数型言語の機能を取り入れる前のC#を念頭に置いて書いたバージョン。以下のような感じ。
public class FizzBuzz { public static void Main(string[] args) { for (var i = 1; i <= 100; ++i) { Console.WriteLine(ToFizzBuzz(i)); } } private static string ToFizzBuzz(int number) { if (number % 15 == 0) { return "FizzBuzz"; } if (number % 5 == 0) { return "Buzz"; } if (number % 3 == 0) { return "Fizz"; } return Convert.ToString(number); } }
ふぅ、やっぱりキモチワルイ。関数型に書き直したい……。
本題の結論。FizzBuzzの関数型バージョン
というわけで、関数型に書き換えます。まずは、引数で指定された数で割り切れたら引数で指定された文字列に変換する関数(を作る関数)を作りました。
private static Func<int, string> ConvertFunc(int x, string s) { return a => a % x == 0 ? s : null; }
ConvertFunc(15, "FizzBuzz")みたいに使うわけですな。こうすると、引数で指定された数値が15で割り切れたら"FizzBuzz"を、そうでなければnullを返す関数が作られます。
次に、変換関数のリストを作ります。
var fs = new List<Func<int, string>> { ConvertFunc(15, "FizzBuzz"), ConvertFunc( 5, "Buzz"), ConvertFunc( 3, "Fizz"), Convert.ToString };。
3の倍数でも5の倍数でもなかった場合用に、最後にConvert.ToStringを入れておきます。あとは、この関数を順番に実行して、最初にnull以外が返ってきたらそれを返す関数(をつくる関数)を作ればおっけぇ。以下のような感じ。
private static Func<int, string> ToFizzBuzzFunc() { var fs = new List<Func<int, string>> { ConvertFunc(15, "FizzBuzz"), ConvertFunc( 5, "Buzz"), ConvertFunc( 3, "Fizz"), Convert.ToString }; return a => fs.Select(f => f(a)).First(s => s != null); }
Select()で変換(この命名はどうにかならないのでしょうか……。私はLINQのクエリ形式をほとんど使わない人なので、クエリ形式での見た目をSQLっぽくするためという理由は認めません)して、First()でnull以外を探しています。遅延評価なのでパフォーマンスも問題ないはず。
あとはMain()をてきとーに修正。というわけで、Main()を含めた全体のコードは、以下のようになりました。
using System; using System.Collections.Generic; using System.Linq; public class FizzBuzz { public static void Main(string[] args) { Enumerable.Range(1, 100).Select(ToFizzBuzzFunc()).ToList().ForEach(Console.WriteLine); } private static Func<int, string> ToFizzBuzzFunc() { var fs = new List<Func<int, string>> { ConvertFunc(15, "FizzBuzz"), ConvertFunc( 5, "Buzz"), ConvertFunc( 3, "Fizz"), Convert.ToString }; return a => fs.Select(f => f(a)).First(s => s != null); } private static Func<int, string> ConvertFunc(int x, string s) { return a => a % x == 0 ? s : null; } }
あれ、なんかコードが汚い……。手段(関数型)と目的(保守性が高いコード)が入れ替わっちゃった?
おまけ。クロージャー(Clojure)でクロージャー(Closure)なFizzBuzz
いやね、私もね、こんな汚いコードになるとは思っていなかったんですよ。
というのも、最初はClojure(言語の方)でFizzBuzzを書く遊びをやっていて、で、その中でClosure(機能の方)を使ったら面白く書けたから、他の言語でも同じように面白く書けるんじゃないかとやってみたわけでして……。
というわけで、最後にClojure(言語の方)でClosure(機能の方)なFizzBuzzのコードを載せておきます。
(defn fizz-buzz-fn [] (letfn [(convert-fn [x s] (fn [a] (if (= (rem a x) 0) s)))] (some-fn (convert-fn 15 "FizzBuzz") (convert-fn 5 "Buzz") (convert-fn 3 "Fizz") str))) (defn print-fizz-buzz [] (doseq [s (->> (range 1 101) (map (fizz-buzz-fn)))] (println s)))
ね、なんか面白いコードでしょ?Clojureは素晴らしい!