Extending OCaml's open
1. はじめに
こんにちは、びしょ〜じょです。 これは ML Advent Calendar 2020 の22日目の記事です。 皆さんは大晦日ですか? 私は22日にいます。
さて話は OCaml 4.08 まで戻るのですが、みなさんはopen
の次に module path のみならず module expression を書けるようになっていたのをご存知でしたか?
本日はそういった話をした論文[1]の話をします。
モジュールのフィールドを現在のモジュールのスコープに展開する方法としてinclude
とopen
がある。
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, whileopen
should not.
2. extending open
in structures
さて OCaml 4.08 以降は豊かなopen
が使えます。やりましたね。
でもなにができるんですか?
2-1. Unexported top-level values
signature や mli ファイルを用意しないと definitions は全てそれらが属するモジュールのフィールドになります。
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 を書く
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
を使ってみましょう。
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 という感じですが例を見るとまぁしゃーなしやなぁという気にもなります。
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.t
をFuga.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 は遠目で眺めました…。
よし! 書き終わったので大晦日に私も追いつきました。 みなさん良いお年を。