すごいErlangゆかいに学ぼうメモ

Jun 3, 2016 ( Oct 23, 2016 更新 )

1章はじめましょう

リスト内表記

リストとhead/tail

[Var1 | Var2] =[1,2,3]のように書くことで、リストをHeadとTailに分割することができる。

> Something = [1,2,3].
[1,2,3]
> [Head | Tail] = Something.
1
> [Head | Tail].
[1,2,3]

集合操作

  • 集合がわからなかったので、Wikipediaで調べる。。。

  • A ∈ BA <- Bと書ける

  • さらにリスト内表記で [A || {A, B} <- Something] のように書いて特定の要素だけ取り出せる

71> Wether = [{tokyo, fog}, {kanagawa, rainy}, {saitama, fog}, {chiba, sunny}].
[{tokyo,fog},{kanagawa,rainy},{saitama,fog},{chiba,sunny}]
72> FoggyPlaces = [Place || {Place, fog} <- Wether].
[tokyo,saitama]

バイナリ構文

<<100>>のように<<>>で囲った書き方はバイナリ構文で、1つの要素はデフォルトで1バイト=8ビットとして扱われる。各要素のビット数は:を使って<<100:24>>(24ビットで100)のように表すこともできる。

<<値:サイズ/型指定子リスト(複数指定可能。ハイフン区切り)>>の構文で細かく解釈を指定することができる。

  • サイズは型指定子が何も定義されていなければ常にビット
  • <<25:4/unit:8>>は数値26を4バイト(8ビット*4)の整数にエンコードする。以下のバイナリ構文と同じ値を表現する
    • <<0,0,0,25>>と同じ
    • <<25:1/unit:32>>と同じ
1> <<X1/unsigned>> = <<-44>>.
<<"Ô">>
2>
2> X1.
212
5> <<X2/signed>> = <<-44>>.
<<"Ô">>
6> X2.
-44

バイナリ内包表記

  • リスト記法
11> << <<X>> || <<X>> <= <<1,2,3,4>>, X rem 2 == 0 >>.
<<2,4>>

関数の構文

ガード

head([H | _]) -> H.

second([_, X | _]) -> X.

same(X, X) ->
  true;
same(_, _) ->
  false.


old_enough(X) when X >= 16 -> true;
old_enough(_) -> false.

right_age(X) when X >= 16, X =< 104 ->
  true;
right_age(X) ->
  false.

ガードの条件指定はパターンマッチで表現できる。パターンマッチとは別にif文もあるが後述する。

ガード式の基本的なルールは、成功時に true を返さなければいけないことです。もし false を返すか、例外を投げるなら、ガードは失敗です。 とあるのでbooleanを返すconditionを書かなければいけない。

また、複数の条件を書きたいときはガードを使うと完結に書ける。 when X = 16 , X = 17のように,を使うとANDになる。 when X = 16 ; X = 17のように;を使うとORになる。

andalsoorelseという文もある。,;は前の条件でエラーが発生しても次の条件のマッチを見に行くが、andalsoorelseは前の条件でエラーが発生するとそこでマッチ失敗になる。 また、andalsoorelseは入れ子にできるが、,;は入れ子にできない。

compare_two(X, Y) when (X >= 10 andalso Y >= 10) andalso X + Y >= 100 -> true;
compare_two(_, _) -> false.

guardで使えるexpression

caseの例をguardで書きなおそうとしてて、以下のコードがなんで通らないんだろうと思ってたら

insert_guard(X, Set) when lists:member(X, Set) ->
  Set;
insert_guard(X, Set) when lists:member(X, Set) == false ->
  [X | Set].

guardのexpressionはErlangのexpressionしか受け付けないとのことだった。guradのexpressionとして有効なものは以下のドキュメントのページにリストがある。

if文

if文を使うときは、かならずswitchのdefaultのような必ずどんな場合にも一致する条件を作る必要がある。

oh_god(N) ->
  if N =:= 2 -> might_succeed;
    true -> always_does
  end.

Erlangコミュニティ的には; true -> ...else true)の書き方は避けるべきとされている。ifはすべてのパターンを網羅すべきとのこと。

case文

case <expression> of <result> -> <retunr value>; ...の形式で使えるcase文というのもある。if文の条件が増えたらcase文がよさそう。それよりも関数のパターンマッチのほうがいいかもしれないけど…。

insert(X, []) ->
  [X];
insert(X, Set) ->
  case lists:member(X, Set) of
    true -> Set;
    false -> [X | Set]
  end.

2章 モジュール

マクロ

マクロはコードがVM用にコンパイルされる前に置き換えられる。参照は?NAMEのようにして行う。 下記のマクロはコード内で?SPECでタプルを参照するためのもの。

-define(SPEC(MFA),
  {woker_sup,
    {ppool_worker_sup, start_link, [MFA]},
    temporary,
    10000,
    supervisor,
    [ppool_worker_sup]
  }).

4章 型

  • Erlangは動的型付け言語=実行時に型が決定する
  • Erlangは強い型付け言語= 1 + "1" は許容されない

5章 再帰

リストの長さを計算する再帰関数の例。

len([]) -> 0;
len([_]) -> 1;
len([_ | List]) -> 1 + len(List).

上記のような再帰関数を書くと、100万件のリストの結果を返したいときに毎回len(List)の結果をメモリ上に保持しておく必要があり、かなり非効率。そこで、関数にパラメータとして一時変数を定義し、それを参照させる方法を提供している。この一時変数をアキュムレータという。 上記のlen関数はアキュムレータを使って以下のように書き換えることができる。

tail_len(List) -> tail_len(List, 1).

tail_len([_], Acc) -> Acc; % 1件しかないリストなら1が返る
% tail_len([_ | Tail], Acc) -> Acc + tail_len(Tail, Acc). % 2件以上の場合は、Tailをとって再帰
tail_len([_ | Tail], Acc) -> tail_len(Tail, Acc + 1). % 上記の書き換え

正直難解すぎてErlangをやめたくなった。多分練習しないと慣れない…。可能な限りループ処理的なものはリスト内包表記にしたい。 下は整数とオブジェクトを受け取って、整数の回数だけオブジェクトをコピーしたリストを返す関数の実行例。

duplicate(0, _) ->
  [];
duplicate(N, List) ->
  [List | duplicate(N-1, List)].

tail_duplicate(N, Term) ->
  tail_duplicate(N, Term, []).

tail_duplicate(0, _, List) ->
  List;
tail_duplicate(N, Term, List) ->
  tail_duplicate(N-1, Term, [Term | List]).

2分木の実装

2 つの終了条件があります。1 つはノードが空の場合(キーが二分木の中になかった 場合)で、もう 1 つはキーが見つかった場合です。キーが見つからなかった場合にプ ログラムをクラッシュさせたくないので、見つからなかったときには undefined と いうアトムを返します。そうでなくて見つかった場合は{ok, Value}を返します。理 由は、もし値だけ返すと、ノードが undefined というアトムを持っていた場合に、二 分木が正しい値を持っていたのか、それとも検索に失敗したのか、違いが分からない からです。このようにタプルで成功したことをラップしておくと、どちらなのか簡単 に判明します。

{ok, Value}のラップのやり方よさそう。

再帰関数の書き方のコツ

本を参考に、再帰関数でループを実装するコツっぽいものをまとめると以下のようになりそう。

  • 終了条件を書き出す
    • 終了条件のパターンマッチを書く
  • 再帰関数が書けたら、Accumulatorを書けないか考える
    • Accumulatorが考えられるポイントは以下
      • len(List) + 1のように、関数の結果を演算に利用するか?
      • [len(List) | 1]のように関数の結果からリストを生成する場合も同じ

6章 高階関数

無名関数

fun(Val1) ->
  % 式
  (Val2) ->
  % 式
  (Val3) ->
end.

7章 エラーと例外

終了処理について

終了処理はexit/1exit/2がある・。

try catch throew

catchできるエラーの種類は下記のものがある。

  • error
  • exit
  • throw

例外から保護された部分は末尾再帰にできない。例えば以下のような処理。

try
  % 何かの処理
catch
  Exception:Reason -> {caught, Exception, Reason}
end.

以下の処理のofの部分には末尾再帰が適用できる。

try
  % 何かの処理
of
  % 成功パターンなので、末尾最適が書ける
catch
  Exception:Reason -> {caught, Exception, Reason}
end.

9章 一般的なデータ構造への小さな旅

レコード

以下のようにレコード定義を予めしておき、データ構造として扱うことができる。

-record(dog, {name, age}).

erlでレコードを扱うためには、rr(module_name)で読み込まなければいけない。

1> c(record).
{ok,record}
2> #dog{name="john"}.
* 1: record dog undefined
3> rr(record).      
[dog]
4> #dog{name="john"}.
#dog{name = "john",age = undefined,details = undefined}

10章 並行性ヒッチハイクガイド

Erlangでの並行性(Concurrency)と並列性(Parallelism)の定義:

  • 並行
    • アクターが独立して稼働していること
  • 並列
    • アクターが同時に稼働していること

大規模ソフトウェアシステムにおけるダウンタイムの主な原 因は、断続的あるいは一時的なバグであることがいくつかの研究で判明しています (http://dslab.epfl.ch/pubs/crashonly/ を参照)。また、データを破損する ようなエラーはシステムの障害がある部分をできるだけ早く殺し、システムの他の 部分にエラーや悪いデータを伝搬させないようにすべきである、という原則があり ます。

receiveの挙動

self() ! {greet, "Hello!"}のようにしてself()に対して送信されたメッセージは、self()のメッセージボックス中に入ってきた順に積まれる。 読み込みは突っ込まれた順、とのことなのでFIFO。

receive ... endの処理が入った時点でメッセージが受信される。receive実行時にすべてのメッセージを参照するように見える。 ※ 上記は「選択的受信」のサンプルコードから想定している。

11章 マルチプロセスについてもっと

timer:sleepの実装は以下のようになる。

sleep(Time) ->
  receive
  after Time -> ok % Timeはミリ秒
  end.

選択的受信

-module(important_message).
-compile(export_all).

important() ->
  receive
    {Priority, Message} when Priority > 10 ->
      [Message | important()]
  after 0 ->
    normal()
  end.

normal() ->
  receive
    {_, Message} ->
      [Message | normal()]
  after 0 ->
    []
  end.

を実行すると

48> self() ! {100, "high"}, self()! {0, "low"}, self() ! {200, "high"}.
{200,"high"}
49> important_message:normal().
["high","low","high"]
50> self() ! {100, "high"}, self()! {0, "low"}, self() ! {200, "high"}.
{200,"high"}
51> important_message:important().
["high","high","low"]

となるため、receiveに到達すると対象プロセスの全メッセージを処理しているように見える。上記の選択的受信の例だと、パターンにマッチしない場合はafterでマッチしない場合の処理、という書き方になっている。 大量のメッセージが溜まっている場合、receiveは重くなるのかな?と思ったけど、そもそもErlang VM上の軽量プロセスで処理されるのでErlang VMがよしなにマシンリソースを管理してくれるのかな…。と思った。(とは言えメモリはめちゃくちゃでかいメッセージ内容、数だったらやばそう)

↑の通りで、メッセージがマッチしないまま大量に受信ボックスに残されてしまう可能性があり、こういったことがErlangの性能問題になるという注意書きがあった…。

receive
  {name, Message} -> ok;
  Unexpected -> io:format("unexpected: ~p~n", Unexpected) % どんなものにもマッチするのでログだけ出して終わり
  % _ -> ng. 変数を参照せずに終了するならみたいな感じだと思われる。そういう使い方はしなさそうだけど…
end.

12章 エラーとプロセス

リンクとモニター

link(Pid)は自分のプロセスに、Pidのプロセスをリンクする。Pidが例外で終了したら自身も終了する。

register/2でプロセスに名前をつける

register(process_name, Pid),
process_name ! {message, {Something}}.

のように、メッセージ送信のためのPidはregisterで登録したatomに置き換えることができる。 Pidのプロセスが死んだら、registerで登録したatomは開放される。

whereis(process_name)でPidを取得することができる。

registerで登録したatomは、他のプロセスからも参照される。shared stateであり、競合状態である。

behaviour

behaviourは汎化のための機能。コードを一般的な箇所(behaviour)と、specificな箇所(callback module)に分けるための仕組み。 Erlangでよくあるsupervisorパターンを実装するために、-behaviour(Behaviour)を使うことができる。 -behaviour(supervisor)でsupervisorのbehaviourを取り込む。そして、supervisorに必要なcallback関数を実装する。callback関数を実行するモジュールのことを、callback moduleという。

Erlangが提供するsupervisor behaviourを使うときは、init/1関数をcallback functionとして実装する必要がある

16章 イベントハンドラ

gen_eventは以下のようなユースケース時に利用する。

  • gen_event:start_linkでイベントマネージャプロセスを起動
  • gen_event:add_handler(EventManagerRef, GenEventHandler, Args)でイベントマネージャにgen_event behaviourのハンドラ関数を追加
    • add_handlerを使って複数のgen_event handlerを登録することができる
  • notify(EventMgrRef, Event) -> okcall(EventMgrRef, Handler, Request) -> Resultを使ってEnvet情報をイベントマネージャに通知
  • イベントマネージャは、add_handlerで登録されたgen_event handlerのhandle_event(Event, State) -> Resulthandle_call(Request, State) -> Resultを実行する
    • 複数のgen_event handlerが登録されていた場合は、それぞれのgen_eventのcallbackを呼び出す
    • ※T TODO: gen_eventのcallbackでbadargsとかでクラッシュした場合はどうなるのか?イベントマネージャプロセスはクラッシュする?

17章 誰が監督を監督するの?

再起動戦略

one_for_one

  • スーパバイザがたくさんのワーカを監視する
  • ワーカの1つが失敗したら、その1つを再起動する

以下の場合に使うべきである。

  • 監視されるプロセスが独立している場合
  • プロセスが再起動して状態が失われても、他のプロセスに影響がない

one_for_all

  • スーパバイザがたくさんのワーカを監視する
  • ワーカの1つが失敗したら、すべてのワーカを再起動する

以下の場合に使うべきである。

  • ワーカプロセスが互いに依存している
  • あるワーカプロセスのクラッシュが、他のワーカプロセスの状態に影響する

rest_for_one

  • スーパバイザがたくさんのワーカを監視する
    • 新しく起動したワーカは、以前より起動していた古いワーカの影響を受ける
  • あるワーカがクラッシュしたら、そのワーカより後に起動したワーカのみ再起動する

以下の場合に使うべきである。

  • 新しく起動したワーカは、以前より起動していた古いワーカの影響を受ける
  • AがBを起動し、BはCを起動する、というようなチェーン状に依存している関係がある

simple_one_for_one

  • スーパバイザがたくさんのワーカを監視する
    • このスーパバイザが管理するワーカは1種類のみ

以下の場合に使うべきである。

  • 動的にワーカを追加しなければいけない
  • 多数のワーカに素早くアクセスしなければいけない

one_for_oneとsimple_one_for_oneの違いは、

  • one_for_oneは起動したプロセスのリストを起動した順で保持している
  • simple_one_for_oneは起動したプロセスのリストをディクショナリとして保持している
    • こちらのほうがアクセスが早い
    • 起動したワーカがクラッシュしたときに、他にもワーカがたくさんいる場合はこちらのほうが効率的

supervisour beghaviour

start_link/3にModuleを与える。このModuleのinit/1がすべてのErlang子プロセスを返すまで、supervisorはreturnしない。 つまり、準備完了状態にならない。

18章 アプリケーションを作る

アプリケーションの設計で気をつけるべきこと。スーパバイザはErlangプロセスを殺す。そのため、殺されたErlangプロセスが持っていた状態は失われる。

状態には以下のようなものがある。

  • static
    • 設定ファイルや他のErlangプロセス、他のスーパバイザから容易に取得できるもの
  • dynamic
    • 再計算できるデータからなる。初期状態からある状態に変換するために必要だった状態など
  • 再計算できない動的なデータ
    • ユーザの入力、逐次的な外部イベントなど

静的なデータと動的なデータはスーパバイザで持つ。init/1関数で再計算することにより対応できる。 Erlangの子プロセスがクラッシュしたら、スーパバイザは再起動して、静的なデータを注入できる。

玉ねぎの皮理論というものがある。最も重要なデータ・最も取り戻すのが困難なデータは、最も保護されるべきデータである。失敗が許されない箇所を「アプリケーションのエラーカーネル」という。ここでの例外は致命的になる。 以下の原則に則り、リスクを抑える。

  • 関連している操作は同じツリー上におく。関連しないものは別のツリー
  • 致命的なデータは最も安全な核にできるだけ保存する
    • クラッシュが許されないErlangプロセスはツリーの根の近くで動かす
  • 失敗しがちな操作はツリーの深いところにおいておく

superviorのchild specificationについて

17章では、ppool_supppool_worker_supが登場する。これらのsupervisor自身のrestart strategyとchildのrestart strategyは以下のような違いがある。

  • ppool_sup
    • supervisorのstrategy: one_for_all
    • childのstrategy: permanent
  • ppool_worker_sup
    • supervisorのstrategy: simple_one_for_one
    • childのstrategy: temporary

init/1で返すResultのchild specificationのプロパティがわからなかったので調べてみる。

child_spec() = #{id => child_id(),       % mandatory
                 start => mfargs(),      % mandatory
                 restart => restart(),   % optional
                 shutdown => shutdown(), % optional
                 type => worker(),       % optional
                 modules => modules()}   % optional
    child_id() = term()
    mfargs() = {M :: module(), F :: atom(), A :: [term()]}
    modules() = [module()] | dynamic
    restart() = permanent | transient | temporary
    shutdown() = brutal_kill | timeout()
    worker() = worker | supervisor

restart strategyの違いは以下。

  • permenent
    • どんなときも再起動
  • transient
    • 子プロセスが想定外で落ちてしまったときのみ再起動する。具体的には、終了した理由がnormal, shutdown, {shutdown, Term}以外だった場合に再起動する
  • temporary
    • 再起動しない。ただし、以下の場合は再起動する
      • supervisorのrestart strategyがrest_for_oneone_for_allの場合
      • 兄弟プロセスの死がtemporary processの死を引き起こした場合

ワーカのrestart strategyをなぜtemporaryにするか

E本では以下のように説明している。

  • 再起動が必要かどうか知りようがないから
    • これは死ぬときにメッセージに情報を含められれば解決できるのでは?と思ったけど、想定外で落ちた場合は無理か…
  • プールが役に立つのはワーカの生成元(ppool_server)がワーカのPidにアクセスできるとき
    • ワーカの生成元を追跡して再起動の通知をすることなく勝手にワーカを再起動してしまうと、生成元は安全かつ単純な方法でワーカのPidにアクセスできなくなる、とのこと
    • ppool_worker_supが再起動通知を受け取れるなら、ppool_supにこの再起動通知を転送?して、さらにppool_serverに通知できないのかな?と思った

gen_server behaviour

  • client-server モデルのためのbehaviour
  • trace/error reportingのための機能がのっている

gen_* behaviourのプロセスをspawnするプロセスは、gen_* behaviourのinit/1関数がreturnするまで待ってしまう。デッドロックしないように注意する。

handle_call(Request, From, State)

gen_server:call/2,3かgen_server:multi_call/2,3,4が呼び出されたとき、handle_callが呼ばれる。 handle_callが

  • {reply, Reply, NewState}
  • {reply, Reply, NewState, Timeout}
  • {reply, Reply, NewState, hibernate}

を返したときは、FromにReplyを渡す。gen_server:call/2,3かgen_server:multi_call/2,3,4経由で呼びだされた場合も、これらの関数の戻り値としてReplyを返す。 gen_serverはNewStateで自身のstateを更新して、このstateで処理を続ける。

handle_callが

  • {noreply, NewState}
  • {noreply, NewState, Timeout}
  • {noreply, NewState, hibernate}

を返した場合は、自身のstateをNewStateにして処理を続ける。この場合Fromにはgen_server:reply/2の戻り値が渡される。

handle_callが

  • {stop, Reason, Reply, NewState}

を返した場合は、FromReplyが渡る。

  • {stop, Reason, NewState}

を返した場合は、Fromにはgen_server:reply/2の戻り値が渡される。その後、gen_serverはModule:terminate(Reason, NewStateを呼んで終了する。

handle_info(Info, State)

以下の場合に実行される。

  • gen_serverがメッセージを受け取ったとき
  • gen_serverがタイムアウトに達した時
    • gen_server:init/2{ok,Arfs,10}(Timeout=10)のようにTimeoutを指定した場合は、10msec後にhandle_infoが呼ばれる。また、Infoはatomのtimeoutになる

Infoは、タイムアウトに達した時はtimeout(init/2でのタイムアウトについても参照)、それ以外は受け取ったメッセージになる

ppoolの動作の流れ

%% ppool_supersup.erl

start_link() ->
  %% start_linkはrestart strategyを取得するためにinitを呼び出す
  %% localで`ppool`としてPidを登録する(参照できるようになる)
  %% ※ ppoolは最上位のsupervisorなので名前の衝突の心配がない
  %% callback moduleは自身なので?MODULE
  %% ppool_supersup:init/0なのでArgsは[]
  supervisor:start_link({local, ppool}, ?MODULE, []).

%% start_link実行時にrestart strategyを取得するために呼ばれる
init([]) ->
  MaxRestart = 6,
  MaxTime = 3600,
  {ok,
    {
      {one_for_one, MaxRestart, MaxTime},
      []
    }
  }.

ppool_supersup:start_pool(Name, Limit, MFA)

40> ppool:start_pool(nagger, 2, {ppool_nagger, start_link, []}).
{ok,<0.92.0>}

ppoolの最上位のsupervisor, ppool_supersupを起動する。 ppool_supをppool_supersupの子プロセスとして起動する。 また、naggerとしてppool_naggerモジュールを

%% ppool_supersup.erl

%% ppool_supはppool_supersupのchild processesとして起動する
start_pool(Name, Limit, MFA) ->
  ChildSpec = {
    Name,
    %% `start` defines the function call used to start the child process.
    %% {M ,F, A}
    {ppool_sup, start_link, [Name, Limit, MFA]},
    permanent,
    10500,
    supervisor,
    %% `module` is used by release handler during code replacement
    %% if the child process is a supervisor, gen_server, or gen_fsm,
    %% this should be a list with one element [Module],
    %% where Module is the callback module.
    %% 子プロセスの再リリースの際に参照される。単一のcallback functionを指定する。
    %% 子プロセスはppool_supなので、これを参照する。
    [ppool_sup]
  },
  supervisor:start_child(ppool, ChildSpec).

%% ppool_sup.erl

start_link(Name, Limit, MFA) ->
  supervisor:start_link(?MODULE, {Name, Limit, MFA}).

init({Name, Limit, MFA}) ->
  MaxRestart = 1,
  MaxTime = 3600,
  {ok, {
    %% one_for_allはワーカの1つが失敗したら他のすべてのワーカも再起動する
    {one_for_all, MaxRestart, MaxTime}, % SupFlags; strategy, restart intensity, period
    [ % child_spec()
      {
        serv, % Id
        %% ppool_supが監視する、ppool_server/worker_supというErlang子プロセスに
        %% ppool_supのErlangプロセスIDを渡す。
        %% これにより、ppool_serverからwoker_supが監視するwoker Erlang子プロセスを
        %% spawnさせることができる。
        {ppool_serv, start_link, [Name, Limit, self(), MFA]}, % StartFunc
        permanent, % restart policy
        5000, % shutdown time
        worker,
        [ppool_serv]
      }
    ]
  }}.

ppool_supを起動するときに同時にppool_servも起動。この時点でppool_supersup-pool_sup-ppool_servという構成になる。 ppool_serv:start_linkを実行する。ここでppool_servの初期状態(state)がつくられる。 gen_server:initでは{ok, state}の形式で戻り値を指定しければいけない。

%% ppool_serv.erl

start_link(Name, Limit, Sup, MFA) when is_atom(Name), is_integer(Limit) ->
  %% init(Args)に{Limit, MFA, Sup}が渡される
  gen_server:start_link({local, Name}, ?MODULE, {Limit, MFA, Sup}, []).

%% ...省略
init({Limit, MFA, Sup}) ->
  self() ! {start_worker_supervisor, Sup, MFA},
  %% stateとしてgen_serverの初期状態を返す
  %% このstateはgen_server:handle_call(SupRef, From, State)のStateとして扱われる
  {ok, #state{limit=Limit, refs=gb_sets:empty()}}.

handle_call/3のドキュメントに、

State is the internal state of the gen_server.

とあるので、gen_server behaviourでstateを管理してくれており、gen_server:call(Name, {run, Args})すると内部でhandle_call({run, Args}, From, State)の呼び出しに変換される…という理解をしている。

%% ppool_serv.erl
%% ...省略

run(Name, Args) ->
  %% Nameはgen_serverのRef
  %% gen_server:callはgen_server callbackのModule:handle_call/3に{run, Args}を渡す
  gen_server:call(Name, {run, Args}).

%% command from "run" function
%% #stateはinitで初期化されている
handle_call({run, Args}, _From, S = #state{limit=N, sup=Sup, refs=R})
  when N > 0 ->
    %% ppool_supの子プロセスを、gen_server:call(Name(ppool_servの名前), {run, Args})のArgsを与えて起動
    {ok, Pid} = supervisor:start_child(Sup, Args),
    %% Refのstateが変更されたら、{Tag, MonitorRef, Type, Object, Info}の形式でこのプロセスに通知
    %% ppool_serv:handle_infoで通知を受け取る
    Ref = erlang:monitor(process, Pid),
    {reply, {ok, Pid}, S#state{limit=N-1, refs=gb_sets:add(Ref, R)}};
%% ワーカを実行できない場合はnoallocを返す
handle_call({run, _Args}, _From, S=#state{limit=N})
  when N =< 0 ->
    {reply, noalloc, S};

19章 OTP流、アプリケーションの作り方

アプリケーションコントローラ

Erlang VMを起動すると、application_controllerという名前のErlangプロセスが起動する。他のすべてのアプリケーションを起動し、これらすべてに対するスーパーバイザのように振る舞う。 アプリケーションを起動したいと思った時、ACがアプリケーションマスターを起動する。アプリケーションマスターは、ACとアプリケーションの仲介役となる。階層的には以下のイメージ。

  • AC
    • アプリケーションマスター
      • スーパーバイザ(以下がアプリケーション)
        • Erlang プロセス

application behaviour

Erlang -- application

このbehaviourを実装している関数は、OTP designのトップレベルのsupervisorということになる。

基本的なcallback関数は以下:

  • Module:start(StartType, StartArgs) -> {ok, Pid} | {ok, Pid, State} | {error, Reason}
    • StartTypeでは分散環境向けの設定をすることができる
    • StartArgsApplication Resource Filemodキーにより指定される。

以下のようなApplication Resource Fileの設定では、

{application, ch_app,
 [{mod, {ch_app,[]}}]}.

以下のようにcallback関数が呼ばれる。

ch_app:start(normal, [])
  • Module:stop(State)

24章 EUnit

rebar3 eunit

rebar3 eunitでEUnitのテストを楽に走らせるtことができる。標準でtestディレクトリ以下のファイルをコンパイルして、プロジェクトのそれぞれのApplicationに対してeunit:test([{application, App}])する。

  • Erlang – EUnit - a Lightweight Unit Testing Framework for Erlang test representationsにはいろいろな種類がある。eunit:test([{application, App}])の書き方はPrimitivesというもの。Applicationにひもづくmodulesのすべてのテストセットをつくる。Apllicationの設定は、.appファイルにもとづく。もし.appファイルがない場合は、applicationのebinディレクトリを参照する。それもなかったら、code:lib_dir(AppName)ディレクトリを参照する。

わからないところメモ

  • 結局のところtest()test_()の使い分けポイントがわからない… テストジェネレータだけあればよさそう?なので、test_()にしておけばいいということなのか?

28章 common test

基本的な書き方 setup/teardown/case

-module(state_SUITE).
-include_lib("common_test/include/ct.hrl").
-export([all/0, init_per_testcase/2, end_per_testcase/2]).
-export([ets_tests/1]).

all() -> [ets_tests].

%% テストスイート実行前に1回のみ事項
init_per_suite(Config) ->
 Config.

%% テストスイート実行後に1回のみ事項
end_per_suite(Config) ->
 Config.

%% 第1引数はテスト名かグループ名
init_per_testcase(ets_tests, Config) ->
  TabId = ets:new(account, [ordered_set, public]),
  ets:insert(TabId, {andy, 2131}),
  ets:insert(TabId, {david, 12}),
  ets:insert(TabId, {steve, 12943752}),
  %% Configに{table, TabId}を追加
  [{table,TabId} | Config].

%% 第1引数はテスト名かグループ名
end_per_testcase(ets_tests, Config) ->
  %% Configからtable keyのvalue(TabId)を取り出し
  ets:delete(?config(table, Config)).

ets_tests(Config) ->
  TabId = ?config(table, Config),
  [{david, 12}] = ets:lookup(TabId, david),
  steve = ets:last(TabId),
  true = ets:insert(TabId, {zachary, 99}),
  zachary = ets:last(TabId).

特定のテストケースにのみsetup/teardoownしたいときは以下のように書ける:

init_per_testcase(a, Config) ->
  [{some_key, 124} | Config];
init_per_testcase(b, Config) ->
  [{other_key, duck} | Config];
init_per_testcase(_, Config) ->
  %% Ignore for all other cases.
  Config.

テストグループ

groups() -> ListOfGroups.でテストグループを定義することができる。 ListOfGroupsの内容はgroups() -> [{GroupName, GroupProperties, GroupMembers}]のようになる。

GroupPropertiesにはテスト実行時のオプションを指定することができる。オプションの解説

groups() ->
  [{test_case_street_gang,
      [shuffle, sequence],
      [simple_case, more_complex_case, emotionally_complex_case,
        {group, name_of_another_test_group}]}, % グループを1ケースとして呼び出す
    {name_of_another_test_group,
      [],
      [case1, case2, case3]}].

%% 以下は上記groups()と同じ定義。case定義内にグループ定義を入れることができる
groups() ->
  [{test_case_street_gang,
    [shuffle, sequence],
    [simple_case, more_complex_case,
      emotionally_complex_case,
      {name_of_another_test_group,
        [],
        [case1, case2, case3]}
    ]}].

グループ定義はall()で使う。

all() -> [some_case, {group, test_case_street_gang}, other_case].

複雑なテスト

E本の28.5の会議室テストの例がわかりやすい。

specファイル

テストの設定は*.specファイルに記述することができる。

Erlang – Running Tests and Analyzing Results

30章 型仕様とDialyzer

本文とは直接関係ないけど調べたこと

ビッグエンディアン/リトルエンディアン

Erlangのバイナリ構文から。

例えば1234ABCD(16進数)という4バイトのデータを、データの上位バイトからメモリに「12 34 AB CD」と並べる方式をビッグエンディアン[1]、データの下位バイトから「CD AB 34 12」と並べる方式をリトルエンディアン[2]という。

エンディアン - Wikipedia

Erlangのシングルクオートとダブルクオートの違い

  • シングルクオートはatom。atomはシングルクオートなしでもatomとして扱われるが、シングルクオートで囲むこともできる
  • ダブルクオートはString

Erlang – Data Types

関数の戻り値

関数の書き方は「〔関数名〕(〔引数〕) ->〔本体〕.」という形式です。〔関数名〕はアトムで、〔本体〕はカンマで区切られた1つ以上のErlangの式です。関数はピリオドで終わります。Erlangではreturnキーワードを使わないことに注意してください。returnは役立たずです! その代わりに関数の最後の論理式が実行されて、その値が呼び出し元に自動的に戻されます。

とのことだが、戻り値の説明は公式ドキュメント内で見つけられなかった。

1つだけの要素のリストでパターンマッチ

1つだけしか要素のないリストのパターンマッチも可能。その場合、Tail側は空のリストになる。

[Head | Tail] = [1].
Head.
> 1
Tail.
> []

tree.erl:17: Warning: this clause cannot match because a previous clause at line 11 always matches

insert(Key, Val, {node, nil}) ->
  {node, {Key, Val, {node, nil}, {node, nil}}};

%% 新しいキーの方が小さければSmallerに新しいキー・値を挿入する
%% 新しいキーの方が大きければLargerに挿入
insert(NewKey, NewVal, {node, {Key, Val, Smaller, Larger}}) ->
  case NewKey < Key of
    true -> {node, {Key, Val, insert(NewKey, NewVal, Smaller), Larger}};
    false -> {node, {Key, Val, Smaller, insert(NewKey, NewVal, Larger)}}
  end;
%% 新しいキー・値がnodeのキーと同一だった場合は同じnodeを返す
insert(Key, Val, {node, {Key, _, Smaller, Larger}}) ->
  {node, {Key, Val, Smaller, Larger}}.

というコードを書いてコンパイルするとtree.erl:17: Warning: this clause cannot match because a previous clause at line 11 always matchesと言われてしまう。11行目はNewKeyのほうのinsert。Keyという変数が一致しているかでパターンマッチングをかけているつもりなのだが、なぜ注意されてしまうのかわからなかった。

Refとunique referenceとは何か

monitor関数を実行すると、「モニターの参照」としてRefが返ってくる。このRefは結局のところ何かというと、Erlang VM上の一意の値ということらしい。これは、make_ref()関数により取得できる。

参考: erlang - Reference vs pid? - Stack Overflow

make_ref()のドキュメントによるとDistributed Erlang(TCP/IP上で実装される、異なるErlang VM上でメッセージパッシングできる仕組み)のノード間でもほとんど一意だという。ただし、ノードを何回も再起動したり同じ名前で起動すると、すでに終了した古いノードのRefを参照してしまうかもしれないらしい。

unique referenceの生成数には制限があるが、かなり大きな数なので制限に達することはなさそう?(Erlang – Advanced

再帰関数を書かずに、リストに対する逐次処理が書けるか?

できた!

23> PList = [spawn(fun() -> receive {From, Message} -> From ! "I got a message!"
end end)].
[<0.80.0>]
24> [P ! {self(), hello} ||  P <- PList].
[{<0.33.0>,hello}]
25> flush().
Shell got "I got a message!"
ok

UTF16バイト列からUTF8バイト列への変換

116> io:format("~ts~n", [<<2#11100011, 2#10000001, 2#10000010>>]).
あ
ok
117> io:format("~ts~n", [ <<(One bor 2#11100000), (Two bor 2#10000000), (Three bor 2#10000000)>> || <<One:4, Two:6, Three:6>> <= <<16#3042:16>> ]).
あ
ok
136> io:put_chars(io_lib:format("~ts~n", [<< <<(One bor 2#11100000), (Two bor 2#10000000), (Three bor 2#10000000)>> || <<One:4, Two:6, Three:6>> <= <<"あいうえお"/utf16>> >>])).
あいうえお
ok

引数でもパターンマッチ

すごいE本18章より。 function(Var = {_, _, _})みたいな感じで変数を変形できるって知った。

start_link(MFA = {_, _, _}) ->

OTPでわからないところメモ

  • gen_server:handle_callとgen_server:handle_infoのResultタプルの最初のatomがreply, noreplyと同じように見えるが、これはhandle_infoもcallと同じように呼び出し元にResultを送るのか?

  • timeoutをやめたときに「プロセス全体がゾンビ化する」のはなぜか?

handle_info(timeout, {Task, Delay, Max, SendTo}) ->
  SendTo ! {self(), Task},
  if Max =:= infinity ->
    {noreply, {Task, Delay, Max, SendTo}, Delay};
    Max =< 1 ->
      {stop, normal, {Task, Delay, 0, SendTo}};
    Max > 1 ->
      {noreply, {Task, Delay, Max-1, SendTo}, Delay}
  end.
%% 以下のコードは上記のconditionに当てはまらないメッセージが入った場合に
%% タイムアウトをキャンセルする。
%% すごいE本によると、プロセス全体がゾンビ化する??
%% handle_info(_Msg, State) ->
%%   {noreply, State}.

application behavior

applicationはOTPのsupervision tree。treeのstart/stopがどのようになされるべきかをcallback関数に書く。 applicationはApplication.appのような形式のapplication specificationファイルで定義される。

Erlangの時間の扱いについて

Erlang Monotonic Time

そもそもLinuxにPOSIXで定められた時刻取得APIが備わっている。時刻にも種類がある。

  • CLOCK_REALTIME
    • システム全体で使う、人間が日常利用する実時間。変更可能
  • CLOCK_MONOTONIC
    • 単調に進む時間。ユーザからは操作不可能
    • UNIX time??

Erlang Monotinic TimeはこのMonotonic Timeを指すとのこと。OSがサポートしていない場合はMonotonicであることを保証しない。

erlオプション

%% make:all()を実行してerlを起動
erl -make

%% すべての再コンパイルされたモジュールをロードする
%% erlシェル内で実行
make:all([load]).

make:all()はworking dir以下のEmakefileを見て何をコンパイルするか決める。 なかったら、working directory以下のモジュールをコンパイルする。

%% code pathの先頭にDirを追加する
%% erl -pa ebin/
erl -pa Dir1, Dir2 ....

Erlang processes vs Java threads

Java

  • onjectが基本。objectはデータA,B,Cをもち、D(),E(),F()という操作ができる。。

  • Javaは複雑さに対してcompound algorithmsで対応する

  • sequencialで single execution context.複数のthreadは複数のcontext。これは扱うのが難しい。異なるタイミングのコンテキストに、重要なactivityがある場合が特にそう。

  • Javaのthread操作便利ツール

    • Executor
      • thread poolがあってpoolに余裕がある分だけ順次処理
    • fork join
      • map reduce的なものっぽい

Erlang

  • processが世界の最小単位, own time and space
  • computation単位は、この最小単位processで完全にお互いにわかれている
  • Javaのように、ほかのオブジェクトのメソッドや、オブジェクトがもつデータにアクセスすることはできない
  • メッセージ受信の順番も保証しない
  • Erlangプロセスは独自にクラッシュし、意図的に関連づけられた他のErlangプロセスのみ影響を受ける。メッセージも同様。つまり、死んだプロセスの遺言メッセージを受け取るように登録すること。これは、総じてシステムに関連する順序を保証していないし、ユーザーがこのメッセージに反応するかしないかということにも関係がない。
  • Erlangはcontextとスケジューリングが分離している。

This comes at the cost of never knowing exactly the sequence of any given operation once a part of your processing sequences crosses a message barrier – because messages are all essentially network protocols and there are no method calls that can be guaranteed to execute within a given context.

  • Erlangのモデルは「どんな流れでどんな処理をするのかを明確に知ること」を犠牲にしている。これはErlangがNetworkプロトコルをターゲットにしているためだろう。Network protocolは、method callという仕組みがないために、どういうcontextでどういう処理を行うかについての保証はない。
  • Javaで例えると、処理ごとにJVMを起動して

小林さんから。並行並列処理は

にわけられるというコメント。 ※あとで追記する

rebar3について

erlware/relxというリリースツールがある。 rebar3.configにはrelxの設定をそのまま書ける。(Releases · rebar3

デバッガについて

$ rebar3 shell

> debugger:start

Erlangの型について

Erlangにはsubtypeという考え方がある。 typeにはpredefinedなものがあり、それは

  • integer()
  • atom()
  • pid()

など。atom()はErlangのatom termsにひもづいているtype。どのtypeも何らかのtypeにひもづくようになっており、最終的にはErlang termsにいきつく。 interger()やatom()はsingleton。typeはpredefined typesかsingleton typesの集合になる。typeがあるtypeと、そのsubtypeの組み合わせだった場合にはsupertypeに吸収される。

atom() | 'bar' | integer() | 42

の場合は

atom() | integer()

として解釈される。 また、any()はすべてのErlang terms, none()はtermsのempty setを意味する。(なので、どれにも所属しないということを表していると思われる。)

predefined typesは以下に一覧がある。

User defined types

結局のところErlangのtypes definitionというのは、ドキュメントやツールを使ったバグ検知のためだけに用意された仕組みだという理解…だけど合ってるのか?

自分で型を定義したいときは以下のように記述できる。

-type my_struct_type() :: Type.
-opaque my_opaq_type() :: Type.

-typeで通常の型宣言。-opaqueは、このそれ自体が定義されているモジュール外での構造をサポートしないことをあらわす。つまり、opaque typesが定義されているモジュール内でのみ、opaque typesに依存できるということになる。module-localにするのであれば、どうせ同じModuleの中でしか参照できないので、opaqueにしても意味がない。

Typeは上記のpredefined typeリストのものか、user definedのものを指定する。user definedには以下の種類がある。

  • Module-local
    • Moduleのコード内で宣言しているもの
  • Remote
    • 他のModuleからexportedされているもの

Module-localの場合は、定義がコード中にないとコンパイラエラーとなる。これは、recordを使ったときに未定義だとエラーになるのと同じようなもの。 typeの括弧の中にパラメータ情報を含めることができる。パラメータ名はErlangの変数のシンタックスと同じで、最初の文字を大文字にしなければいけない。 type definitionは関数同様exportすることができる。

-export_type([my_struct_type/0, orddict/2]).

関数のexportと同様に、名前/パラメータ数という書き方をする。exportされていないtypeは呼び出すことができない。

-type orddict(Key, Val) :: [{Key, Val}].
具体的にどうtype definitionを使っていくのか?

cowboyの例。自分で定義したhandlerのテストを書きたい。

handlerのinit/2関数にはcowboy:req()というtypeが第1引数として渡ってくる。

rebar3での環境変数について

アプリケーションを起動するときに、起動する環境によって設定を変えたいというのはよくあることだと思う。

ErlangにはApplication Resouce Fileというものがあって、この設定のenvに好きな値を入れることによって、コード内でこの好きな値を取り出せる。ファイル名はApplication.appとなる。 .appファイルで以下のような設定をしたとする。

%% ... 省略
    {env,[{redis_host, "192.168.99.100"}]},

アプリケーションのコード(.erl)内では以下のように書いて、上記のenvredis_hostの値を取得することができる。

{ok,"192.168.99.100"} = application:get_env(message_server, redis_host)

applicationのenvはconfiguration fileとコマンドラインフラグによって上書きされるとのこと。configuration fileというのは.appファイルのこと。これはrebar3を使っているとrebar3が生成してくれる。

rebar3のsys.configというファイルがこのconfiguration fileにあたる。ここにapplicaitonごとの設定を書くことができる。

さらにconfiguration fileの設定はerlのコマンドラインフラグによって上書きされる

Erlangのアプリケーションのリリースについて

Erlang/OTPではApplicationという単位でコンポーネントを定義することができる。また、Erlangでは標準のリリース方法も説明している。これによると、以下のような手順でリリースするとのこと。

1. Release resource fileの作成

  • Release resource fileを作成
    • .relファイルとして、ERTS(Erlang Run-Time System Application)にバージョン情報などを記載する
    • ここにincludeするapplicationを記載する。Erlang/OTPはKernelSTDLIB applicationは必ず必要になるため、includeリストに含めなければいけない。また、relaaseをupgradeする場合(ホットデプロイ?)はSASL applicationも必要。

.relファイルの例は以下。

{release,
 {"ch_rel", "A"},
 {erts, "5.3"},
 [{kernel, "2.9"},
  {stdlib, "1.12"},
  {sasl, "1.10"},
  {ch_app, "1"}]
}.

2. boot scriptの生成

  • systools:make_script/1,2でboot scriptを作成する
  • systools:make_script/1,2
    • systools:make_script("my_application", [local])
      • Opts[local]を指定すると、applicationがある場所を$ROOTディレクトリとして扱う
    • boot scriptのName.scriptと、そのバイナリ版Name.bootを作成する
    • Name.scriptはVMにどのapplicationがロードされるかの設定を書くファイル。わりと読める
    • Name.bootはバイナリ。erl -boot Nameで読み込まれる

erl -bootで起動すると以下のようになる。

% erl -boot ch_rel-1
Erlang (BEAM) emulator version 5.3

Eshell V5.3  (abort with ^G)
1>
=PROGRESS REPORT==== 13-Jun-2003::12:01:15 ===
          supervisor: {local,sasl_safe_sup}
             started: [{pid,<0.33.0>},
                       {name,alarm_handler},
                       {mfa,{alarm_handler,start_link,[]}},
                       {restart_type,permanent},
                       {shutdown,2000},
                       {child_type,worker}]

...

=PROGRESS REPORT==== 13-Jun-2003::12:01:15 ===
         application: sasl
          started_at: nonode@nohost

...
=PROGRESS REPORT==== 13-Jun-2003::12:01:15 ===
         application: ch_app
          started_at: nonode@nohost

3. release packageの作成

  • systools:make_tar/1,2は.relファイルをもとにzipped tar fileをつくる。これがErlangでrelease packageと呼ばれるもの
    • systools:make_tar/1,2
  • release package(tar)の構造は以下のようになる
    • lib/
      • ※.appで記述されたすべてのapplicationのコード
      • my_application/ など
    • releases/
      • .rel
      • .boot
  • relupファイルか、sys.configファイルがあれば、これらのファイルもrelease packageに含まれる

※ release packageは置き場所を限定しなくても動くようにすべき。絶対パスはコード中では使わない。

以下意味がわからなかった点

The release resource file mysystem.rel is duplicated in the tar file. Originally, this file was only stored in the releases directory to make it possible for the release_handler to extract this file separately. After unpacking the tar file, release_handler would automatically copy the file to releases/FIRST. However, sometimes the tar file is unpacked without involving the release_handler (for example, when unpacking the first target system) and the file is therefore now instead duplicated in the tar file so no manual copying is necessary.

rebar3のProfilesについて

Profiles · rebar3

development, productionなどのように環境ごとにパラメータを設定してビルド・リリースパッケージ作成したかったので、rebar3でそのようなことをサポートしてないか調べてみる。Profilesがそれっぽい。

Profileの設定のしかたは以下の方法がある。

  • REBAR_PROFILE環境変数
  • rebar3 as <profile> <command>
    • 複数のProfilesはrebar3 as <profile>, <profile> <command>のようにカンマ区切りで指定することができる
  • rebar3のコマンドによってProfileが決定する場合がある。たとえばrebar3 eunit rebar3 ctは毎回testProfileを利用する

設定例はProfiles ·rebar3にいろいろと用意されている。 その他細かいポイントは以下。

  • rebar3 as prod, native releaseはreleaseをnativeProfileとして実行する。prodは関係ない
  • REBAR_PROFILE=native rebar3 as prod releaseの場合は最初にnativeProfileが適用され、その後prodが適用される(ややこしすぎ
    • 詳細なProfile適用順ルール
      1. default
      2. REBAR_PROFILE
      3. asで指定されたProfile
      4. rebar3コマンド設定で指定されたProfile
  • たとえProfileがprodでも、依存ライブラリはProfile名のディレクトリ以下にダウンロードされる。例)_build/default/libとか_build/test/lib

環境ごとの設定をしたいとき

外部ミドルウェアのエンドポイント設定をしたいときなどには、rebar.configでrelxの設定をprofileごとに設定すればよさそう? erl -config <config file>というオプションでconfigファイルパスを指定できる。relxの実行シェルスクリプト内部でこのconfig fileパスをよしなに設定してくれるが、

試しに書いてみた設定ファイルは以下。

{relx, [
  {release,
    {message_server, "0.0.1"},
    [message_server, sasl]},

  {dev_mode, true},
  {include_erts, false},
  {vm_args, "./config/vm.args"},
  {sys_config, "./config/sys.config"},
  {overlay, [
    {copy, "./apps/message_server/keys", "{{output_dir}}/keys"}
  ]},
  {extended_start_script, true}
]}.

{profiles, [
  {production, [
    {relx, [
      {dev_mode, false},
      {sys_config, "./config/sys.config.production"}
    ]}
  ]}
]}.

  • .rel/.app
  • .script
  • .boot
environment info for logging purposes
echo "Exec: $@" -- ${1+$ARGS}
echo "Root: $ROOTDIR"

# Log the startup
echo "$RELEASE_ROOT_DIR"
logger -t "$REL_NAME[$$]" "Starting up"

# Start the VM
exec "$@" -- ${1+$ARGS}
;;

メモ: set -- # 何かのオプション…で、exec $@したときにsetで設定したパラメータをそのまま渡せるらしい。 引数を処理する | UNIX & Linux コマンド・シェルスクリプト リファレンス

シェルスクリプト実行時に指定された引数は位置パラメータと呼ばれる特殊な変数に自動的に設定される。シェルスクリプト内からはこの変数を参照することで、引数を処理することが可能になる。 $@ シェルスクリプト実行時、もしくはsetコマンド実行時に指定された全パラメータが設定される変数。 変数$*と基本的に同じだが、”で囲んだときの動作が異なる。

gen_server:start_linkについて

start_linkでModule:initが呼ばれるが、ここで呼び出し元のPidとlinkしている?それで、{error, Reason}がModule:initで返された時はクラッシュする?

If Module:init/1 fails with Reason, the function returns {error,Reason}. If Module:init/1 returns {stop,Reason} or ignore, the process is terminated and the function returns {error,Reason} or ignore, respectively.

eunitで起こったこと

Redisに依存するapplicationがあって、Redisにつなぎにいけなかったときにthrowするように書いた(つもり)なのに、

subscribe(State, Channels) ->
  %% TODO: Replace Redis address loaded form ENV
  case has_subscriber(State) of
    false ->
      RedisHost = os:getenv("REDIS_HOST"),
      try
        {ok, Sub} = eredis_sub:start_link([{host, RedisHost}]),
        eredis_sub:controlling_process(Sub),
        eredis_sub:subscribe(Sub, Channels),
        State#chat_handler_state{subscriber=Sub}
      catch
        Exception:Reason ->
          io:format("==== Exception with Reds: ~p~n", [{Exception, Reason}]),
          throw(redis_error)
      end;
    true ->
      eredis_sub:subscribe(State#chat_handler_state.subscriber, Channels),
      State
  end.

redis_errorではなくてRelated process exited with reason:で落ちてしまった。関連するプロセスが落ちるとeunitも落ちる??

~/g/m/message_server ❯❯❯ ERL_FLAGS="-env REDIS_HOST 192.168.99.100" rebar3 eunit                                                               ⏎ master ✱
===> Verifying dependencies...
===> Compiling message_server
/Users/01002532/github/mookjp/message_server/_build/test/lib/message_server/test/chat_handler_test.erl:59: Warning: variable 'ActualReq' is unused
/Users/01002532/github/mookjp/message_server/_build/test/lib/message_server/test/chat_handler_test.erl:59: Warning: variable 'ActualState' is unused

===> Performing EUnit tests...

Pending:
  undefined
    %% Related process exited with reason: {connection_error,
                                     {connection_error,econnrefused}}


Finished in ? seconds
3 tests, 0 failures, 3 cancelled

etsについて

Erlang -- ets

  • 大量のデータにアクセスし、constant access time to dataを実現するためのbuilt-in term storage.
  • データはa set of dynamic tablesとして扱われ、このtableはtuplesを保存することができる
  • tableはプロセスによって生成される
  • プロセスが終了したら、tableも自動的に消滅する
  • tableは作成されるときにアクセス権のセットを持つ
    • public
      • どのプロセスもread/writeできる
    • protected
      • デフォルト
      • ownerプロセスはread/writeできる。他のプロセスはreadのみ可能
    • private
      • ownerプロセスのみread/write可能
  • tablesには以下のtypeがある
    • set
    • ordered_set
    • bag
    • duplicate_bag
  • set, ordered_setはキー1つに1オブジェクトしか保存できない
    • ets:insert/2すると上書きになる
    • ets:insert_new/2で上書きしないで挿入(キーがなかったら挿入)することもできる
  • bag, duplicated_bagはキー1つに複数オブジェクトを保存することができる
    • ets:insert/2するとvalueのリストが増える
  • 1つのErlangノード上に作成できるtablesの数には制限がある。現在のデフォルト制限値は1400
    • ERL_MAX_ETS_TABLES環境変数をErlang runtimeを実行する前に設定することによって、上限値を増加させることができる
      • その場合は--envオプションを利用する必要がある
    • 実際の制限値は、設定値より多少多めになるが、制限値よりも小さくなるということは起こりえない
  • tablesにはガベージコレクションがない
    • どのプロセスからも参照されていないtableがあったとしても、tableのownerプロセスが終了しない限り、tableは自動的に削除されない
    • tableを明示的に削除するにはdelete/1を利用することができる
    • デフォルトのownerはtableを作成したプロセスになる。プロセスが終了するときに、talbeのownershipを委譲したいときにはets:newのheirオプションを設定(予めtableを引き継ぐプロセスを指定する)するか、give_away(Tab, Pid, GiftData) -> true関数で明示的に委譲することができる
  • objectのinsert, look-upはobjectのコピーが結果となる
  • $end_of_tableという特殊なatomはユーザが利用することはできない。これは、tableの終了地点を表すatomとして使われる。fitst/1next/2の戻り値となる
  • new(Name, Options) -> tid() | atom()でtablesを作成できる
    • Optionsを指定しない場合([])は[set, protected, {keypos,1}, {heir,none}, {write_concurrency,false}, {read_concurrency,false}].Optionsとなる
  • tableの内容は同じノード内でのみ共有可能

match_pattern()

etsのオブジェクトを検索するために、match_pattern()を利用することができる。

Erlang -- Match specifications in Erlang

1> ets:new(table, [named_table, bag]).
table
2> ets:insert(table, [{items, a, b, c, d}, {items, a, b, c, a},
2> {cat, brown, soft, loveable, selfish},
2> {friends, [jenn,jeff,etc]}, {items, 1, 2, 3, 1}]). true
3> ets:match(table, {items, '$1', '$2', '_', '$1'}).
[[a,b],[1,2]]
4> ets:match(table, {items, '$114', '$212', '_', '$6'}). [[d,a,b],[a,a,b],[1,1,2]]
5> ets:match_object(table, {items, '$1', '$2', '_', '$1'}). [{items,a,b,c,a},{items,1,2,3,1}]
6> ets:delete(table).
true
  • ets:match(table, {items, '$1', '$2', '_', '$1'})のように書くことによって、itemsの1-4番目の要素に対してパターンマッチをかける
  • $<数字>は、マッチした箇所を結果として取り出すための記法。数字が小さいほど、先に取り出される
    • 同じ数字を使った場合は、同じ値のみマッチという意味になる。例えば、ets:match(table, {items, '$1', '$2', '_', '$1'})の場合は1番目と4番目の要素が同じ値のみマッチとなる
  • _は無視する項目を表す

match_pattern()の書き方を抽象化すると以下のようになる(すごいE本から抜粋):

[{InitialPattern1, Guards1, ReturnedValue1},
     {InitialPattern2, Guards2, ReturnedValue2}].

Guard patternは以下のように解釈される:

%% guard patternが以下の場合
[{'<','$3',4.0},{is_float,'$3'}]

%% 以下のwhen文と同じになる
when Var < 4.0, is_float(Var) -> % ...

%% andalsoを使う場合
[{'andalso',{'>','$4',150},{'<','$4',500}},
     {'orelse',{'==','$2',meat},{'==','$2',dairy}}]

%% 以下のwhen文と同じになる
when Var4 > 150 andalso Var4 < 500, Var2 == meat orelse Var2 == dairy -> % ...
  • 条件評価は、関数はそのまま使えるが、Erlangのexpressionやoperatorは'andalso'のように文字列として指定する
  • match_patternですべての変数を返したいときは$_ですべて返る

match_patternをETSのパース変換を利用して生成する

ets:fun2msを使うと、funからmatch specを生成することができる。 あらかじめmatch spec生成用のfunを定義しておくと、可読性が上がりそう。

13> ets:fun2ms(fun({X,Y}) when X < Y, X rem 2 == 0; Y == 0 -> X end).
[{{'$1','$2'},
  [{'<','$1','$2'},{'==',{'rem','$1',2},0}],
  ['$1']},
 {{'$1','$2'},[{'==','$2',0}],['$1']}]

ets:fun2msを使う場合は以下の制約に注意する。

  • 関数のヘッドは単一の変数またはタプルに対してマッチしなければいけない
    • ets:fun2ms(fun(X) -> % ...はOK
    • ets:fun2ms(fun(X, Y) -> % ...はNG
    • ets:fun2ms(fun({X, Y}) -> % ...はOK
  • 戻り値の一部としてガードされていない関数を返すことができない
    • ローカル関数を戻り値として指定することができない
    • ※「ガードされていない関数」というのがどういうものか確認取れてない。。。
15> MyLocalFunc = fun(X) -> X*X end.
#Fun<erl_eval.6.52032458>
16> ets:fun2ms(fun(X) -> MyLocalFunc(X) end). % これはNG
Error: the language element call (in body) cannot be translated into match_spec
17> fun(X) -> GuardedFunc = fun(X2) -> X2*X2 end, GuardedFunc(X) end. % これはOK
#Fun<erl_eval.6.52032458>
  • バイナリ内の値を割り当てることはできない
    • ets:fun2ms(fun({<<X/binary>>}) -> ok end).はNG
match_patternの使い方

ets:select/2などで使える。 削除の場合は戻り値がbooleanになるようにする。

以下はE本からの抜粋。

17> ets:select_delete(food, ets:fun2ms(fun(#food{price=P})
17> when P > 5 -> true end)).
3

apply関数について

apply(Mod, Func, [Arg1, Arg2, ..., ArgN])Mod:Func(Arg1, Arg2, ..., ArgN)と同じ。 apply関数は、「動的に関数を呼び出したいとき」のみに利用する。 プログラムのコード中にモジュール名を書くのではなく、パラメータとしてモジュール情報を受け取ってspawnしたいときなどが考えられる。

privディレクトリ

TODO: ちゃんとerlangドキュメントからprivディレクトリの位置づけを調べて記載する

  • my_application
    • src
    • priv

のようなディレクトリ構造にしておき、privディレクトリ配下に、アプリケーションのソースコード中から参照したいファイルを配置しておくと、以下のようにcode:priv_dir関数を使ってファイルを参照できる。

PublicKey = jose_jwk:from_pem_file(filename:join([code:priv_dir(message_server), "keys/public_key.pem"])),

pric_dirはビルド後もcode:priv_dirで参照できるので、開発環境でerl shellでアプリケーションを起動していても、production releaseのアプリケーションを起動していても、同じように参照できるのがメリット。

gen_serverとprocess registry

Erlang – gen_server

start_link(ServerName, Module, Args, Options) -> Resultで、ServerName={via,Module,ViaName}のとき、gen_server processはModule registryに登録される。 このModuleregister_name/2などを実装している必要がある。(詳細はドキュメント)

EDocを使ったドキュメント作成・生成

Erlang – Welcome to EDoc

マクロ

Pre defined macros

標準で使えるマクロ一覧は以下。

Erlang – Preprocessor

Retrun to top