Erlang の gen_server ビヘイビアを学ぶ
Erlang/OTP の gen_server ビヘイビアについて学びたいと思います。
クライアント・サーバの原則
クライアントサーバーモデルは、複数の異なるクライアントで共有するリソース管理に使用されます。サーバーは、このリソースの管理を担当します。
図の実線はQuery, 破線はReplyを表す。
サンプルコード
gen_server のサンプルコードとして sample_queue.erl というファイル名で下記のコードを作成します。 このコードは、gen_server のビヘイビアを使って、キューの機能を提供します。sample_queue.erl モジュールは、gen_server のコールバックモジュールです。
in/1
関数で gen_server にアイテムを登録して、out/0
関数で gen_server からアイテムを取得します。
ここで、start_link/0
, in/1
, out/0
関数はインターフェース関数と呼ばれます。init/1
, handle_call/3
, handle_cast/2
関数はコールバック関数と呼ばれます。インターフェース関数とコールバック関数は同じモジュールに配置されます。一般的に、1つのプロセスに対応するコードは1つのモジュールにまとめられます。
ただし、インターフェース関数の実行は gen_server を起動したり、gen_server にリクエストを送信する Erlang プロセス側で実行されます。ここでのコールバック関数は、gen_server として起動する Erlang プロセス側で実行されるものであることに注意してください。
-module(sample_queue). -behaviour(gen_server). -export([start_link/0, in/1, out/0]). -export([init/1, handle_call/3, handle_cast/2]). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). in(Item) -> gen_server:cast(?MODULE, {in, Item}). out() -> gen_server:call(?MODULE, out). init(_Args) -> {ok, queue:new()}. handle_call(out, _From, Queue) -> case queue:out(Queue) of {{value, Item}, NewQueue} -> {reply, Item, NewQueue}; {empty, NewQueue} -> {reply, empty, NewQueue} end. handle_cast({in, Item}, Queue) -> NewQueue = queue:in(Item, Queue), {noreply, NewQueue}.
下記は使用例です。下記では、sample_queue モジュールの gen_server を起動し、その後、sample_queue モジュールのAPIを使って、a, b, c というアイテムを登録し、その後は逆にキューのアイテムを空になるまで取り出しています。
> c(sample_queue). {ok,sample_queue} > sample_queue:start_link(). {ok,<0.68.0>} > sample_queue:in(a). ok > sample_queue:in(b). ok > sample_queue:in(c). ok > sample_queue:out(). a > sample_queue:out(). b > sample_queue:out(). c > sample_queue:out(). empty
gen_server の起動
サンプルコードの start_link/0
を呼び出すことで gen_server が起動します。
start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
start_link/0
は gen_server:start_link/4
関数を呼び出します。この関数は gen_server として新しいプロセスを spawn し、そのプロセスに link します。
最初の引数 {local, ?MODULE}
は gen_server プロセスの名前を指定します。この gen_server はローカルのErlangノードに ?MODULE
として登録されます。?MODULE
はモジュール名を表します。つまり、この例では sample_queue
です。
名前を省略すると、gen_server の名前は登録されません。代わりに、その pid を使用する必要があります。名前は{global, Name}
としても指定できます。この場合、gen_server は global:register_name/2
を使用して登録されます。
第2の引数も ?MOUDLE
です。これは gen_server のコールバックモジュールの名前を指定します。たいてい start_link
関数をコールバックモジュールに記述するので、そのモジュール自身の名前が使われます。
第3の引数 []
は、コールバック関数 init/1
関数にそのまま渡される1つの erlang term です。今回のサンプルコードでは、データを必要としないので単に無視しています。
第4の引数は[]
は、オプションのリストです。ここでは、gen_server の概要を掴みたいので、オプションには触れません。
gen_server プロセスの名前の登録が成功すると、新しい gen_server プロセスはコールバック関数 sample_queue:init([])
を呼び出します。 init は {ok, State}
を返すことが期待されます。State
は gen_server の内部状態です。 今回の場合は空の queue です。
init(_Args) -> {ok, queue:new()}.
gen_server:start_link
は同期的に実行されます。 gen_server が init 関数によって初期化され、リクエストを受信する準備ができるまで戻りません。
gen_server が監視ツリーの一部である場合、つまりスーパーバイザーによって開始された場合は、gen_server:start_link
を使用しなければなりません。 これとは別に、スタンドアロンの gen_server を起動する gen_server:start
があります。この関数で起動した gen_server は、監視ツリーには含まれません。
同期リクエスト - call
同期リクエストの out/0
は、 gen_server:call/2
を使用して実装されています。同期リクエストではリクエストに対して gen_server からレスポンスが返ってきます。
out() -> gen_server:call(?MODULE, out).
?MODULE
はモジュール名であり、これが gen_server の名前になっています。サンプルコードでは sample_queue
という atom です。これは gen_server の起動に使用される名前と一致する必要があります。 out
が gen_server への実際のリクエストです。
リクエストはメッセージになり、?MODULE
(sample_queue
) という名前の gen_server に送信されます。 リクエストを受信すると、gen_server は handle_call(Request, From, State)
を呼び出します。これは、タプル{reply, Reply, NewState}
を返すことが期待されています。 Reply
はクライアントに送り返されるレスポンスであり、NewState
は gen_server の内部状態の新しい値です。
handle_call(out, _From, Queue) -> case queue:out(Queue) of {{value, Item}, NewQueue} -> {reply, Item, NewQueue}; {empty, NewQueue} -> {reply, empty, NewQueue} end.
この場合、out
リクエストを受け取る handle_call/3
関数の応答は、Queue
に Item
があればそのItem
を返し、なければ empty
を返します。新しい状態はQueue
からItem
を削除したキュー NewQueue
です。
したがって、queue_sample:out/1
の呼び出しは、Item
または empty
を返し、gen_server は新しいリクエストを待機します。待機中の gen_server の内部状態はリクエストに含まれていたアイテムが削除されたキューで更新されています。
非同期リクエスト - cast
非同期リクエストの in/1
は、 gen_server:cast/2
を使用して実装されています。非同期リクエストではリクエストに対して gen_server からレスポンスは返って来ません。単に ok
が返ってきます。
in(Item) -> gen_server:cast(?MODULE, {in, Item}).
?MODULE
はモジュール名であり、これが gen_server の名前になっています。サンプルコードでは sample_queue
という atom です。{in, Item}
が gen_server への実際のリクエストです。
リクエストはメッセージになり、gen_server のプロセス送信すると、ok
が返ってきます。この ok
は gen_server のプロセスのレスポンスではなく、単に gen_server:cast
関数が ok
を返しているだけで、リクエストの結果ではないことに注意してください。また、メッセージを送信しただけで、gen_server がメッセージを受け取ったかどうかも保証されません。
gen_server のプロエスはリクエストを受信すると、handle_cast(Request, State)
を呼び出します。これは、タプル{noreply, NewState}
を返すことが期待されています。 NewState
は、gen_serverの内部状態の新しい値です。
handle_cast({in, Item}, Queue) -> NewQueue = queue:in(Item, Queue), {noreply, NewQueue}.
この場合、新しい状態は Queue
にリクエストに含まれる Item
を登録した新しいキュー NewQueue
です。これで、 gen_server プロセスは内部状態のキューに Item
を登録しました。
停止
監視ツリーでの停止
gen_server が監視ツリーの一部である場合、停止機能は必要ありません。gen_serverは、スーパーバイザーによって自動的に終了します。これがどのように行われるかは、スーパーバイザーに設定されたシャットダウン戦略によって定義されます。
終了前にクリーンアップする必要がある場合は、シャットダウン戦略にタイムアウト値にし、関数initで終了シグナルを補足できるように gen_server を設定する必要があります。 シャットダウンするように命令されると、gen_server はコールバック関数 terminate(shutdown, State)
を呼び出します。この teminate/2
関数に後始末をする処理を記述します。
init(Args) -> ..., process_flag(trap_exit, true), ..., {ok, State}. ... terminate(shutdown, State) -> ..code for cleaning up here.. ok.
スタンドアローンでの停止
もし、gen_server が監視ツリーの一部でなかった場合には、 stop 関数が役に立ちます。
... export([stop/0]). ... stop() -> gen_server:cast(?MODULE, stop). ... handle_cast(stop, State) -> ... create new state {stop, normal, NewState}; handle_cast({in, Item}, Queue) -> .... ... terminate(normal, NewState) -> ..code for cleaning up here.. ok.
停止リクエストを処理するコールバック関数は、タプル {stop, normal, NewState}
を返します。ここで、normal は正常終了であり、NewState
はgen_server の状態の新しい値です。 これにより、gen_serverは terminate(normal, NewState)
を呼び出してから、正常に終了します。
他のメッセージの扱い
gen_server がリクエスト以外のメッセージを受信できるようにする場合、コールバック関数 handle_info(Info, State)
を実装してそれらを処理することができます。 他のメッセージの例は、gen_server が他のプロセス(スーパーバイザーではない)にリンクされていて、終了シグナルをトラップしている場合、そのプロセスの終了メッセージです。
handle_info({'EXIT', Pid, Reason}, State) -> ..code to handle other process exits here.. {noreply, NewState}.
以上で、おおよその gen_server の実装イメージを把握しました。
Erlang/OTP の設計原則を学ぶ - 監視ツリーとビヘイビア
監視ツリー (Supervision Trees)
Erlang/OTPの基本概念は、下記の図に示す監視ツリーです。これはワーカーとスーパーバイザーのアイデアに基づいたプロセス構造です。
- ワーカーは、計算を実行するプロセスで、実際の作業をします。
- スーパーバイザーはワーカーの挙動を監視するプロセスです。スーパーバイザーは何か問題があった場合にワーカーを再起動することができます。
- 監視ツリーは、コードをスーパーバイザーとワーカーに階層的に配置したものです。これによって、耐障害性のあるソフトウェアを設計し、プログラム可能にします。
上記の図の、四角はスーパーバイザーです。円はワーカーを示しています。 スーパーバイザーにぶら下がる形でワーカーが起動し、ぶら下がっているスパーバイザーに監視されます。普通は木構造を構成し、根や節はスーパーバイザーで、葉ノードがワーカーになります。
ビヘイビア・振る舞い (Behaviours)
監視ツリーでは、多くのプロセスに共通する構造・パターンがあります。
例えば、多くのスーパーバイザーは似たような構造をしており、実質的に異なる点はどの子プロセスを監視するかということだけです。
多くのワーカーもサーバー・クライアントの関係であったり、有限状態機械であったり、イベントハンドラーなどのパターンをとります。
ビヘイビア はこれらの一般的なパターンの形式化です。アイデアは、プロセスのコードを一般的な部分(ビヘイビアモジュール)と特定の部分(コールバックモジュール)に分けることです。
ビヘイビアモジュールは Erlang/OTP の一部です。スーパーバイザーなどのプロセスを実装するには、ユーザーはコールバックモジュールを実装するだけで済みます。
標準の Erlang/OTP のビヘイビアは下記の4つあります。
- gen_server - クライアントとサーバーの関係のサーバーを実装するためのビヘイビア
- gen_statem - 状態機械を実装するためのビヘイビア
- gen_event - イベント処理機能を実装するためのビヘイビア
- supervisor - 監視ツリーのスーパーバイザーを実装するためのビヘイビア
コールバックモジュールには、どのビヘイビアのモジュールであるかを指定するために、-behaviour(Behaviour)
というモジュール属性を記述することができます。コンパイラーはこのモジュール属性を理解し、コールバックモジュールに必要なコールバック関数がない場合、警告を発行します。
Erlang の queue モジュールを一通り試す - Okasaki API
cons/2 関数
cons(Item, Q1 :: queue(Item)) -> Q2 :: queue(Item)
キューQ1
の先頭にアイテムを挿入した結果であるキューQ2
を返す。
> L = [b,c]. [b,c] > Q1 = queue:from_list(L). {[c],[b]} > Q2 = queue:cons(a, Q1). {[c],[a,b]} > queue:get(Q2). a
daeh/1 関数
daeh(Q :: queue(Item)) -> Item
キューQ
の最後のアイテムを返す。
Q
が空の場合、理由はempty
で失敗する。
> L = [a,b,c]. [a,b,c] > Q = queue:from_list(L). {[c],[a,b]} > queue:daeh(Q). c
head/1 関数
head(Q :: queue(Item)) -> Item
キューQ
の先頭のアイテムを返す。
Q
が空の場合、理由はempty
で失敗する。
> L = [a,b,c]. [a,b,c] > Q = queue:from_list(L). {[c],[a,b]} > queue:head(Q). a
> E = queue:new(). {[],[]} > queue:head(E). ** exception error: empty in function queue:head/1 called as queue:head({[],[]})
init/1 関数
init(Q1 :: queue(Item)) -> Q2 :: queue(Item)
Q1
から最後のアイテムを削除した結果であるキューQ2
を返す。
Q1
が空の場合、理由はempty
で失敗する。
> L = [a,b,c]. [a,b,c] > Q = queue:from_list(L). {[c],[a,b]} > queue:init(Q). {[b],[a]}
lait/1 関数
lait(Q1 :: queue(Item)) -> Q2 :: queue(Item)
Q1
から最後のアイテムを削除した結果であるキューQ2
を返す。
Q1
が空の場合、理由はempty
で失敗する。
lait/1
の関数名はつづりが間違っているので、 使用しない。
last/1 関数
last(Q :: queue(Item)) -> Item
キューQ
の最後のアイテムを返す。
Q
が空の場合、理由はempty
で失敗する。
> L = [a,b,c]. [a,b,c] > Q = queue:from_list(L). {[c],[a,b]} > queue:last(Q). c
liat/1 関数
liat(Q1 :: queue(Item)) -> Q2 :: queue(Item)
Q1
から最後のアイテムを削除した結果であるキューQ2
を返す。
Q1
が空の場合、理由はempty
で失敗する。
> L = [a,b,c]. [a,b,c] > Q = queue:from_list(L). {[c],[a,b]} > queue:liat(Q). {[b],[a]}
snoc/2 関数
snoc(Q1 :: queue(Item), Item) -> Q2 :: queue(Item)
キューQ1
の最後にアイテムを挿入した結果であるキューQ2
を返す。
> L = [a,b]. [a,b] > Q = queue:from_list(L). {[b],[a]} > queue:snoc(Q, c). {[c,b],[a]}
tail/1 関数
tail(Q1 :: queue(Item)) -> Q2 :: queue(Item)
Q1
か先頭のアイテムを削除した結果であるキューQ2
を返す。
Q1
が空の場合、理由はempty
で失敗する。
> L = [a,b,c]. [a,b,c] > Q = queue:from_list(L). {[c],[a,b]} > queue:tail(Q). {[c],[b]}
Erlang の queue モジュールを一通り試す - 拡張API
drop/1 関数
drop(Q1 :: queue(Item)) -> Q2 :: queue(Item)
Q1
から前方のアイテムを削除した結果であるキューQ2
を返す。
Q1
が空の場合、理由はempty
で失敗する。
> L1 = [a,b,c,d,e]. [a,b,c,d,e] > Q1 = queue:from_list(L1). {[e,d],[a,b,c]} > Q2 = queue:drop(Q1). {[e,d],[b,c]} > L2 = queue:to_list(Q2). [b,c,d,e]
> Q = queue:new(). {[],[]} > queue:drop(Q). ** exception error: empty in function queue:drop/1 called as queue:drop({[],[]}) >
drop_r/1 関数
drop_r(Q1 :: queue(Item)) -> Q2 :: queue(Item)
Q1
から後方のアイテムを削除した結果であるキューQ2
を返す。 Q1
が空の場合、理由はempty
で失敗する。
> L1 = [a,b,c,d,e]. [a,b,c,d,e] > Q1 = queue:from_list(L1). {[e,d],[a,b,c]} > Q2 = queue:drop_r(Q1). {[d],[a,b,c]} > L2 = queue:to_list(Q2). [a,b,c,d]
get/1 関数
get(Q :: queue(Item)) -> Item
キューQ
の前にあるアイテムを返す。
Q1
が空の場合、理由はempty
で失敗する。
> L = [a,b,c]. [a,b,c] > Q = queue:from_list(L). {[c],[a,b]} > queue:get(Q). a
get_r/1 関数
get_r(Q :: queue(Item)) -> Item
キューQ
の後ろにあるアイテムを返す。
Q1
が空の場合、理由はempty
で失敗する。
> L = [a,b,c]. [a,b,c] > Q = queue:from_list(L). {[c],[a,b]} > queue:get_r(Q). c
peek/1 関数
peek(Q :: queue(Item)) -> empty | {value, Item}
Q
の前方のアイテムItem
をタプル{value, Item}
で返す。Q
が空の場合はempty
を返す。
> L = [a,b,c]. [a,b,c] > Q = queue:from_list(L). {[c],[a,b]} > queue:peek(Q). {value,a}
> E = queue:new(). {[],[]} > queue:peek(E). empty
peek_r/1 関数
peek_r(Q :: queue(Item)) -> empty | {value, Item}
Q
の後方のアイテムItem
をタプル{value, Item}
で返す。Q
が空の場合はempty
を返す。
> L = [a,b,c]. [a,b,c] > Q = queue:from_list(L). {[c],[a,b]} > queue:peek_r(Q). {value,c}
> E = queue:new(). {[],[]} > queue:peek_r(E). empty
Docker で byobu を起動しようとしたら locale で怒られた
Docker 内でいろいろいじって遊んでいると複数のターミナルウィンドウが欲しくなりますが、ターミナルウィンドウを得るために docker exec
を実行するのは面倒なので byobu をインストールしました。
# apt update && apt install -y byobu
byobu をインストールした後、起動しようとしたところ、下記のように locale に UTF-8 が必要らしく、怒られました。
# byobu tmux: need UTF-8 locale (LC_CTYPE) but have ANSI_X3.4-1968
現在のロケールはこんな感じ。POSIX
が設定されている。
# locale LANG= LANGUAGE= LC_CTYPE="POSIX" LC_NUMERIC="POSIX" LC_TIME="POSIX" LC_COLLATE="POSIX" LC_MONETARY="POSIX" LC_MESSAGES="POSIX" LC_PAPER="POSIX" LC_NAME="POSIX" LC_ADDRESS="POSIX" LC_TELEPHONE="POSIX" LC_MEASUREMENT="POSIX" LC_IDENTIFICATION="POSIX" LC_ALL=
利用可能なロケールを表示したところ、C.UTF-8
があります。
# locale -a C C.UTF-8 POSIX
これを設定します。
# export LANG=C.UTF-8 # locale LANG=C.UTF-8 LANGUAGE= LC_CTYPE="C.UTF-8" LC_NUMERIC="C.UTF-8" LC_TIME="C.UTF-8" LC_COLLATE="C.UTF-8" LC_MONETARY="C.UTF-8" LC_MESSAGES="C.UTF-8" LC_PAPER="C.UTF-8" LC_NAME="C.UTF-8" LC_ADDRESS="C.UTF-8" LC_TELEPHONE="C.UTF-8" LC_MEASUREMENT="C.UTF-8" LC_IDENTIFICATION="C.UTF-8" LC_ALL=
設定後、無事に byobu
を起動できました。
Erlang の queue モジュールを一通り試す - オリジナルAPI
new/0 関数
new() -> queue()
空のキューを返す。
> queue:new(). {[],[]}
in/2 関数
in(Item, Q1 :: queue(Item)) -> Q2 :: queue(Item)
キューQ1
の後ろにアイテムを挿入した結果のキューQ2
を返す。
> Q = queue:new(). {[],[]} > queue:in(a, Q). {[a],[]} > Q1 = queue:in(a, Q). {[a],[]} > Q2 = queue:in(b, Q1). {[b],[a]} > Q3 = queue:in(c, Q2). {[c,b],[a]} > Q4 = queue:in(d, Q3). {[d,c,b],[a]}
out/1 関数
out(Q1 :: queue(Item)) -> {{value, Item}, Q2 :: queue(Item)} | {empty, Q1 :: queue(Item)}
キューQ1
の前にあるアイテムを削除する。 タプル{{value, Item}, Q2}
を返す。ここで、Item
は削除されたアイテムで、Q2
はItem
を削除した結果のキューである。Q1
が空の場合、タプル{empty, Q1}
が返される。
in/2 の続き > {V5, Q5} = queue:out(Q4). {{value,a},{[d,c],[b]}} > {V6, Q6} = queue:out(Q5). {{value,b},{[d],[c]}} > {V7, Q7} = queue:out(Q6). {{value,c},{[],[d]}} > {V8, Q8} = queue:out(Q7). {{value,d},{[],[]}} > queue:out(Q8). {empty,{[],[]}}
in_r/2 関数
in_r(Item, Q1 :: queue(Item)) -> Q2 :: queue(Item)
キューQ1
の前にアイテムを挿入した結果のキューQ2
を返す。
> Q = queue:new(). {[],[]} > Q1 = queue:in_r(a, Q). {[],[a]} > Q2 = queue:in_r(b, Q1). {[a],[b]} > Q3 = queue:in_r(c, Q2). {[a],[c,b]} > Q4 = queue:in_r(d, Q3). {[a],[d,c,b]}
out_r/1 関数
out_r(Q1 :: queue(Item)) -> {{value, Item}, Q2 :: queue(Item)} | {empty, Q1 :: queue(Item)}
キューQ1
の後ろにあるアイテムを削除する。 タプル{{value, Item}, Q2}
を返す。ここで、Item
は削除されたアイテムで、Q2
はItem
を削除した結果のキューである。Q1
が空の場合、タプル{empty, Q1}
が返される。
in_r/2 の続き > {V5, Q5} = queue:out_r(Q4). {{value,a},{[b],[d,c]}} > {V6, Q6} = queue:out_r(Q5). {{value,b},{[c],[d]}} > {V7, Q7} = queue:out_r(Q6). {{value,c},{[d],[]}} > {V8, Q8} = queue:out_r(Q7). {{value,d},{[],[]}} > queue:out_r(Q8). {empty,{[],[]}}
filter/1 関数
filter(Fun, Q1 :: queue(Item)) -> Q2 :: queue(Item)
Types
Fun = fun((Item) -> boolean() | [Item])
Q1
のすべてのアイテムに対して、キューの前から後ろに順番に Fun(Item)
を呼び出した結果であるキューQ2
を返す。
Fun(Item)
が true
を返した場合、Item
は結果のキューにコピーされる。Fun(item)
が false
を返した場合、Item
はコピーされない。Fun(Item)
がリストを返す場合、リストの要素はItem
の代わりに結果のキューに挿入される。
[Item]
を返すFun(Item)
は、true
を返すことと意味的に同等で、[]
を返すことはfalse
を返すことと意味的に同等である。ただし、リストを返すとアトムを返すよりも多くのガーベッジが構築される。
> Fun = fun(X) -> is_atom(X) end. #Fun<erl_eval.7.126501267> > L = ["a", "b", "c", x, y, z]. ["a","b","c",x,y,z] > Q = queue:from_list(L). {[z,y],["a","b","c",x]} > Q2 = queue:filter(Fun, Q). {[z,y],[x]} > queue:out(Q2). {{value,x},{[z],[y]}}
from_list/1 関数
from_list(L :: [Item]) -> queue(Item)
同じ順序でLのアイテムを含むキューを返す。 リストの先頭項目がキューの先頭項目になる。
> L = [a,b,c]. [a,b,c] > Q = queue:from_list(L). {[c],[a,b]} > queue:out(Q). {{value,a},{[c],[b]}}
is_empty/1 関数
is_empty(Q :: queue()) -> boolean()
Q
が空かどうかをテストし、空の場合はtrue
を返し、そうでない場合はfalse
を返す。
> Q = queue:new(). {[],[]} > queue:is_empty(Q). true > Q1 = queue:in(a, Q). {[a],[]} > queue:is_empty(Q1). false > {V2, Q2} = queue:out(Q1). {{value,a},{[],[]}} > queue:is_empty(Q2). true
is_queue/1 関数
is_queue(Term :: term()) -> boolean()
Term
がキューかどうかをテストし、そうであればtrue
を返し、そうでなければfalse
を返す。
> Q = queue:new(). {[],[]} > queue:is_queue(Q). true > L = []. [] > queue:is_queue(L). false > T = {}. {} > queue:is_queue(T). false > Q2 = {[],[]}. {[],[]} > queue:is_queue(Q2). true > queue:is_queue(Q3). true
len/1関数
len(Q :: queue()) -> integer() >= 0
queue の長さを返す。
> L = [a,b,c,d,e]. [a,b,c,d,e] > Q = queue:from_list(L). {[e,d],[a,b,c]} > queue:len(Q). 5
member/2 関数
member(Item, Q :: queue(Item)) -> boolean()
Item
がQ
の要素に一致する場合はtrue
を返し、そうでない場合はfalse
を返す。
> L = [a,b,c,d,e]. [a,b,c,d,e] > Q = queue:from_list(L). {[e,d],[a,b,c]} > queue:member(a, Q). true > queue:member(f, Q). false
join/2 関数
join(Q1 :: queue(Item), Q2 :: queue(Item)) -> Q3 :: queue(Item)
Q2
の前にQ1
を結合した結果であるキューQ3
を返す。
> L1 = [a,b,c]. [a,b,c] > L2 = [1,2,3]. [1,2,3] > Q1 = queue:from_list(L1). {[c],[a,b]} > Q2 = queue:from_list(L2). {[3],[1,2]} > queue:join(Q1, Q2). {[3],[a,b,c,1,2]} > Q3 = queue:join(Q1, Q2). {[3],[a,b,c,1,2]} > queue:out(Q3). {{value,a},{[3],[b,c,1,2]}}
reverse/1 関数
reverse(Q1 :: queue(Item)) -> Q2 :: queue(Item)
Q1
のアイテムを逆の順序にしたキューQ2
を返す。
> L = [1,2,3,4,5]. [1,2,3,4,5] > Q = queue:from_list(L). {[5,4],[1,2,3]} > queue:out(Q). {{value,1},{[5,4],[2,3]}} > Q_r = queue:reverse(Q). {[1,2,3],[5,4]} > queue:out(Q_r). {{value,5},{[1,2,3],[4]}}
to_list/1 関数
to_list(Q :: queue(Item)) -> [Item]
キューのアイテムを同じ順序でリストにして返す。 キューの先頭のアイテムがリストの先頭になる。
> Q1 = queue:new(). {[],[]} > Q2 = queue:in(a, Q1). {[a],[]} > Q3 = queue:in(b, Q2). {[b],[a]} > Q4 = queue:in(c, Q3). {[c,b],[a]} > queue:to_list(Q4). [a,b,c]
split/2 関数
split(N :: integer() >= 0, Q1 :: queue(Item)) -> {Q2 :: queue(Item), Q3 :: queue(Item)}
Q1
を2つに分割する。N
個のフロントアイテムはQ2
に、残りはQ3
に入れられる。
> L = [a,b,c,d,e]. [a,b,c,d,e] > Q1 = queue:from_list(L). {[e,d],[a,b,c]} > {Q2, Q3} = queue:split(3, Q1). {{[c],[a,b]},{[e],[d]}} > Q2. {[c],[a,b]} > Q3. {[e],[d]} > queue:len(Q2). 3 > queue:len(Q3). 2
Erlang の queue モジュールを一通り試す - 準備
キューを扱いたかったので queue モジュールを学ぶ。
引数が間違った型の場合、たとえば、キュー引数がキューではなく、インデックスが整数ではなく、リスト引数がリストではない場合、すべての関数はbadarg
で失敗する。 不適切なリストは内部クラッシュを引き起こす。 キューの範囲外のインデックスも、badarg
で失敗する。
いくつかの関数は、空のキューで失敗する。
実行時間はO(n)
であるfilter/2
, join/2
, len/1
, member/2
, split/2
を除いて、すべての関数はO(1)
である。
キュー操作によって構築されるガーベッジの量を最小限に抑えるため、キューのサイズを最小限にする。そのため、キューには明示的に長さ情報が含まれておらずlen/1
がO(n)
になる。
この特定の操作がのパフォーマンスを向上させることが重要な場合、ユーザは簡単に長さを追跡することができる。
キューはダブルエンド(double-ended)である。キューのイメージは、順番を待っている人(や物)の列である。キューの前は、最も長く待機したアイテムのエンドである。キューの後は、アイテムが待機を開始するときに入るエンドである。
前から入り、後ろから出るのはキューの逆の操作である。
このモジュールには、"Original API" と "Extended API" と "Okasaki API" の3つのインターフェース関数のセットがある。
"Original API" と "Extended API" はどちらも、アイテムの待機列のイメージを使う。両方とも"_r"という接尾辞が付いた逆の操作がある。
"Original API" のアイテム削除関数は、削除されたアイテムと結果のキューの両方を含む複合語を返す。
"Extended API"には、ガベージを削減する代替関数と、キューの終了を検査するための関数が含まれてる。
また、"Okasaki API" 関数はガーベッジを少なくする。
"Okasaki API" は Chris Okasaki による「Purely Functional Data Structures」に触発さされている。これはキューをリストと見なす。