クライアントとサーバー
未来へのコールバック
最初に見ていくOTPビヘイビアは、最もよく使用されるものの1つです。その名前は`gen_server`で、前の章で`my_server`で書いたものと少し似たインターフェースを持っています。いくつかの関数を使用して、その代わりにお使いのモジュールは、`gen_server`が使用するいくつかの関数を既に持っている必要があります。
init
最初の関数は`init/1`関数です。サーバーの状態を初期化し、依存するすべての1回限りのタスクを実行するために使用される点で、`my_server`で使用したものと似ています。この関数は`{ok, State}`、`{ok, State, TimeOut}`、`{ok, State, hibernate}`、`{stop, Reason}`、または`ignore`を返すことができます。
通常の`{ok, State}`戻り値は、Stateが後で保持する状態としてプロセスのメインループに直接渡されることを言う以外に、説明する必要はありません。TimeOut変数は、サーバーがメッセージを受信することを期待する期限が必要な場合に、タプルに追加することを目的としています。期限前にメッセージが受信されない場合、特別なメッセージ(アトム`timeout`)がサーバーに送信され、`handle_info/2`で処理する必要があります(これについては後で詳しく説明します)。
一方、返信を得る前にプロセスが長時間かかることを期待していて、メモリを心配している場合は、タプルに`hibernate`アトムを追加できます。休止状態は、メッセージを受信するまでプロセスの状態のサイズを削減しますが、処理能力を犠牲にします。休止状態の使用について疑問がある場合は、おそらく必要ありません。
`{stop, Reason}`を返すのは、初期化中に何か問題が発生した場合に行う必要があります。
注記: プロセス休止状態のより技術的な定義を以下に示します。一部の読者が理解したり気にしたりしなくても問題ありません。BIF `erlang:hibernate(M,F,A)`が呼び出されると、現在実行中のプロセスのコールスタックは破棄されます(関数は決して戻りません)。その後、ガベージコレクションが実行され、残っているのは、プロセスのデータのサイズに縮小された連続したヒープです。これにより、基本的にすべてのデータが圧縮されるため、プロセスが占めるスペースが少なくなります。
プロセスがメッセージを受信すると、Aを引数とする関数`M:F`が呼び出され、実行が再開されます。
注記: `init/1`の実行中は、サーバーを生成したプロセスで実行がブロックされます。これは、すべてが正常に終了したことを確認するために、`gen_server`モジュールによって自動的に送信される「準備完了」メッセージを待機しているためです。
handle_call
関数`handle_call/3`は、同期メッセージを処理するために使用されます(送信方法はすぐに説明します)。3つの引数Request、From、およびStateを取ります。`my_server`で独自の`handle_call/3`をプログラムした方法と非常によく似ています。最大の違いは、メッセージにどのように返信するかです。独自のサーバーの抽象化では、プロセスに返信するには`my_server:reply/2`を使用する必要がありました。`gen_server`の場合、タプルの形式で8つの異なる戻り値が可能です。
数が多いため、代わりに簡単なリストを以下に示します。
{reply,Reply,NewState}
{reply,Reply,NewState,Timeout}
{reply,Reply,NewState,hibernate}
{noreply,NewState}
{noreply,NewState,Timeout}
{noreply,NewState,hibernate}
{stop,Reason,Reply,NewState}
{stop,Reason,NewState}
これらすべてについて、Timeoutと`hibernate`は`init/1`の場合と同じように機能します。Replyにあるものはすべて、最初にサーバーを呼び出した相手に送り返されます。3つの可能な`noreply`オプションがあることに注意してください。`noreply`を使用する場合、サーバーの一般的な部分は、返信を自分で送信していると考えています。これは`gen_server:reply/2`を使用して行うことができ、`my_server:reply/2`と同じように使用できます。
ほとんどの場合、`reply`タプルのみが必要になります。`noreply`を使用する有効な理由がいくつかまだあります。別のプロセスに返信を送信させたい場合、または確認メッセージ(「メッセージを受信しました!」)を送信したいが、その後も処理を続けたい場合などです(今回は返信なし)。これを行う場合は、`gen_server:reply/2`を使用することが絶対に必要です。そうでない場合、呼び出しがタイムアウトしてクラッシュが発生します。
handle_cast
`handle_cast/2`コールバックは、`my_server`にあったものと非常によく似ています。MessageとStateのパラメーターを取り、非同期呼び出しを処理するために使用されます。`handle_call/3`で可能なものと非常によく似た方法で、そこで何でも行います。一方、返信のないタプルのみが有効な戻り値です。
{noreply,NewState}
{noreply,NewState,Timeout}
{noreply,NewState,hibernate}
{stop,Reason,NewState}
handle_info
独自のサーバーがインターフェースに合わないメッセージを実際に処理しなかったことを述べたことを覚えていますか?`handle_info/2`はその解決策です。`handle_cast/2`と非常によく似ており、実際には同じタプルを返します。違いは、このコールバックは、`!`演算子で直接送信されたメッセージと、`init/1`の`timeout`、モニターの通知、`'EXIT'`シグナルなどの特別なメッセージに対してのみ存在することです。
terminate
コールバック`terminate/2`は、3つの`handle_Something`関数のいずれかが`{stop, Reason, NewState}`または`{stop, Reason, Reply, NewState}`という形式のタプルを返すたびに呼び出されます。2つのパラメーターReasonとStateを取り、`stop`タプルと同じ値に対応します。
`terminate/2`は、親(それを生成したプロセス)が死亡した場合にも呼び出されますが、`gen_server`が終了をトラップしている場合のみです。
注記: `terminate/2`が呼び出されたときに`normal`、`shutdown`、`{shutdown, Term}`以外の理由が使用された場合、OTPフレームワークはこれをエラーと見なし、あちこちで大量のログを開始します。
この関数は、ほぼ`init/1`の正反対であり、そこで行われたことはすべて`terminate/2`でその反対を行う必要があります。これはサーバーの管理人で、全員が退出したことを確認した後にドアを施錠する役割を担う関数です。もちろん、この関数はVM自体によって支援されており、通常はすべてのETSテーブルを削除し、すべてのポートを閉じます。この関数の戻り値は実際には重要ではありません。呼び出された後、コードの実行は停止するためです。
code_change
関数`code_change/3`は、コードをアップグレードできるようにするためにあります。`code_change(PreviousVersion, State, Extra)`という形式です。ここで、変数PreviousVersionは、アップグレードの場合にはバージョンターム自体(モジュールについてさらに詳しくをもう一度読んで、これが何であるかを忘れた場合)、またはダウングレードの場合には`{down, Version}`(古いコードの再ロード)です。 State変数は、サーバーの現在の状態全体を保持するため、変換できます。
すべてのデータを格納するためにorddictを使用していたと想像してください。しかし、時間が経つにつれて、orddictが遅くなりすぎて、通常のdictに変更することにしました。次の関数呼び出しでプロセスがクラッシュするのを避けるために、データ構造の変換をそこで安全に行うことができます。行う必要があるのは、`{ok, NewState}`で新しい状態を返すだけです。
Extra変数は、現時点では心配する必要はありません。主に大規模なOTP展開で使用され、VM全体でリリース全体をアップグレードするための特定のツールが存在します。まだその段階ではありません。
これで、すべてのコールバックが定義されました。少し迷っていても心配しないでください。OTPフレームワークは、フレームワークの一部Aを理解するには部分Bを理解する必要があるが、部分Bは部分Aが有用になるのを見る必要があるため、時々循環しています。その混乱を克服する最良の方法は、実際にgen_serverを実装することです。
.BEAMで私を拾って、スコッティ!
これは`kitty_gen_server`になります。`kitty_server2`とほぼ同じで、APIの変更は最小限です。最初に、次の行を含む新しいモジュールを開始します。
-module(kitty_gen_server). -behaviour(gen_server).
そして、コンパイルしてみてください。次のようなものになるはずです。
1> c(kitty_gen_server).
./kitty_gen_server.erl:2: Warning: undefined callback function code_change/3 (behaviour 'gen_server')
./kitty_gen_server.erl:2: Warning: undefined callback function handle_call/3 (behaviour 'gen_server')
./kitty_gen_server.erl:2: Warning: undefined callback function handle_cast/2 (behaviour 'gen_server')
./kitty_gen_server.erl:2: Warning: undefined callback function handle_info/2 (behaviour 'gen_server')
./kitty_gen_server.erl:2: Warning: undefined callback function init/1 (behaviour 'gen_server')
./kitty_gen_server.erl:2: Warning: undefined callback function terminate/2 (behaviour 'gen_server')
{ok,kitty_gen_server}
コンパイルは成功しましたが、コールバックがないことについての警告があります。これは`gen_server`ビヘイビアが原因です。ビヘイビアは、モジュールが別のモジュールに持つことを期待する関数を指定する方法です。ビヘイビアは、コードの一般的な部分とコードの特定の部分(あなた自身)の間の契約を締結する契約です。
注記: 'behavior'と'behaviour'の両方がErlangコンパイラで受け入れられます。
独自のビヘイビアを定義するのは非常に簡単です。次のように実装された`behaviour_info/1`という関数をエクスポートするだけです。
-module(my_behaviour).
-export([behaviour_info/1]).
%% init/1, some_fun/0 and other/3 are now expected callbacks
behaviour_info(callbacks) -> [{init,1}, {some_fun, 0}, {other, 3}];
behaviour_info(_) -> undefined.
ビヘイビアについては以上です。関数を忘れた場合にコンパイラ警告を得るために、それらを実装するモジュールで`-behaviour(my_behaviour).`を使用するだけです。とにかく、3番目のkittyサーバーに戻りましょう。
最初の関数は`start_link/0`でした。これは次のように変更できます。
start_link() -> gen_server:start_link(?MODULE, [], []).
最初の引数はコールバックモジュール、2番目の引数は`init/1`に渡すパラメータのリスト、3番目の引数は、現時点では説明しないデバッグオプションに関するものです。最初の位置に4番目のパラメータを追加できます。これは、サーバーを登録するための名前です。関数の以前のバージョンは単にpidを返しましたが、これは`{ok, Pid}`を返すことに注意してください。
次に関数を見てみましょう。
%% Synchronous call
order_cat(Pid, Name, Color, Description) ->
gen_server:call(Pid, {order, Name, Color, Description}).
%% This call is asynchronous
return_cat(Pid, Cat = #cat{}) ->
gen_server:cast(Pid, {return, Cat}).
%% Synchronous call
close_shop(Pid) ->
gen_server:call(Pid, terminate).
これらの呼び出しはすべて一対一の変更です。タイムアウトを与えるために、`gen_server:call/2-3`に3番目のパラメータを渡すことができることに注意してください。関数にタイムアウト(またはアトム`infinity`)を指定しない場合、デフォルトは5秒に設定されます。時間がなくなる前に返信がない場合、呼び出しはクラッシュします。
これで、gen_serverコールバックを追加できます。次の表は、呼び出しとコールバックの間の関係を示しています。
| gen_server | YourModule |
start/3-4
|
init/1
|
start_link/3-4
|
init/1
|
call/2-3
|
handle_call/3
|
cast/2
|
handle_cast/2
|
そして、特別なケースに関するもの、その他のコールバックがあります。
handle_info/2terminate/2code_change/3
既に持っているものをモデルに合わせるものから始めましょう。`init/1`、`handle_call/3`、`handle_cast/2`です。
%%% Server functions
init([]) -> {ok, []}. %% no treatment of info here!
handle_call({order, Name, Color, Description}, _From, Cats) ->
if Cats =:= [] ->
{reply, make_cat(Name, Color, Description), Cats};
Cats =/= [] ->
{reply, hd(Cats), tl(Cats)}
end;
handle_call(terminate, _From, Cats) ->
{stop, normal, ok, Cats}.
handle_cast({return, Cat = #cat{}}, Cats) ->
{noreply, [Cat|Cats]}.
繰り返しますが、ほとんど変更されていません。実際、よりスマートな抽象化のおかげで、コードは短くなっています。これで新しいコールバックに移ることができます。最初のものは`handle_info/2`です。これはおもちゃのモジュールであり、事前に定義されたログシステムがないため、予期しないメッセージを出力するだけで十分です。
handle_info(Msg, Cats) ->
io:format("Unexpected message: ~p~n",[Msg]),
{noreply, Cats}.
次のものは`terminate/2`コールバックです。これは、以前持っていた`terminate/1`プライベート関数と非常によく似ています。
terminate(normal, Cats) ->
[io:format("~p was set free.~n",[C#cat.name]) || C <- Cats],
ok.
そして最後のコールバック、`code_change/3`
code_change(_OldVsn, State, _Extra) ->
%% No change planned. The function is there for the behaviour,
%% but will not be used. Only a version on the next
{ok, State}.
make_cat/3 プライベート関数は残しておくのを忘れないでください。
%%% Private functions
make_cat(Name, Col, Desc) ->
#cat{name=Name, color=Col, description=Desc}.
これで、真新しいコードを試すことができます。
1> c(kitty_gen_server).
{ok,kitty_gen_server}
2> rr(kitty_gen_server).
[cat]
3> {ok, Pid} = kitty_gen_server:start_link().
{ok,<0.253.0>}
4> Pid ! <<"Test handle_info">>.
Unexpected message: <<"Test handle_info">>
<<"Test handle_info">>
5> Cat = kitty_gen_server:order_cat(Pid, "Cat Stevens", white, "not actually a cat").
#cat{name = "Cat Stevens",color = white,
description = "not actually a cat"}
6> kitty_gen_server:return_cat(Pid, Cat).
ok
7> kitty_gen_server:order_cat(Pid, "Kitten Mittens", black, "look at them little paws!").
#cat{name = "Cat Stevens",color = white,
description = "not actually a cat"}
8> kitty_gen_server:order_cat(Pid, "Kitten Mittens", black, "look at them little paws!").
#cat{name = "Kitten Mittens",color = black,
description = "look at them little paws!"}
9> kitty_gen_server:return_cat(Pid, Cat).
ok
10> kitty_gen_server:close_shop(Pid).
"Cat Stevens" was set free.
ok
おお、そしてなんと、動きました!
この汎用的なアドベンチャーについて、何が言えるでしょうか?おそらく以前と同じ汎用的なことでしょう。汎用的なものと具体的なものを分離することは、あらゆる点で素晴らしいアイデアです。保守が容易になり、複雑さが軽減され、コードはより安全になり、テストが容易になり、バグが発生しにくくなります。バグが発生した場合でも、修正が容易になります。汎用サーバーは、利用可能な多くの抽象化の1つに過ぎませんが、間違いなく最も広く使用されているものの1つです。これらの抽象化と動作については、次の章でさらに詳しく説明します。