今年に入ってから,分散処理がもう少し簡単にできないかなぁと 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]ところとデフォルトパラメーターが末尾じゃないパラメーターにも指定できるところは微妙な感じがしますが,それ以外の点では今の所不満は見つかっていません。