1. はじめに

こんにちは、びしょ〜じょです。 これは ML Advent Calendar 2020 の22日目の記事です。 皆さんは大晦日ですか? 私は22日にいます。

さて話は OCaml 4.08 まで戻るのですが、みなさんはopenの次に module path のみならず module expression を書けるようになっていたのをご存知でしたか? 本日はそういった話をした論文[1]の話をします。

モジュールのフィールドを現在のモジュールのスコープに展開する方法としてincludeopenがある。 includeはモジュールの中身を文字どおり include 、つまり内容が継ぎ足されます。 openはスコープ上で見えるようにするだけでモジュールの定義に展開されたメンバーは含まれません。

さて、includeにはいろいろ書けるのですが、

include (struct
    type t = int
    let x = 3
  end : sig
    type t
    val x : t
  end)
(** val x : t = <abstr> *)

(**
 *  t は opaque なので int としては扱えない
 *  # x + 3;;
 *  Error: This expression has type t but an expression was expected of type int
 *)

openはなんか不自由だったんですよねぇ

(** 
 *   # open struct end;;
 *   Error: Syntax error
 *)

Q. なんで?

A.

This distinction is less useful: there is no fundamental reason why include should accept arbitrary module expressions, while open should not.

2. extending open in structures

さて OCaml 4.08 以降は豊かなopenが使えます。やりましたね。 でもなにができるんですか?

2-1. Unexported top-level values

signature や mli ファイルを用意しないと definitions は全てそれらが属するモジュールのフィールドになります。

hoge.ml というファイルがあるとする
type t = A | B
module Utils = struct
  let default = 5
end

(* 適当 *)
let add x = function
  | Some y -> x + y
  | None   -> x + Utils.default

Utils モジュールは門外不出にしたいんで mli を書く

hoge.mli
type t = A | B
val add : int -> int option -> int

またはinclude (M : Sig)

include (struct
  type t = A | B
  module Utils = struct
    let default = 5
  end

  let add x = ......
end : sig
  type t = A | B
  val add : int -> int option -> int
end)

えっtの定義2回書くんですか? ダル… Utilsだけ外したいのにそれ以外全部について記述するのでなんかイヤーて感じですね。

というときに open struct を使ってみましょう。

hoge.ml
type t = A | B
open struct
  module Utils = struct
    ......
  end

  (* なんでもいい *)
  type tsukawan_t = int
  let tsukawan_other = 100
  module type TsukawanSig = sig type nonrec t = t end
end

let add x = function
  | Some y -> x + y
  (* 使う側はそのまま *)
  | None   -> x + Utils.default

open struct ... endでモジュール外に出したくないものを定義できます。 なんとなく mli を書きたくない勢にとっては大変嬉しい。 この便利な使い方を知らずに OCaml 4.08 以降を使っていたので皆さんにもお伝えしたかった。

2-2. その他

詳細は論文を読んでくれい。 recursive な型の名前の shadowing をしたり(2.2)

type t = A
module M = struct
  open struct type t' = t end
  type t = B of t' * t | C
end

複数の関数にまたがる変数を簡単に用意したり(2.3)

(**
 *  let f, g =
 *    let aux x y = ... in
 *    (fun p -> aux p true),
 *    (fun p -> aux p false)
 *)

include struct
  (* この無名モジュール外には aux は出ていかない *)
  open struct let aux x y = ...  end
  let f p = aux p true
  let g p = aux p false
end

ローカルな例外を簡単に定義したり(2.4)、 共有ステートで状態だけ隠してオペレータを提供するのを簡単に定義したり(2.5)、 ppx などで自動生成される変数をうまく隠すだとか(2.6)、 open (M : S)も書けるようになったんでopenされるものを絞ったり(2.7)など、 人生に大切なことが書かれています。

3. extending open in signatures

ところで signature 内でもopenは使える。 signature は深掘ってこなかったので、知らないことが書いてあって面白かった。

The OCaml compiler has a feature that is often useful during development: passing the -i flag when compiling a module causes OCaml to display the inferred signature of the module. However, users are sometimes surprised to find that a signature generated by OCaml is subsequently rejected by OCaml, because it is incompatible with the original module, or even because it is invalid when considered in isolation.

マジかい!w という感じですが例を見るとまぁしゃーなしやなぁという気にもなります。

fuga.ml
type t = T1
module M = struct
  type t = T2
  let f T1 = T2
end

(**
 *  the signature generated by the compiler with -i flag
 *
 *  type t = T1
 *  module M : sig
 *    type t = T2
 *    val f : t -> t  (* !?w *)
 *  end
 *)

ファイルの成すモジュールは recursive に定義できないので、Fuga.tFuga.M内で参照できないわけですね。 そしてM.tが型引数を持つと興味深い結果になります。

type t = T
module M = struct
  type 'a t = S
  let f T = S
end

(**
 *  type t = T
 *  module M : sig
 *    type 'a t = S
 *    val f : t -> t (* え?! *)
 *  end
 *)

型変数が虚空に消えました。これはすごい。 そしてこの問題の本質は OCaml コンパイラが頑張ってくれないことではなく、人間も手で書けないということですね。 余計なフィールドを追加せずに書けますか? ちょいと無理そうやな〜というところで open struct しますと

type t = T1
open struct type t' = t end
module M : sig
  type t = T2
  val f : t' -> t
end

ヨシ! ついでに OCaml 4.11 で試したところ生成するほうもいい感じになってました。

type t = T1
module M : sig
  type t = T2
  val f : t/2 -> t/1
end

4. おわりに

面白かったんで4章の restriction と6章の alteranative designs(Standard ML のlocal、 OOP みたいなprivate、destructive substitution2を拡張するやつ)も読んでみてください。5章の implementation sketch は遠目で眺めました…。


よし! 書き終わったので大晦日に私も追いつきました。 みなさん良いお年を。


  1. Li, Runhang and Yallop, Jeremy. "Extending OCaml’s “open”". arXiv. 2019. 

  2. with type ですね。これも用語を知らなかった。