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 ∈ A
は、aは集合Aの要素- 集合 - Wikipedia
-
A ∈ B
はA <- 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
になる。
andalso
とorelse
という文もある。,
と;
は前の条件でエラーが発生しても次の条件のマッチを見に行くが、andalso
とorelse
は前の条件でエラーが発生するとそこでマッチ失敗になる。
また、andalso
とorelse
は入れ子にできるが、,
と;
は入れ子にできない。
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]
のように関数の結果からリストを生成する場合も同じ
- Accumulatorが考えられるポイントは以下
6章 高階関数
無名関数
fun(Val1) ->
% 式
(Val2) ->
% 式
(Val3) ->
end.
7章 エラーと例外
終了処理について
終了処理はexit/1
とexit/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) -> ok
やcall(EventMgrRef, Handler, Request) -> Result
を使ってEnvet情報をイベントマネージャに通知- イベントマネージャは、
add_handler
で登録されたgen_event handlerのhandle_event(Event, State) -> Result
やhandle_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_sup
とppool_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_one
かone_for_all
の場合 - 兄弟プロセスの死がtemporary processの死を引き起こした場合
- supervisorのrestart strategyが
- 再起動しない。ただし、以下の場合は再起動する
ワーカの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}
を返した場合は、From
にReply
が渡る。
{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:start_link()
%% 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
このbehaviourを実装している関数は、OTP designのトップレベルのsupervisorということになる。
基本的なcallback関数は以下:
Module:start(StartType, StartArgs) -> {ok, Pid} | {ok, Pid, State} | {error, Reason}
StartType
では分散環境向けの設定をすることができるStartArgs
はApplication Resource Fileのmod
キーにより指定される。
以下のような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]という。
Erlangのシングルクオートとダブルクオートの違い
- シングルクオートはatom。atomはシングルクオートなしでもatomとして扱われるが、シングルクオートで囲むこともできる
- ダブルクオートはString
関数の戻り値
関数の書き方は「〔関数名〕(〔引数〕) ->〔本体〕.」という形式です。〔関数名〕はアトムで、〔本体〕はカンマで区切られた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的なものっぽい
- Executor
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を起動して
小林さんから。並行並列処理は
- task parallel
- タスク並列性 - Wikipedia
- data parallel
にわけられるというコメント。 ※あとで追記する
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は
Kernel
とSTDLIB
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
- lib/
relup
ファイルか、sys.config
ファイルがあれば、これらのファイルもrelease packageに含まれる- これらのファイルはErlang/OTPのホットデプロイのためのもの。詳細はErlang – Release Handling
※ 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について
development, productionなどのように環境ごとにパラメータを設定してビルド・リリースパッケージ作成したかったので、rebar3でそのようなことをサポートしてないか調べてみる。Profilesがそれっぽい。
Profileの設定のしかたは以下の方法がある。
REBAR_PROFILE
環境変数rebar3 as <profile> <command>
- 複数のProfilesは
rebar3 as <profile>, <profile> <command>
のようにカンマ区切りで指定することができる
- 複数のProfilesは
rebar3
のコマンドによってProfileが決定する場合がある。たとえばrebar3 eunit
rebar3 ct
は毎回test
Profileを利用する
設定例はProfiles ·rebar3にいろいろと用意されている。 その他細かいポイントは以下。
rebar3 as prod, native release
はreleaseをnative
Profileとして実行する。prod
は関係ないREBAR_PROFILE=native rebar3 as prod release
の場合は最初にnative
Profileが適用され、その後prod
が適用される(ややこしすぎ- 詳細なProfile適用順ルール
default
REBAR_PROFILE
as
で指定されたProfile- rebar3コマンド設定で指定されたProfile
- 詳細な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について
- 大量のデータにアクセスし、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/1
やnext/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) -> % ...
はOKets:fun2ms(fun(X, Y) -> % ...
はNGets: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
start_link(ServerName, Module, Args, Options) -> Result
で、ServerName={via,Module,ViaName}
のとき、gen_server processはModule
registryに登録される。
このModule
はregister_name/2
などを実装している必要がある。(詳細はドキュメント)
EDocを使ったドキュメント作成・生成
マクロ
Pre defined macros
標準で使えるマクロ一覧は以下。