環境
$ 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 でプロジェクトを作ってみる。
$ 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
猫を示すデータは構造体で。
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
$ mix format
$ iex -S mix
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 のプロトタイプとして見ることができる。
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
子猫サーバの汎用化
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
Kitty2 は Kitty と等価に動作する。
14.4 コールバック・トゥ・ザ・フューチャー
OTP GenServer のコールバック関数の紹介。
ざっと読んでコンセプトを理解する。
後は hexdocs を眺める。
GenServer — Elixir vx.x.x
14.5 スコッティ、転送を頼む
先の Kitty プロジェクトをルートとする。
$ mkdir lib/kitty
$ touch lib/kitty/genserver.ex
defmodule Kitty.GenServer do
use GenServer
end
use GenServer の意味ってなんだろ?
Erlang だと -behavior(gen_server) と宣言する。
そして gen_server であるための必須条件としてコールバックを実装する。
このコールバックの実装が、振る舞い(ビヘイビア)の操作的意味を与える。
したがって、コールバックの実装が不足していると Erlang はエラーを吐く。
Java のインターフェースに近いのかな?
$ 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 が無いよと警告が出た。
説明通りに実装してみる。
defmodule Kitty.GenServer do
use GenServer
@impl true
def init(init_arg), do: {:ok, init_arg}
end
@impl true はコンパイラに対するアノテーション。
「何かしらのビヘイビアのコールバックです」と告げている。
「GenServer のコールバックです」と告げる場合は @impl GenServer と書く。
書かなくてもコンパイル通るけど、書いておいた方がドキュメント性が増すね。
$ mix compile
Compiling 1 file (.ex)
Elixir の場合、init/1 のみを実装すればいいんだね。楽だ。
ということは、これって Erlang と少し意味が違うね。
-behavior(gen_server) はインターフェースの取り込みで、
use GenServer は抽象クラスの取り込みに近いかな?
あれ?抽象クラスは多重継承できないんだったっけ?じゃ、違うか。
なんにせよ、Elixir の方は意味的には「継承」になってるんだね。
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
$ mix format
$ iex -S mix
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
うむ。