前回に引き続き、Michael Kohlさんの2015年2月13日付のブログ記事An intro to OTP in Elixirの翻訳です。
OTPとは何か?
ドキュメントによればOTP-Open Telecom Platform-は「並行プログラミングのために完備された開発環境」で、Erlangコンパイラとインタプリタ、データベースサーバー(Mnesia)、解析ツール(Dyalizer)それに多数のライブラリを含んでいます。人々がOTPについて話をするときに引き合いに出すのはこの後半の部分です。
ビヘイビア(ふるまい)
Erlang/OTPのデザイン原則の中心的なもののひとつはアプリケーションのパターン、OTP用語でいうところの「ビヘイビア」です。ビヘイビアは共通的なタスクに対する汎用的な実装を定義します。その一例として汎用サーバー(gen_server)モジュールがあります。アプリケーション開発者は実装固有のコードを特定の関数のセットとしてエクスポートされているコールバックモジュールに追加します。Erlangとは異なりElixirはこれらの関数についてデフォルトの実装を提供しているので必要な実装をオーバーライドするだけですみます。
GenServerを使ったFizzBuzzサーバー
理論は十分だ、コードを書きましょう!
新しくプロジェクトを作るのにmixを使います。モジュール名はキャメルケース(CamelCase)にしたいので--module
オプションを与える必要があります。そうしないとモジュール名はデフォルトのFizzbuzzになります。
$ mix new fizzbuzz --module FizzBuzz
このコマンドはアプリケーションのテンプレートを一式生成しますが、このブログの投稿記事の目的のためにはlib/fizzbuzz.ex
についてだけ作業します。
ではアプリケーションにGenServer
OTPビヘイビアを使うということを教えましょう:
use GenServer
require Logger
デバッグのためにLoggerモジュールも必要です。次にクライアントのためのAPIを定義します。
def start_link do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
def get(n) do
GenServer.call(__MODULE__, {:print, n})
end
def print(n) do
GenServer.cast(__MODULE__, {:print, n})
end
FizzBuzz.start_link/0
はサーバーをリンクされたプロセスとして起動します。プロセスは監視ツリーの一部となることもあります。サーバーを起動する時にname
オプションを使いモジュール名(__MODULE__
)という名前をつけたことに注意してください。この方法を使うと他の関数からサーバーを参照するのにPIDを使わずに済みます。
FizzBuzz.get/1
とFizzBuzz.print/1
はサーバーの主なインターフェースです。数値をひとつ引数として与えるとその数値に関連したFizzBuzzの値を出力します。GenServerは2種類のリクエストをサポートしています:callとcastです。callは同期呼び出しで何かをクライアント(この例で言うとget関数)に返します。一方でcastは非同期呼び出しでクライアント(同じく、print関数)に値を返す必要はありません。
これはGenServerのインターフェースを個別のクライアントAPIの中でラップするのに一般的な方法なので覚えておいて損はありません。
これで面倒なことは片付いたので必要なGenServerのコールバック関数を実装しましょう。
def init(:ok) do
Logger.debug "FizzBuzz server started"
{:ok, %{}}
end
def handle_call({:print, n}, _from, state) do
{:ok, fb, state} = fetch_or_calculate(n, state)
{:reply, fb, state}
end
def handle_cast({:print, n}, state) do
{:ok, fb, state} = fetch_or_calculate(n, state)
IO.puts fb
{:noreply, state}
end
init/1
はGenServer.start_link/3
から呼び出され{:ok, state}
の形のタプルを返します。この例のケースではstate(状態)は単なるElixirのマップ(%{})です。
handle_call/2
とhandle_cast/2
はこのアプリケーションのコアです。第1引数をパターンマッチングで{:print, n}
の形式のメッセージを処理する方法を指定します。プライベート関数fetch_or_calculate/2
は値を取得、または計算します。そして適切な反応を返します。call
の場合は{:reply, response, state}
の形式、一方 cast
は {:noreply, state}
を返します。GenServerの慣習としてドキュメントは全てのリクエストのタイプについてあり得る全ての返り値のタイプをリストアップします。
これがプライベートヘルパー関数がどのように見えるかです。
defp fetch_or_calculate(n, state) do
if Dict.has_key?(state, n) do
Logger.debug "Fetching #{n}"
{:ok, fb} = Dict.fetch(state, n)
else
Logger.debug "Calculating #{n}"
fb = fizzbuzz(n)
state = Dict.put(state, n, fb)
end
{:ok, fb , state}
end
fetch_or_calculate/2
は数値nに対するFizzBuzzの値が既に計算されていたかどうかをチェックします。もし計算済みであったらそれを辞書から取得します。もしその値がまだ計算されていなかった場合は計算し、状態を新しく計算された値で(Dict.put(state, n, fb))することで更新します。最後にハンドラー関数でパターンマッチした結果に対する{:okm value, state}
の形式のタプルを返します。
よし、これでFizzBuzzサーバーは準備完了です。以下に完成した全コードを示します:
defmodule FizzBuzz do
use GenServer
require Logger
## Client API
def start_link do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
def get(n) do
GenServer.call(__MODULE__, {:print, n})
end
def print(n) do
GenServer.cast(__MODULE__, {:print, n})
end
## Server Callbacks
def init(:ok) do
Logger.debug "FizzBuzz server started"
{:ok, %{}}
end
def handle_call({:print, n}, _from, state) do
{:ok, fb, state} = fetch_or_calculate(n, state)
{:reply, fb, state}
end
def handle_cast({:print, n}, state) do
{:ok, fb, state} = fetch_or_calculate(n, state)
IO.puts fb
{:noreply, state}
end
defp fetch_or_calculate(n, state) do
if Dict.has_key?(state, n) do
Logger.debug "Fetching #{n}"
{:ok, fb} = Dict.fetch(state, n)
else
Logger.debug "Calculating #{n}"
fb = fizzbuzz(n)
state = Dict.put(state, n, fb)
end
{:ok, fb , state}
end
defp fizzbuzz(n) do
case {rem(n, 3), rem(n, 5)} do
{0, 0} -> :FizzBuzz
{0, _} -> :Fizz
{_, 0} -> :Buzz
_ -> n
end
end
end
我々の書いたコードを使う時がきました。プロジェクトのディレクトリでアプリケーションのコンテキストでiexセッションを起動するために以下のコマンドを実行します。
$ iex -S mix
さて、いくつか値を与えてサーバーを起動しましょう。
FizzBuzz.start_link
4..6 |> Enum.map(&FizzBuzz.print/1)
以下はコマンドによって生成された出力です。リクエストに対して非同期で動作するので出力とログメッセージと関数の戻り値が入り混じっています:
4
10:54:58.026 [debug] Calculating 4
Buzz
[:ok, :ok, :ok]
10:54:58.028 [debug] Calculating 5
Fizz
別々のプロセスで走っているにも関わらずIO.puts
の出力はiexのセッションで出てしまうのは、iexがここではグループリーダーになっているからです。
もし、既に計算済みの値を得ようとした(FizzBuzz.get(5))場合はその値が確かにキャッシュから取り出されていることがわかります:
10:58:56.774 [debug] Fetching 5
:Buzz
まとめ
この投稿で、OTPを活用することでサーバーを実装するのがいかに簡単かを見てきました。我々が書いたFizzBuzzサーバーはより大きなElixirのアプリケーションの一部としてデプロイ可能になりました。つまり監視とホットスワップという特長を持つに至ったわけです。