スポンサーリンク

2016年3月2日

Using OTP with Elixir (GenServer)

とある錬金術師の万能薬(Elixir)

Wait a minute

プログラミングErlangのOTP入門をElixirで行っていく内容です。
前回の記事では、プロセスを使って設計的な部分を学んだが、
今回はGenServerを利用したり、本格的(?)にOTPを使っていきます。
ElixirからOTPってどうやって使うんだ?とか、そもそもOTPって何?って思う人のための記事…(主に私です!!)

Goal

  • プログラミングErlangのOTP入門をElixirで実施し、ブログへアップする(時間制限なし)
  • ElixirからOTPを使うというのは、どのようにすればいいのかサンプルとなるものを作成する
  • とにかく色々作る(だいぶファジー)

Outcome

2016/03/02 23:20 GenServerを使うサンプルを作成した。
Erlang側のソースコードで知らない文法があった。

Analyze

Erlang側の文法がまだ網羅できていない。
Erlangの知らない文法はマクロであった。

Learn

OTP(GenServer)の使い方が分かった。

Content

Dev-Environment

  • OS: Windows8.1
  • Erlang: Eshell V7.2.1, OTP-Version 18.1
  • Elixir: v1.2.0

Index

Using OTP with Elixir (GenServer ed)
|> Prepare
|> Using GenServer

Prepare

プロジェクトの準備を行います。
> cd プロジェクト作成ディレクトリ

> mix new elixir_otp
...

> cd elixir_otp

> mix test
...
本記事内においてプロジェクトと言った場合、”elixir_otp”を指す。
GenServerでコールバックモジュールを書くときの簡単な3点計画と言うものがあるみたいです。
  1. コールバックモジュールの名前を決める
  2. インターフェイス関数を書く
  3. コールバックモジュールの6つの必須コールバック関数を書く
覚えておけば、どっかで役に立つはず~

First Step

1. コールバックモジュール名

モジュール名は”MyBank”
簡単な支払いシステムを作るとのこと。

2. インターフェイスルーチンを書く

  • start()
銀行をオープンする。
  • stop()
銀行をクローズする。
  • new_account(who)
新しい口座を作る。
  • deposit(who, amount)
銀行にお金を預ける。
  • withdraw(who, amount)
残高が十分であればお金を引き出す。
上記のインターフェイスルーチンを実装します。

File: lib/my_bank.ex

defmodule MyBank do
  use GenServer

  ## Interface routine
  def start do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  def stop do
    GenServer.call(__MODULE__, :stop)
  end

  def new_account(who) do
    GenServer.call(__MODULE__, {:new, who})
  end

  def deposit(who, amount) do
    GenServer.call(__MODULE__, {:add, who, amount})
  end

  def withdraw(who, amount) do
    GenServer.call(__MODULE__, {:remove, who, amount})
  end
end
GenServer.call/2は、サーバに対して遠隔手続きの呼び出しをするために使われます。

3. コールバックルーチンを書く

6つのコールバックルーチンを実装します。
  • init/1
  • handle_call/3
  • handle_cast/2
  • handle_info/2
  • terminate/2
  • code_change/3
GenServer用のテンプレート。(自分用です)

Example

defmodule GenServerMiniTemplate do
  use GenServer

  def start_link(server) do
    GenServer.start_link(__MODULE__, [server], name: __MODULE__)
  end

  def init([]) do
    {:ok, state}
  end

  def handle_call(_request, _from, state) do
    {:reply, reply, state}
  end

  def handle_cast(_msg, state) do
    {:noreply, state}
  end

  def handle_info(_info, state) do
    {:noreply, state}
  end

  def terminate(_reason, _state) do
    :ok
  end

  def code_change(_old_vsn, state, extra) do
    {:ok, state}
  end
end
インターフェイスルーチンの引数がテンプレートの引数と一致するようにします。
今回重要になるのは、handle_call/3のコールバック…
インターフェイスルーチンにある3つの問合せに対応する必要がある。

File: lib/my_bank.ex

defmodule MyBank do
  use GenServer

  def start do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  def stop do
    GenServer.call(__MODULE__, :stop)
  end

  def new_account(who) do
    GenServer.call(__MODULE__, {:new, who})
  end

  def deposit(who, amount) do
    GenServer.call(__MODULE__, {:add, who, amount})
  end

  def withdraw(who, amount) do
    GenServer.call(__MODULE__, {:remove, who, amount})
  end

  def init([]) do
    {:ok, :ets.new(__MODULE__, [])}
  end

  def handle_call({:new, who}, _from, tab) do
    reply = case :ets.lookup(tab, who) do
              [] ->
                :ets.insert(tab, {who, 0})
                {:welcome, who}
              [_] ->
                {who, :you_already_are_a_customer}
            end
    {:reply, reply, tab}
  end

  def handle_call({:add, who, x}, _from, tab) do
    reply = case :ets.lookup(tab, who) do
              [] -> :not_a_customer
              [{who, balance}] ->
                new_balance = balance + x
                :ets.insert(tab, {who, new_balance})
                {:thanks, who, :your_balance_is, new_balance}
            end
    {:reply, reply, tab}
  end

  def handle_call({:remove, who, x}, _from, tab) do
    reply = case :ets.lookup(tab, who) do
              [] -> :not_a_customer
              [{who, balance}] when x <= balance ->
                new_balance = balance - x
                :ets.insert(tab, {who, new_balance})
                {:thanks, who, :your_balance_is, new_balance}
              [{who, balance}] ->
                {:sorry, who, :you_only_have, balance, :in_the_bank}
            end
    {:reply, reply, tab}
  end

  def handle_call(:stop, _from, tab) do
    {:stop, :normal, :stopped, tab}
  end

  def handle_cast(_msg, state) do
    {:noreply, state}
  end

  def handle_info(_info, state) do
    {:noreply, state}
  end

  def terminate(_reason, _state) do
    :ok
  end

  def code_change(_old_vsn, state, _extra) do
    {:ok, state}
  end
end

Note:

init/1は、{:ok, state}を返す必要がある。
このときのstateの値は、handle_callの第三引数に使われる。

handle_call(:stop, _from, tab)は、サーバの停止を意味している
{:stop, :normal, :stopped, tab}を返す。
このときの:stoppedはstop/0の戻り値になる。
何はともあれ、実行してみましょう。

Example:

## 一通り使ってみる
iex> MyBank.start
{:ok, #PID<0.100.0>}
iex> MyBank.deposit("darui", 10)
:not_a_customer
iex> MyBank.new_account("darui")
{:welcome, "darui"}
iex> MyBank.deposit("darui", 10)
{:thanks, "darui", :your_balance_is, 10}
iex> MyBank.deposit("darui", 30)
{:thanks, "darui", :your_balance_is, 40}
iex> MyBank.withdraw("darui", 15)
{:thanks, "darui", :your_balance_is, 25}
iex> MyBank.withdraw("darui", 45)
{:sorry, "darui", :you_only_have, 25, :in_the_bank}

## もう一人アカウントを作ってみる
iex> MyBank.deposit("hoge", 1000)
:not_a_customer
iex> MyBank.new_account("hogehoge")
{:welcome, "hogehoge"}
iex> MyBank.deposit("hogehoge", 1000)
{:thanks, "hogehoge", :your_balance_is, 1000}
iex> MyBank.deposit("darui", 10)
{:thanks, "darui", :your_balance_is, 35}

## サーバを止めてみる
iex> MyBank.stop
:stopped
iex> MyBank.deposit("darui", 10)
** (exit) exited in: GenServer.call(MyBank, {:add, "darui", 10}, 5000)
    ** (EXIT) no process
    (elixir) lib/gen_server.ex:564: GenServer.call/3
iex> MyBank.start
{:ok, #PID<0.115.0>}
iex> MyBank.deposit("darui", 10)
:not_a_customer
GenServerのインターフェイス関数を全てやったわけではないですが、
これが基本的な考え方とのことです。
今回のは簡単なサンプルですが、大概はこれで十分対応できます。
後は、少し複雑になりますが、GenServerにはnoreplyで応答させ、
本当の応答は別のプロセスに委任する手法が多いみたいです。
それに関しては、Erlangの以下のドキュメントが参考になるそうです。
まぁ、それは追々やりましょう。

Improve

Erlang側の文法をもう少し網羅する必要がある。
ブログのテンプレートが分かり辛い。これならば前の方が良かった。
同様に学習メソッドにも改善が必要である。

Prepare

なし

Speaking to oneself

人に読まれることを考えると、今のブログテンプレートは良くないので改善します。
全体的に質が下がった気がする…やっちまったな(汗)

Bibliography

人気の投稿