OCamlで説明するgen_serverの使い方
Erlang/OTPを勉強し始めたので、自分が理解するためにOCamlの型でgen_serverを理解したい。間違いの指摘など大歓迎。
gen_serverを完璧に型付けすることが目的ではない。なんとなく理解できれば良いつもり。
想定する読者
- OCaml使い
- Erlang/OTPの勉強中の人
- (optional) すごいE本の14章を読んだ
バージョンについて
- Erlang/OTP 19
- OCaml 4.04.1
ビヘイビア
behaviourはOCamlでいうところのファンクタだと思う。
想定するcallback群をSeedという名前のモジュールタイプで表現し、Seedを前提として提供される関数群をMakeという名前のファンクタで表す。
つまり、gen_serverビヘイビアみたいなのをOCamlのgen_server.mlで提供するならば次のようなインターフェイス(mli)となる。
(**
* OCamlの型でErlang/OTP gen_server のインターフェイスを説明したい
* gen_serverを使う人が用意すべき関数群(Erlangではcallbackと呼ぶ)
*)
module type Seed = sig
...
end
(* 上記のSeedが与えられると、gen_serverは以下の関数群を提供する *)
module Make(S : Seed) : sig
...
end
gen_serverをOCamlで実装したいわけではないので、以下ではgen_server.mliを考えることにする。
init
gen_serverによって生成されるプロセスが内部で保持する状態の型stateと、プロセス起動時の初期状態を決める方法initをSeedで決める。
(**
* OCamlの型でErlang/OTP gen_server のインターフェイスを説明したい
* とりあえず、initのみ
* gen_serverを使う人が用意すべき関数群(Erlangではcallbackと呼ぶ)
*)
module type Seed = sig
type state (* プロセスが内部で保持する状態の型、外からは見えない *)
type args (* プロセス起動時に必要とするパラメータの型 *)
val init : args -> (state, exn) result (* プロセス開始時の内部状態を構成する *)
...
end
(* 上記のSeedが与えられると、gen_serverは以下の関数群を提供する *)
module Make(S : Seed) : sig
val start : S.args -> start_option list -> (pid, exn) result
val start_link : S.args -> start_option list -> (pid, exn) result
...
end
すごいE本 kitty_serverでの例
module KittySeed : Gen_server.Seed = struct
type state = cat list
type args = unit
let init () = Ok []
...
end
call系の同期的なAPI
(**
* OCamlの型でErlang/OTP gen_server のインターフェイスを説明したい
* とりあえず、同期的なやりとりのためのcall系のみ
* gen_serverを使う人が用意すべき関数群(Erlangではcallbackと呼ぶ)
*)
module type Seed = sig
type state (* プロセスが内部で保持する状態の型、外からは見えない *)
...
type call_request (* プロセスが提供することになる同期APIの入力 *)
type call_response (* プロセスが提供することになる同期APIの出力 *)
type handle_call_result = (*hibernateとtimeoutについては省略*)
| Reply of call_response * state
| NoReply of state
| Stop of reason * call_response option * state
val handle_call : call_request -> (pid*tag) -> state -> handle_call_result
end
(* 上記のSeedが与えられると、gen_serverは以下の関数群を提供する *)
module Make(S : Seed) : sig
...
val call : pid -> S.call_request -> timeout option -> S.call_response
...
end
- handle_callでNoReplyを返すときはreply関数を直接どこかで呼んでいないといけない (;゚ ロ゚ )
- なるべくReplyを返すようにするのがお行儀が良いのかもしれない
- handle_call_resultにおける hibernateやtimeoutの更新に関してはあまり本質じゃない気がしたので省略した
すごいE本 kitty_serverでの例
module KittySeed : Gen_server.Seed = struct
type state = cat list
...
type call_request = (* kitty_serverが提供する同期的なAPIは次の二種類 *)
| OrderCat of cat
| Terminate
type call_response =
| OrderOk of cat
| TerminateOk of cat list
...
end
本当は call_response
型はcall_requestに依存して変わるのでGADTなど使った方がより強い型付けができるかもしれないが、ここではなんとなく理解する目的なので深追いはしない。
cast系の非同期的なAPI
だいたいcall系とおんなじノリ。requestやresultはcast系と別の型にできるっていうのがポイントなのかも。
その他のcallback関数について
省略