Elixir Advent Calendar 2014 22日目。
私の前回の記事は gen_event
に関するものでした。
せっかくなので、OTP全般について、「Elixirの場合はどう書くのか」「ElixirはErlangとどう違うのか」といった観点で見ていきましょう。
gen_server
gen_server
については、 @k1compolete さんが2年以上前に作られた資料が、とても端的にまとめられています。
http://s.testerhome.com/k1complete/development-appwithelixir
gen_server
には、Elixirならではの違いは特に見られません。
どちらかと言うと、書き方の違いや、その言語自体の処理系の実装の違いとなるでしょう。
(Erlang/OTPでは「振る舞い」として指定するのに対し、Elixirでは継承を使うなど。)
そこで、日本語での解説が少ない話題を一つ。
gen_server
の再起動ストラテジーには simple_one_for_one
, one_for_one
, rest_for_one
, one_for_all
がありますが、Elixirでは実際どのように指定するのでしょうか。
これらは、スーパーバイザーのモジュール定義内で指定します。
以下にサンプルコードを示します。
defmodule MyApp.Server do
use GenServer
def start_link do
GenServer.start_link(MyApp.Server, [], name: MyApp)
end
def init(stack) do
{:ok, stack}
end
def handle_call(:pop, _from, [h|stack]) do
{:reply, h, stack}
end
def handle_cast({:push, new}, stack) do
{:noreply, [new|stack]}
end
end
defmodule MyApp.Supervisor do
use Supervisor
def start_link do
Supervisor.start_link(__MODULE__, :ok)
end
def init(:ok) do
children = [worker(MyApp.Server, [], restart: :temporary)]
supervise(children, strategy: :rest_for_one)
end
end
上記では、 rest_for_one
を指定しました。
MyApp.Supervisor.init(:ok)
をコールすると、再起動ストラテジーがセットされます。
gen_event
Elixirの GenEvent
は、Erlang/OTPの gen_event
に対する上位互換となっており、ストリーム形式のイベントにも対応しています。
例を見てみましょう。
{:ok, pid} = GenEvent.start_link()
stream = GenEvent.stream(pid)
spawn_link fn ->
for event <- stream do
IO.puts "Got: #{IO.inspect(event)}"
end
end
GenEvent.notify(pid, :hello)
GenEvent.sync_notify(pid, :world)
GenEvent.notify/2
をコールすると、イベントマネージャーに通知され、Erlang/OTPの Module:handle_event/2
がイベントハンドラごとにコールされます。
GenEvent.notify/2
は非同期であり、通知が送られると、即座に return します。
これに対して、 GenEvent.sync_notify/2
は同期処理を行います。
イベントハンドラを定義していない点に注目して下さい。
イベントマネージャーに対するイベントハンドラの登録は、ストリームを受信した際に、動的に行われます。
また、ストリームを行うプロセスが死んだ場合、イベントハンドラはイベントマネージャーから削除されます。
これらの挙動により、ストリームの安全性が保証されています。
通常、 GenEvent
を利用する際はイベントハンドラによりコールバックを定義する必要がありますが、ストリームの場合はそれ無しで動作するので、大変便利です。
gen_fsm
gen_fsm
のElixir実装である GenFSM
は、v0.12.5で廃止となりました。
Agent
廃止された GenFSM
に代わって実装されたのが、 Agent
です。
Elixirのプロセス、アクターモデル、 gen_fsm
で実現するステートマシーン、これらを昇華させ、エージェントモデルを実現する振る舞いが、Elixirの Agent
です。
(そういう意味では、eXAT - The erlang eXperimental Agent Tool に近いコンセプトかもしれません。)
Agent
は、 GenServer
を継承して作られています。
ハンドラとしては call
, cast
があり、 call
には get
, get_and_update
, update
, stop
, (任意の)msg があります。
ハンドラ以外には、 init
, terminate
, code_change
があります。 code_change
は、Erlangではおなじみの、ホット・コード・スワッピングのための機構です(※未確認)。
これらが、Elixir上ではAPIとして機能し、プロセス間でのステート遷移をサポートします。
Elixirのコード内では、 Mix.TasksServer
で使われています。以下のような実装になっています。
defmodule Mix.TasksServer do
@moduledoc false
def start_link() do
Agent.start_link(fn -> HashSet.new end, name: __MODULE__)
end
def clear() do
update fn _ -> HashSet.new end
end
def run(tuple) do
get_and_update fn set ->
{not(tuple in set), Set.put(set, tuple)}
end
end
def put(tuple) do
update &Set.put(&1, tuple)
end
def delete_many(many) do
update &Enum.reduce(many, &1, fn x, acc -> Set.delete(acc, x) end)
end
defp get_and_update(fun) do
Agent.get_and_update(__MODULE__, fun, 30_000)
end
defp update(fun) do
Agent.update(__MODULE__, fun, 30_000)
end
end
agent.ex には、Module Docが丁寧に書かれています。詳しく知りたい方は、ぜひ読んでみて下さい。
Task
Task
モジュールは、非同期でデータの処理を行うプロセス同士のスワッピングに便利な機能を提供します。
Task
の定義はこのように行います。
task = Task.async(fn -> do_some_work() end)
res = do_some_other_work()
res + Task.await(task)
Task
の定義ができたら、リモートのノードでスーパーバイザーを start_link
します。
Task.Supervisor.start_link(local: :tasks_supervisor)
クライアント側では、このようにコールします。
Task.Supervisor.async({:tasks_supervisor, :remote@local}, fn -> do_work() end)
RPCの実装が簡単になりますね。
まとめ
少し取り留めの無い感じになってしまいましたが、OTP周りに関するElixirにおける特徴を挙げてみました。
Elixirからは、Erlang/OTPを踏襲しつつも、更に発展させていこうというコンセプトが見受けられますね。
明日は @niku さんです。