無題の備忘録

IT技術について調べたことや学んだこと、試したこと記録するブログです。Erlang、ネットワーク、 セキュリティ、Linux関係のものが多いです。

Erlang の gen_server ビヘイビアを学ぶ

Erlang/OTP の gen_server ビヘイビアについて学びたいと思います。

クライアント・サーバの原則

クライアントサーバーモデルは、複数の異なるクライアントで共有するリソース管理に使用されます。サーバーは、このリソースの管理を担当します。

f:id:storkibis:20191014151228p:plain

図の実線は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/0gen_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 関数の応答は、QueueItemがあればその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の基本概念は、下記の図に示す監視ツリーです。これはワーカーとスーパーバイザーのアイデアに基づいたプロセス構造です。

  • ワーカーは、計算を実行するプロセスで、実際の作業をします。
  • スーパーバイザーはワーカーの挙動を監視するプロセスです。スーパーバイザーは何か問題があった場合にワーカーを再起動することができます。
  • 監視ツリーは、コードをスーパーバイザーとワーカーに階層的に配置したものです。これによって、耐障害性のあるソフトウェアを設計し、プログラム可能にします。

f:id:storkibis:20191014140208p:plain

上記の図の、四角はスーパーバイザーです。円はワーカーを示しています。 スーパーバイザーにぶら下がる形でワーカーが起動し、ぶら下がっているスパーバイザーに監視されます。普通は木構造を構成し、根や節はスーパーバイザーで、葉ノードがワーカーになります。

ビヘイビア・振る舞い (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は削除されたアイテムで、Q2Itemを削除した結果のキューである。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は削除されたアイテムで、Q2Itemを削除した結果のキューである。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()

ItemQの要素に一致する場合は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/1O(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」に触発さされている。これはキューをリストと見なす。