Elixir で FizzBuzz


今年に入ってから,分散処理がもう少し簡単にできないかなぁと Elixir の学習を始めました。とりあえず文法は一通りやったのでとりあえず FizzBuzz あたりを書いてみようかと。

検索すると Qiita に「ElixirでFizzBuzz書いてみた」とか「[翻訳] Elixirで書く関数型FizzBuzz」とか「[翻訳] ElixirにおけるOTPの紹介」とかあります。最初の記事はいわゆる普通の FizzBuzz で,まあ FizzBuzz だなぁという感想です。コメントにマクロを使った方法が記述されていますが, FizzBuzz としてやっていることは一緒ですね。 2 つ目の記事は無限ストリームを使って剰余演算を使わない方法ですが,これも割とよく知られた手法です。 3 つ目の記事は並行処理をしていて Elixir っぽさがあって面白いのですが,結局 FizzBuzz 自体はやっていることが普通です。

ここでは少し違うアプローチとして,クライアント側から FizzBuzz の処理の一部 (Fizz/Buzz になる条件) をクライアント側からサーバー側に教えるという手法をとります。ただし簡単のために OTP ではなく multiprocess の手法をとっています。

作成したコードは以下の通りです。

defmodule FizzBuzz do
  def start_link do
    Task.start_link(fn -> loop([]) end)
  end

  defp loop(fizzer) do
    receive do
      {:register, predicate, value} ->
        f = fn n -> if predicate.(n), do: value, else: "" end
        loop [f|fizzer]
      {:get, n, caller} ->
        f = fizzer |> Enum.map(&(&1.(n))) |> Enum.reduce(&<>/2)
        res = cond do
          f === "" -> to_string n
          true -> f
        end
        send caller, res
        loop fizzer
    end
  end
end

{:ok, pid} = FizzBuzz.start_link
send pid, {:register, &(rem(&1, 3) === 0), "Fizz"}
send pid, {:register, &(rem(&1, 5) === 0), "Buzz"}

1..100 |> Enum.each(fn n ->
  send pid, {:get, n, self()}
  receive do
    x -> IO.puts x
  end
end)

順に解説していきます。 Elixir の文法に詳しくない人のために簡単に文法上の解説を書くと, :name はアトム (シンボル), {x, y, ...} はタプル, fn pattern -> ... end は無名関数の作成 (パラメーターがない場合はパラメーターの記述を省略できる), &proc は処理 proc 無名関数化 (&n で n 番目のパラメーターを表す), &func/nn 個のパラメーターをとる関数 func の無名関数化 (&func(&1, ..., &n) と同じ), |> は右側オペランドの最初の引数に左側のオペランドを渡す演算子, sendreceive はプロセス間のデータの送受信です。ちなみに Elixir では無名関数を適用する場合は func.(x) のように . が必要になります。

2-4 行目はサーバーの準備です。これにより 6-20 行目に記述されたループ処理を開始します。 F# でいうところの MailboxProcessor.start に相当する処理です。

6-20 行目がメインのループ処理です。クライアントから数字を受け取ると,クライアントに FizzBuzz の文字列を返します。 8-10 行目は predicate 関数と文字列 value を受け取り, predicate の条件にマッチした場合に value を返すような関数を登録します。 11-18 行目は数字とクライアントの PID を受け取って, FizzBuzz 処理をしてクライアントに返します。

ここでほんの少しだけ工夫している点は, FizzBuzz 処理を行うリストを,与えた順序と逆順にリストとして登録しているところです。これによるメリットは, 1 つはリストの生成が速くなること,もう 1 つは 12 行目の reduce 関数のオペランドの順番を入れ替えなくてもよくなることです[A]。本質的にはあまり意味はありませんが…。

23 行目以降はクライアント側の処理です。 23 行目はサーバーの起動を行い, 24, 25 行目でサーバーに条件と値を渡しています。 27-32 行目がメインの処理で,各値に対してサーバーに数字と自分自身の PID を渡し,結果を受け取ってコンソールに出力します。自分自身の PID を渡さなくても良い方法はないのだろうか。

Elixir は結構書きやすくて良いですね。パラメーターなしの関数の () が省略できる[B]ところとデフォルトパラメーターが末尾じゃないパラメーターにも指定できるところは微妙な感じがしますが,それ以外の点では今の所不満は見つかっていません。

脚注

  1. 直感的には reduce/3 関数 (パラメーターが 3 つの reduce 関数) は (a -> b -> a) -> a -> Enumerable.b なのですが,実際には (b -> a -> a) -> a -> Enumerable.b なので集積する方が関数の 2 つ目ののパラメーターにきます。必然的に今回使っている reduce/2 も同様です。 []
  2. 無名関数では () 必須。これはパラメーターがある関数の () が省略できるところからきていると思いますが,パラメーターがない場合は括弧がないと関数だってわかりづらいです。 []