Michael Kohlさんの2014年12月16日付のブログ記事Exploring Elixir processesの翻訳です。象さんがかわいい…。
以前Elixirのプロセスについて翻訳したのですが、それを自分のサンプルに適用しようとしました。というのもあの実装、どこかでエラーが起こると全体が止まっちゃうんで。ところがさてやってみようとしてもよくわからずにどうしたものかと悩んでいました。そこでさらに例を探してこの記事を見つけました。訳しながら勉強します。この記事と次のOTPに関する記事を合わせれば何とか理解できるかなあ…
それにしてもElixirはREPLであるiexの出来がいいのでこういうのを試すのも簡単でいいですね。
Elixir 1.0がリリースされてから3ヶ月近く1経ちましたが私達はまだこの言語についてとても興奮しています。
このブログのポストで以前投稿したFizzBuzzを拡張してサーバーにしてみます。
FizzBuzzサーバー
defmodule FizzBuzz do
@moduledoc "A very simple FizzBuzz server"
@doc """
Computes and outputs the FizzBuzz value for the
provided argument.
"""
@spec compute(Integer) :: :ok
def compute do
receive do
n -> IO.puts "#{n}: #{compute(n)}"
end
compute
end
defp compute(n) do
case {rem(n, 3), rem(n, 5)} do
{0, 0} -> :FizzBuzz
{0, _} -> :Fizz
{_, 0} -> :Buzz
_ -> n
end
end
end
これはいくつかのドキュメンテーションとcompute/0のtypespecを含むシンプルなモジュールでFizzBuzz
モジュールのエントリポイントとして動作します。プロセスのメールボックスから次の番号を取り出し、(プライベートなcompute/1関数を介して)計算、そして自分自身を次のメッセージ待ち受けのために呼び出してから結果を出力するのにreceive/1を使っています。
サーバーとの通信
新しく生成したこのサーバーを使うためにまずspawn/3を使って起動します。spawn/3はモジュール名、呼び出される関数名、及び関数への引数を引数に取ります。spawn/3の戻り値は新しく生成されたprocessのPIDで変数fbに格納されます(Elixirでは関数の引数を囲むかっこはあってもなくてもよいことに注意)。
fb = spawn FizzBuzz, :compute, []
サーバーへメッセージを送るにはsend/2が使えます。
send fb, 3
この結果、"3:Fizz"が出力され、値として3が返されます。ではいくつかのメッセージを送ってみてcompute/0の中で再帰呼び出しが期待通りに動作するか確かめましょう。
10..15 |> Enum.map(&send(fb, &1))
ここではElixirのパイプ演算子(|>/2)で10から15までの数のRangeをEnum.map/2に送ります。Enum.map/2はsend/2の部分適用バージョンを無名関数の引数(&1)とともに使います。結果は期待通り以下のようになりました:
10: Buzz
11: 11
12: Fizz
13: 13
14: 14
15: FizzBuzz
エラーに対処する
ここで、観察力の鋭い読者諸兄はもし整数ではなくそれ以外の型のデータのが送られたらどうなるかと心配されていると思います。やってみましょう:
send fb, 1.0
このために次のようなエラーメッセージが出ます:
14:25:32.742 [error] Error in process <0.95.0> with exit value: {badarith,[{'Elixir.FizzBuzz',compute,1,[{file,"fizzbuzz.ex"},{line,10}]},{'Elixir.FizzBuzz',compute,0,[{file,"fizzbuzz.ex"},{line,4}]}]}
これはFizzBuzzサーバーがクラッシュしたことを意味します。しかしリンクしていないプロセスをあつかっているので、メインプログラム(この場合はiexのセッション)は影響を受けません。ではまたサーバーを起動しましょう。ただし今度はspawn_link/3を使ってリンクされたプロセスとして起動します:
fb = spawn_link FizzBuzz, :compute, []
これでFizzBuzzサーバー内で起きた問題は呼び出し元に伝搬し、メインプロセスを停止させます。
14:33:40.426 [error] Error in process <0.119.0> with exit value: {badarith,[{'Elixir.FizzBuzz',compute,1,[{file,"fizzbuzz.ex"},{line,10}]},{'Elixir.FizzBuzz',compute,0,[{file,"fizzbuzz.ex"},{line,4}]}]}
** (EXIT from #PID<0.116.0>) an exception was raised:
** (ArithmeticError) bad argument in arithmetic expression
fizzbuzz.ex:10: FizzBuzz.compute/1
fizzbuzz.ex:4: FizzBuzz.compute/0
FizzBuzzモデルのcompute/0にガード節を追加することでこのエラーは交通整理できます。このように:
def compute do
receive do
n when is_integer(n) -> IO.puts "#{n}: #{compute(n)}"
_ -> IO.puts "Invalid argument"
end
compute
end
これはうまく動作する一方でErlangの最も重要な原理である「クラッシュさせちまえ」に反するものです。防衛的にプログラミングする代わりに、エラーがプロセスを終了させて(もしあれば)モニタリングしているプロセスにお任せしするのがこの状況への対応になります。
モニタリング
この状況への対応のひとつとしてモニターを設定するやり方があります。リンクとは異なり、これはプロセス間の一方向性の関係です。モニターを生成するために、FizzBuzzサーバーをspawn_monitor/3で起動します。spawn_monitor/3はPIDに加えてモニターへの参照を返します。以下の例ではこの参照は使いませんが手動でモニターを無効にするdemonitor/2を呼び出すのに役立ちます。
サーバーが死んだ場合にはメッセージがモニタリングしているプロセスに送られるようになります:
{fb, ref} = spawn_monitor FizzBuzz, :compute, []
send fb, 1.0
receive/1をiexセッションのメッセージレシーバーをセットアップするのに使っていないので手動でflush/0ヘルパーを呼び出してモニタリングアラートを見る必要があります:
{:DOWN, #Reference<0.0.0.262>, :process, #PID<0.79.0>,
{:badarith,
[{FizzBuzz, :compute, 1, [file: 'fizzbuzz.ex', line: 16]},
{FizzBuzz, :compute, 0, [file: 'fizzbuzz.ex', line: 10]}]}}
次は?
Elixirでプロセスを扱うのはとても簡単です。しかしFizzBuzzサーバーをスクラッチから実装したのでElixirの下まわりを支えているErlang/OTPのパワーをほとんど使っていません。なのでこのシリーズの次回はまたFizzBuzzサーバーです。しかし今度はgen_serverとsupervisorOTPビヘイビアを使います。チャンネルはそのまま!
-
Elixir1.0のリリースは2014年9月18日) ↩