OCamlにはファーストクラスモジュール(第一級モジュール)という言語機能があり、関数の引数にモジュールを渡したりモジュールを戻り値にすることができる。要するにモジュールを値のように扱える。
典型的な使い方は、関数の引数にモジュール型を指定して、そのシグネチャに合致したモジュールを渡せるようにする。関数内でモジュールの実装を呼び出せるのでStrategy PatternやDIとして使えるだろう。公式マニュアルでも select at run-time among several implementations と言っているのでそういう使い方を想定しているはず。
(* インターフェースを定義 *)
module type DigestStrategy = sig
val to_hex : string -> string
end
(* MD5のハッシュ値を返す *)
module DigestMD5 = struct
let to_hex s = Digestif.MD5.to_hex (Digestif.MD5.digest_string s)
end
(* SHA1のハッシュ値を返す *)
module DigestSHA1 = struct
let to_hex s = Digestif.SHA1.to_hex (Digestif.SHA1.digest_string s)
end
(* 第一引数でモジュールを受け取る関数 *)
let print_hex_digest (module S : DigestStrategy) s = S.to_hex s |> print_string
let () =
print_hex_digest (module DigestMD5) "test"; (* 098f6bcd4621d373cade4e832627b4f6 *)
print_hex_digest (module DigestSHA1) "test"; (* a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 *)
また、以下のように型を抽象化することで、引数や戻り値の型も変えることができる。これはScalaのDependent function typesに近く、なかなかpowerfulだなぁと感じる。
module type Monoid = sig
type t
val empty : t
val append : t -> t -> t
end
module IntMonoid = struct
type t = int
let empty = 0
let append = ( + )
end
module StrMonoid = struct
type t = string
let empty = ""
let append = ( ^ )
end
let add (type s) (module M : Monoid with type t = s) a b = M.append a b
add (module IntMonoid) 10 10
(* - : int = 20 *)
add (module StrMonoid) "foo" "bar"
(* - : string = "foobar" *)
再帰関数で使う場合、型注釈が必要になるらしい。正直、ここまでやるとシンタックス的に可読性にやや難があるような気もする(極めるとそうでもないのかもしれない)素直に別メソッドとして実装するかファンクターを作るくらいで良いんじゃないかなって思わなくもない。
let rec sum : type s. (module Monoid with type t = s) -> s list -> s =
fun (module M : Monoid with type t = s) xs ->
match xs with
| [] -> M.empty
| x :: rest -> M.append x (sum (module M) rest)
sum (module IntMonoid) [ 1; 2; 3; 4 ]
(* - : int = 10 *)
sum (module StrMonoid) [ "foo"; "bar" ]
(* - : string = "foobar" *)