Edited at

[翻訳] ElixirにおけるOTPの紹介

More than 3 years have passed since last update.

前回に引き続き、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/1FizzBuzz.print/1はサーバーの主なインターフェースです。数値をひとつ引数として与えるとその数値に関連したFizzBuzzの値を出力します。GenServerは2種類のリクエストをサポートしています:callcastです。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/1GenServer.start_link/3から呼び出され{:ok, state}の形のタプルを返します。この例のケースではstate(状態)は単なるElixirのマップ(%{})です。

handle_call/2handle_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のアプリケーションの一部としてデプロイ可能になりました。つまり監視とホットスワップという特長を持つに至ったわけです。


リソース