Help us understand the problem. What is going on with this article?

すごいE本 第14章 on Elixir (GenServer)

環境

sh
$ lsb_release -d
Description:    Ubuntu 18.04.2 LTS

$ elixir -v
Erlang/OTP 21 [erts-10.3.4] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

Elixir 1.8.1 (compiled with Erlang/OTP 20)

14.2 基本的なサーバ

子猫サーバの紹介

Mix でプロジェクトを作ってみる。

sh
$ mix new kitty
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/kitty.ex
* creating test
* creating test/test_helper.exs
* creating test/kitty_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd kitty
    mix test

Run "mix help" for more commands.

$ cd kitty
$ tree
.
├── README.md
├── config
│   └── config.exs
├── lib
│   └── kitty.ex
├── mix.exs
└── test
    ├── kitty_test.exs
    └── test_helper.exs

猫を示すデータは構造体で。

kitty/lib/kitty.ex
defmodule Kitty do
  alias __MODULE__, as: Me

  defstruct [:name, :color, :description]

  defp make_cat(name, color, desc) do
    %Me{name: name, color: color, description: desc}
  end

  ###
  # Client API

  def start_link, do: spawn_link(&init/0)

  # 同期呼び出し
  def order_cat(pid, name, color, desc) do
    ref = Process.monitor(pid)
    send(pid, {self(), ref, {:order, name, color, desc}})

    receive do
      {^ref, cat} ->
        Process.demonitor(ref, [:flush])
        cat

      {:DOWN, ^ref, :process, _pid, reason} ->
        raise inspect(reason)
    after
      5_000 -> raise "timeout"
    end
  end

  # 非同期呼び出し
  def return_cat(pid, cat = %Me{}) do
    send(pid, {:return, cat})
    :ok
  end

  # 同期呼び出し
  def close_shop(pid) do
    ref = Process.monitor(pid)
    send(pid, {self(), ref, :terminate})

    receive do
      {^ref, :ok} ->
        Process.demonitor(ref, [:flush])
        :ok

      {:DOWN, ^ref, :process, _pid, reason} ->
        raise inspect(reason)
    after
      5_000 -> raise "timeout"
    end
  end

  ###
  # Server functions

  defp init, do: loop([])

  defp loop(cats) do
    receive do
      {pid, ref, {:order, name, color, desc}} ->
        case cats do
          [] ->
            send(pid, {ref, make_cat(name, color, desc)})
            loop(cats)

          [cat | cats] ->
            send(pid, {ref, cat})
            loop(cats)
        end

      {:return, cat = %Me{}} ->
        loop([cat | cats])

      {pid, ref, :terminate} ->
        send(pid, {ref, :ok})
        terminate(cats)

      unknown ->
        IO.puts("Unknown message: #{inspect(unknown)}")
        loop(cats)
    end
  end

  defp terminate(cats) do
    cats
    |> Enum.each(fn cat -> IO.puts("#{cat.name} was set free.") end)

    :ok
  end
end
sh
$ mix format
$ iex -S mix
iex
Erlang/OTP 21 [erts-10.3.4] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

Compiling 1 file (.ex)
Generated kitty app
Interactive Elixir (1.8.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> pid = Kitty.start_link()
#PID<0.135.0>
iex(2)> cat1 = Kitty.order_cat(pid, "Carl", :brown, "loves to burn bridges")
%Kitty.Cat{color: :brown, description: "loves to burn bridges", name: "Carl"}
iex(3)> Kitty.return_cat(pid, cat1)
:ok
iex(4)> Kitty.order_cat(pid, "Jimmy", :orange, "cuddly")
%Kitty.Cat{color: :brown, description: "loves to burn bridges", name: "Carl"}
iex(5)> Kitty.order_cat(pid, "Jimmy", :orange, "cuddly")
%Kitty.Cat{color: :orange, description: "cuddly", name: "Jimmy"}
iex(6)> Kitty.return_cat(pid, cat1)
:ok
iex(7)> Kitty.close_shop(pid)
Carl was set free.
:ok
iex(8)> Kitty.close_shop(pid)
** (RuntimeError) :noproc
    (kitty) lib/kitty.ex:51: Kitty.close_shop/1

開始関数

Kitty モジュールを参考に、サーバ汎用化モジュールを作っている。
OTP GenServer のプロトタイプとして見ることができる。

kitty/lib/myserver.ex
defmodule MyServer do
  # このモジュールは、サーバ実装者が使う。

  def start(mod, init_state) do
    spawn(fn -> init(mod, init_state) end)
  end

  def start_link(mod, init_state) do
    spawn_link(fn -> init(mod, init_state) end)
  end

  # 同期呼び出し
  def call(pid, msg) do
    ref = Process.monitor(pid)
    send(pid, {:sync, self(), ref, msg})

    receive do
      {^ref, reply} ->
        Process.demonitor(ref, [:flush])
        reply

      {:DOWN, ^ref, :process, _pid, reason} ->
        raise inspect(reason)
    after
      5_000 -> raise "timeout"
    end
  end

  # 非同期呼び出し
  def cast(pid, msg) do
    send(pid, {:async, msg})
    :ok
  end

  # handle_call/3 を実装した側は、
  # 同期呼び出しなのでクライアントへ値を返さなければならない。
  # これは、実装側にクライアントの参照を意識させないための配慮。
  def reply({pid, ref}, reply), do: send(pid, {ref, reply})

  defp init(mod, init_state), do: loop(mod, mod.init(init_state))

  defp loop(mod, state) do
    receive do
      {:async, msg} ->
        loop(mod, mod.handle_cast(msg, state))

      {:sync, pid, ref, msg} ->
        loop(mod, mod.handle_call(msg, {pid, ref}, state))
    end
  end
end

子猫サーバの汎用化

kitty/lib/kitty2.ex
defmodule Kitty2 do
  alias __MODULE__, as: Me

  defstruct [:name, :color, :description]

  defp make_cat(name, color, desc) do
    %Me{name: name, color: color, description: desc}
  end

  ###
  # Client API

  # MyServer を経由して、
  # このモジュールの Server functions 以下が呼び出される。

  def start_link, do: MyServer.start_link(Me, [])

  def order_cat(pid, name, color, desc) do
    MyServer.call(pid, {:order, name, color, desc})
  end

  def return_cat(pid, cat = %Me{}) do
    MyServer.cast(pid, {:return, cat})
  end

  def close_shop(pid) do
    MyServer.call(pid, :terminate)
  end

  ###
  # Server functions

  # 以下は MyServer に対するコールバック関数。
  # MyServer が Kitty2 を操作できるようにハンドルを用意してるわけだね。

  # サーバの初期状態が空リストであることを前提にしている。
  def init([]), do: []

  # MyServer.reply/2 のお陰で、from の詳細を知らなくも済んでいる。
  def handle_call({:order, name, color, desc}, from, cats) do
    case cats do
      [] ->
        MyServer.reply(from, make_cat(name, color, desc))
        cats

      [cat | cats] ->
        MyServer.reply(from, cat)
        cats
    end
  end

  def handle_call(:terminate, from, cats) do
    MyServer.reply(from, :ok)
    terminate(cats)
  end

  def handle_cast({:return, cat = %Me{}}, cats), do: [cat | cats]

  defp terminate(cats) do
    cats
    |> Enum.each(fn cat -> IO.puts("#{cat.name} was set free.") end)

    exit(:normal)
  end
end

Kitty2Kitty と等価に動作する。

14.4 コールバック・トゥ・ザ・フューチャー

OTP GenServer のコールバック関数の紹介。
ざっと読んでコンセプトを理解する。

後は hexdocs を眺める。
GenServer — Elixir vx.x.x

14.5 スコッティ、転送を頼む

先の Kitty プロジェクトをルートとする。

sh
$ mkdir lib/kitty
$ touch lib/kitty/genserver.ex
kitty/lib/kitty/genserver.ex
defmodule Kitty.GenServer do
  use GenServer
end

use GenServer の意味ってなんだろ?

Erlang だと -behavior(gen_server) と宣言する。
そして gen_server であるための必須条件としてコールバックを実装する。
このコールバックの実装が、振る舞い(ビヘイビア)の操作的意味を与える。
したがって、コールバックの実装が不足していると Erlang はエラーを吐く。
Java のインターフェースに近いのかな?

sh
$ mix compile
Compiling 3 files (.ex)
warning: function init/1 required by behaviour GenServer is not implemented (in module Kitty.GenServer).

We will inject a default implementation for now:

    def init(init_arg) do
      {:ok, init_arg}
    end

You can copy the implementation above or define your own that converts the arguments given to GenServer.start_link/3 to the server state.

  lib/kitty/genserver.ex:1

Generated kitty app

コンパイルは成功したが、init/0 が無いよと警告が出た。
説明通りに実装してみる。

kitty/lib/kitty/genserver.ex
defmodule Kitty.GenServer do
  use GenServer

  @impl true
  def init(init_arg), do: {:ok, init_arg}
end

@impl true はコンパイラに対するアノテーション。
「何かしらのビヘイビアのコールバックです」と告げている。
「GenServer のコールバックです」と告げる場合は @impl GenServer と書く。
書かなくてもコンパイル通るけど、書いておいた方がドキュメント性が増すね。

sh
$ mix compile
Compiling 1 file (.ex)

Elixir の場合、init/1 のみを実装すればいいんだね。楽だ。
ということは、これって Erlang と少し意味が違うね。
-behavior(gen_serber) はインターフェースの取り込みで、
use GenServer は抽象クラスの取り込みに近いかな?
あれ?抽象クラスは多重継承できないんだったっけ?じゃ、違うか。
なんにせよ、Elixir の方は意味的には「継承」になってるんだね。

kitty/lib/kitty/genserver.ex
defmodule Kitty.GenServer do
  use GenServer
  alias __MODULE__, as: Me

  defstruct [:name, :color, :description]

  defp make_cat(name, color, desc) do
    %Me{name: name, color: color, description: desc}
  end

  ###
  # Client API

  def start_link do
    # start_link(module, init_arg, options \\ [])
    GenServer.start_link(Me, [])
  end

  # 同期呼び出し
  def order_cat(pid, name, color, desc) do
    GenServer.call(pid, {:order, name, color, desc})
  end

  # 非同期呼び出し
  def return_cat(pid, cat = %Me{}) do
    GenServer.cast(pid, {:return, cat})
  end

  # 同期呼び出し
  def close_shop(pid) do
    GenServer.call(pid, :terminate)
  end

  ###
  # Callbacks

  @impl true
  def init([]), do: {:ok, []}

  @impl true
  def handle_call({:order, name, color, desc}, _from, cats) do
    case cats do
      [] -> {:reply, make_cat(name, color, desc), cats}
      [cat | cats] -> {:reply, cat, cats}
    end
  end

  @impl true
  def handle_call(:terminate, _from, cats) do
    {:stop, :normal, :ok, cats}
  end

  @impl true
  def handle_cast({:return, cat = %Me{}}, cats) do
    {:noreply, [cat | cats]}
  end

  @impl true
  def handle_info(msg, cats) do
    IO.puts("Unexpected message: #{inspect(msg)}")
    {:noreply, cats}
  end

  @impl true
  def terminate(:normal, cats) do
    cats
    |> Enum.each(fn cat -> IO.puts("#{cat.name} was set free.") end)

    :ok
  end

  # code_change/3 は定義しない。
end
sh
$ mix format
$ iex -S mix
iex
iex(1)> {:ok, pid} = Kitty.GenServer.start_link()
{:ok, #PID<0.144.0>}
iex(2)> send(pid, "Test handle_info")
Unexpected message: "Test handle_info"
"Test handle_info"
iex(3)> cat = Kitty.GenServer.order_cat(pid, "Cat Stevens", :white, "not actually a cat")
%Kitty.GenServer{
  color: :white,
  description: "not actually a cat",
  name: "Cat Stevens"
} 
iex(4)> Kitty.GenServer.return_cat(pid, cat)
:ok
iex(5)> Kitty.GenServer.order_cat(pid, "Kitten Mittens", :black, "look at them little paws!")
%Kitty.GenServer{
  color: :white,
  description: "not actually a cat",
  name: "Cat Stevens"
} 
iex(6)> Kitty.GenServer.return_cat(pid, cat)
:ok
iex(7)> Kitty.GenServer.close_shop(pid)
Cat Stevens was set free.
:ok
iex(8)> Kitty.GenServer.close_shop(pid)
** (exit) exited in: GenServer.call(#PID<0.144.0>, :terminate, 5000)
    ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
    (elixir) lib/gen_server.ex:989: GenServer.call/3

うむ。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした