OCamlのFunctorをクラスっぽく使ってみた
OCamlのFunctorは一般的なOOPにおけるClassに似ているところと似ていないところがあるぞい。
最近1000行ほどのOCamlコードを一から書く機会があって、その時いろいろとFunctorを使ってみたのだが、気付いたらOOPっぽいことをしていて「面白いなー」「本当にこれでいいのかなー」と思ったのでメモ。
勉強がてら分散システムのプロトタイプをLwtを使って書いていたのだが
let my_addr = Sys.argv(1) let other_addrs = List.filter ((!=) my_addr) [16000; 16001; 16002] let other_sockets = ref [] ... let f = ... let server () = Lwt_io.establish_server_with_client_socket my_addr f
こんなコードを書いて複数のプロセスを立ち上げてソケットを使って通信させていたのだが、テストのために同一プロセスで複数のインスタンスを走らせたくなった。
綺麗に副作用を分離させて純粋関数型で書いていれば、モジュールのデータを閉じ込めるレコード型を定義しておしまいだったのだろうけれど、上記のコードからもわかるとおり他のサーバへのコネクションをlist ref
で保持していたり、自分のポートに対する接続を処理するf
関数でもモジュールのデータに対する更新を行なっていたり、と「インスタンスはただのレコードで、単一のモジュールですべてのインスタンスを扱う」という形に持っていくのは相当大変そうだった。
どうリファクタするのがいいかと少し悩んでいたのだが、Functorを使えば解決では?と思って試してみた。
こんな風に既存のモジュールをFunctorとして括って
module Make(C : sig val n : int end) = struct let my_addr = C.n let other_addrs = List.filter ((!=) my_addr) [16000; 16001; 16002] let other_sockets = ref [] ... let f = ... let server () = Lwt_io.establish_server_with_client_socket my_addr f end
あとは使う時に別のモジュールとして複数インスタンス化する:
module M0 = Mymodule.Make(struct let n = 16000 end) module M1 = Mymodule.Make(struct let n = 16001 end) module M2 = Mymodule.Make(struct let n = 16002 end) ... async (fun () -> M0.server ()) async (fun () -> M1.server ()) Lwt_main.run @@ M2.server ()
これは結構うまくいったので、今後も使っていきたいと思う。
今まではモジュールをなるべく
- 純粋なデータ型を定義
- そのデータ型に対する処理を関数として提供
という形で書いていて、Functorもその流れでポリモーフィズムを追加するために使えるという認識だった。
今回の使い方は
- モジュールに内部データを持たせる
- その内部データに対する処理もモジュール内で定義
- それをFunctor化することで複数インスタンス化できるようにする
- さらにFunctorに渡すモジュールを使ってインスタンスの初期データを設定できるようにする
というもの。
使ってみるとこれってほとんどクラスだよな?と気付いた。
まあクラスと違ってモジュールはそのままだと普通の値としては使えず、例えば任意の個数をインスタンス化してどうこうするというのはfirst class moduleを使う必要があってちょっと構文上ごちゃつくかもしれないが・・・
ちなみに実装の継承がしたい場合は:
module Make(C : sig val n : int end) = struct include Mymodule.Make(C) ... end
とすればいい。
モジュールが状態を保持し純粋性が失われていることになるので不必要に使いすぎないほうがいい手法かもしれないが、こういう使い方もあるのかとMLのモジュール機構の柔軟さと多用途性を改めて認識する機会となった。