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" *)

参考文献