Michał Muskałaさんの2015年8月10日付のブログ記事Functional FizzBuzz in Elixirの翻訳です。
文中にもありますが、Erlangでちょっと変わったFizzBuzzの書き方をした人がいてそれをElixirに移植したという記事です。面白かったんで翻訳してみました。
普通は3または5またはその両方で割り切れるか剰余演算子で調べて Fizz/Buzz/FizzBuzz を出し分けるんで、パターンマッチと再帰(そもそもループがないから)のサンプルにしかならないんですけどね、FizzBuzz。関数型の例なのかよ!と言われるとどうなんでしょうか(笑)
あと、これ、Clojureでも同じように書けそうだなあ…と思ったらその通りで書けました(おまけ参照)
前回のSteven ProctorさんのErlang Thursday - 関数型 fizzbuzzからインスパイアされたんでどうやったら剰余演算子を使わないでFizzBuzzをElixirっぽく実装できるかどうかやってみました。
こんな感じかな:
defmodule FizzBuzz do
def print(n) do
generate
|> Stream.take(n)
|> Stream.each(&IO.puts/1)
|> Stream.run
end
def generate do
fizzes = Stream.cycle(["Fizz", "", ""])
buzzes = Stream.cycle(["Buzz", "", "", "", ""])
Stream.zip(fizzes, buzzes)
|> Stream.map(&concat/1)
|> Stream.with_index
|> Stream.map(&translate/1)
|> Stream.drop(1)
end
defp concat({f, b}), do: f <> b
defp translate({"", n}), do: to_string(n)
defp translate({str, _n}), do: str
end
おおよそのところは見ての通りですけど、独自性もあるのでそこを説明します。Erlangでの実装の問題点のひとつとしてはcycle
のような遅延評価のストリーム及び関数がないことだと思っていますが―ElixirではStream
モジュールがあるのでこれは問題になりません。一方でErlangではlists:zipwith/3
1が使えるので項目を合成して結合するのをいちどきに行うことが可能です。Elixirでは2ステップが必要です―まずStream.zip/2
(またはStream.with_index/1
)を使い、直後に結果のタプルをマップして必要なフォーマットにします。
Stream.drop/2
の呼び出しはElixirのインデックスが0
から始まるのにfizzbuzzの結果の最初の項目は1
から始まって欲しいために必要となっています。これを避けるには第3のストリームを生成してそれが1
から始まる番号を返すようにして、それを合成するのも手ですね:
def generate2 do
fizzes = Stream.cycle(["", "", "Fizz"])
buzzes = Stream.cycle(["", "", "", "", "Buzz"])
indexes = Stream.iterate(1, &(&1 + 1))
Stream.zip(fizzes, buzzes)
|> Stream.map(&concat/1)
|> Stream.zip(indexes)
|> Stream.map(&translate/1)
end
どっちがより明快で理解しやすいかは私にはわかりかねますが…。
おまけ:Clojureで書いてみた
割と簡単に書けました。とはいえしばらくClojure書いてなかったからもっといいやり方がありそうですが…
(def fizzes (cycle ["" "" "Fizz"]))
(def buzzes (cycle ["" "" "" "" "Buzz"]))
(def indexes (iterate inc 1))
(defn translate [x,n]
(cond (= x "") (str n)
:else x
)
)
(defn print-fb [n]
(take n (map #(translate %1 %2) (map #(str %1 %2) fizzes buzzes) indexes))
)
-
例:lists:zipwith(fun(X,Y) -> X+Y end, [1,2,3], [4,5,6]).はリスト[5,7,9]を返します。lists:zip([1,2,3],[4,5,6]).が[{1,4},{2,5},{3,6}]という2項タプルのリストを返すんでそのタプルの2つの要素を第1引数の無名関数で演算する感じです。 ↩