Elixir
ElixirDay 22

ElixirとOTP周りの関係

More than 3 years have passed since last update.

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 さんです。