7.14 第一級のモジュール

7.14.1. 簡単な例
7.14.2. 高度な例

(OCaml 3.12 〜)

typexpr ::= ...
| ( module package-type )
module-expr ::= ...
| ( val expr : package-type )
expr ::= ...
| ( module module-expr : package-type )
package-type ::= modtype-path
| modtype-path with package-type-constraint { and package-type-constraint }
package-type-constraint ::= type typeconstr-name = typexpr

一般にモジュールは静的な要素であると考えられています。この拡張では、モジュールを第一級の値として扱えるようにします。この値は動的にモジュールに戻すことができます。

( module module-expr : package-type ) は、モジュール式 module-expr の表すモジュール(ストラクチャないしはファンクタ)をカプセル化した値に変換します。この値の型は ( module package-type ) です。

モジュール式 ( val expr : package-type ) は逆に、式 expr を評価し(この値の型は module package-type でなければなりません)、その値にカプセル化されているモジュールを取り出します。

( module module-expr : package-type ) や、モジュール式 ( val expr : package-type )、 型式 ( module package-type ) に現れる package-type 構文クラスはモジュール型のうち、バラメータ型のない、名前のついたモジュール型だけを表します。型検査においては、パッケージ型はモジュール型の名前のパス等価性により比較され、型制約は通常の型の等価性で検査されます。

モジュール式 ( val expr : package-type ) をファンクタの本体部分で使うことはできません。これを適用可能なファンクタと組み合わせると型システムの健全性が損なわれるからです。ただし、 let M = ( val expr1 : package-type ) in expr2 のような、局所モジュール束縛の文脈では自由に使うことができます。

7.14.1 簡単な例

典型的な第一級モジュールの使用例は、あるシグネチャの複数の実装を実行時に選択することです。各々の実装は第一級モジュールにカプセル化できるストラクチャで、ハッシュテーブルのようなデータ構造に格納されます。

module type DEVICE = sig ... end
let devices : (string, module DEVICE) Hashtbl.t = Hashtbl.create 17

module SVG = struct ... end
let _ = Hashtbl.add devices "SVG" (module SVG : DEVICE)

module PDF = struct ... end
let _ = Hashtbl.add devices "PDF" (module PDF: DEVICE)
      

例えば、コマンドライン引数で実装を選択することができます。

module Device =
  (val (try Hashtbl.find devices (parse_cmdline())
        with Not_found -> eprintf "Unknown device %s\n"; exit 2)
   : DEVICE)
      

また、関数内で実装を選択することもできます。

let draw_using_device device_name picture =
  let module Device =
    (val (Hashtbl.find_devices device_name) : DEVICE)
  in
    Device.draw picture
      

7.14.2 高度な例

第一級のモジュールを使うと、ファンクタを使わずにコード断片をモジュール内でパラメータ化することができます。

let sort (type s) set l =
  let module Set = (val set : Set.S with type elt = s) in
  Set.elements (List.fold_right Set.add l Set.empty)
      

この関数に対して推論される型は (module Set.S with type elt = ’a) -> ’a list -> ’a list です。この関数を使うには Set.Make ファンクタをラップします。

let make_set (type s) cmp =
  let module S = Set.Make(struct
    type t = s
    let compare = cmp
  end) in
  (module S : Set.S with type elt = s)
      

この関数の型は (’a -> ’a -> int) -> (module Set.S with type elt = ’a) です。

第一級モジュールの別の高度な利用法として、存在型を表現するというものがあります。特に、これは一般化代数データ型(GADT)を模倣するのに使うことができます。これを示すために、まず型の等価性を表す型を定義します。

module TypEq : sig
  type ('a, 'b) t
  val apply: ('a, 'b) t -> 'a -> 'b
  val refl: ('a, 'a) t
  val sym: ('a, 'b) t -> ('b, 'a) t
end = struct
  type ('a, 'b) t = ('a -> 'b) * ('b -> 'a)
  let refl = (fun x -> x), (fun x -> x)
  let apply (f, _) x = f x
  let sym (f, g) = (g, f)
end
      

次に、型構成子から型パラメータの情報が得られるような代数データ型を定義します。

module rec Typ : sig
  module type PAIR = sig
    type t and t1 and t2
    val eq: (t, t1 * t2) TypEq.t
    val t1: t1 Typ.typ
    val t2: t2 Typ.typ
  end

  type 'a typ =
    | Int of ('a, int) TypEq.t
    | String of ('a, string) TypEq.t
    | Pair of (module PAIR with type t = 'a)
end = Typ
      

'a typ 型の値が型 'a の実行時の表現になるものとします。 IntString 構成子は簡単で、型パラメータ 'a と、基盤の型 int (または string)との等価性を与えるだけです。 Pair 構成子はもっと複雑です。型パラメータ 'at1 * t2 という形式の型の等価性に、 t1t2 の表現を加えたものを与えたいのですが、 t1t2 は与えられていません。このコードでは、第一級のモジュールを使って存在型を模倣しています。

'a typ 型の値は次のようにして構成します。

let int = Typ.Int TypEq.refl

let str = Typ.String TypEq.refl

let pair (type s1) (type s2) t1 t2 =
  let module P = struct
    type t = s1 * s2
    type t1 = s1
    type t2 = s2
    let eq = TypEq.refl
    let t1 = t1
    let t2 = t2
  end in
  let pair = (module P : Typ.PAIR with type t = s1 * s2) in
  Typ.Pair pair
      

最後に、例として、ある型 'a の実行時表現とその型の値を取り、その値を文字列として整形して返す多相関数を示します。

open Typ
let rec to_string: 'a. 'a Typ.typ -> 'a -> string =
  fun (type s) t x ->
    match t with
    | Int eq -> string_of_int (TypEq.apply eq x)
    | String eq -> Printf.sprintf "%S" (TypEq.apply eq x)
    | Pair p ->
        let module P = (val p : PAIR with type t = s) in
        let (x1, x2) = TypEq.apply P.eq x in
        Printf.sprintf "(%s,%s)" (to_string P.t1 x1) (to_string P.t2 x2)
      

多相再帰を使うために多相型アノテーションを使っていることに注意してください。