今年に入ってから,分散処理がもう少し簡単にできないかなぁと 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/n は n 個のパラメーターをとる関数 func の無名関数化 (&func(&1, ..., &n) と同じ), |> は右側オペランドの最初の引数に左側のオペランドを渡す演算子, send と receive はプロセス間のデータの送受信です。ちなみに 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]ところとデフォルトパラメーターが末尾じゃないパラメーターにも指定できるところは微妙な感じがしますが,それ以外の点では今の所不満は見つかっていません。