この記事はML Advent Calendar 2020 6日目の記事です。
OCamlの定義にはデフォルトで再帰的なものと非再帰的なものがある。
型やクラスの定義はデフォルトが再帰で、値の定義やモジュール、モジュール型の定義は非再帰だ。
型の定義で再帰をしたくない場合は type nonrec
と書き(OCaml 4.02.2以降)、値の定義で再帰したい場合は let rec
と書く。モジュールは module rec
と書く。ではモジュール型の場合は?
そもそもそんなもの必要なのかという気もするが、第一級モジュールでオブジェクトみたいなことをしはじめると出てこなくもない。
たとえば、非破壊的な、2次元平面上の点を考える。
レコードで書くとこんな感じだろう。
type t = { x : int; y : int }
let move ?(dx = 0) ?(dy = 0) {x; y} =
{ x = x + dx; y = y + dy }
クラスにするとこう。
class point = fun ~x ~y ->
object (self)
method x = x
method y = y
method move ?(dx = 0) ?(dy = 0) () =
new point ~x:(self#x + dx) ~y:(self#y + dy)
end
(*
class point :
x:int ->
y:int ->
object
method move : ?dx:int -> ?dy:int -> unit -> point
method x : int
method y : int
end
*)
move
メソッドの戻り値型は自分自身なので、再帰が必要になってしまった。
第一級モジュールで書こうとすると、 module type rec
のようなものはないのでどうしたものかというところだけど、実は再帰モジュールを使うとどうにかなる。
module rec Point : sig
module type S = sig
val x : int
val y : int
val move : ?dx:int -> ?dy:int -> unit -> (module Point.S)
end
end = Point
let rec make_point ~x ~y : (module Point.S) =
(module struct
let x = x
let y = y
let move ?(dx = 0) ?(dy = 0) () =
make_point ~x:(x + dx) ~y:(y + dy)
end)
モジュール型自体では再帰せず、外側のモジュールを再帰させる。やりたいことはシグネチャレベルで完結しているので、モジュール定義の右辺では単に自分自身を参照する。
これで問題(?)は解決したのだけれど、OCamlのissuesを眺めると再帰モジュール関連のものが結構あり、使っていて大丈夫なんだっけという気持ちがよぎる。module type recを直接サポートしようというissueもあるがはたして。
補足
クラスをつかった定義では move
の戻り値を point
にしたが、後々の拡張を考えると(するかどうかは別として)、 self type を使っておいた方がよい。こうしておくと、サブクラスでも move
の定義がそのまま使える。
class point = fun ~x ~y ->
object (_ : 'self)
val x = x
val y = y
method x = x
method y = y
method move ?(dx = 0) ?(dy = 0) () : 'self =
{< x = x + dx; y = y + dy >}
end
(*
class point :
x:int ->
y:int ->
object ('a)
val x : int
val y : int
method move : ?dx:int -> ?dy:int -> unit -> 'a
method x : int
method y : int
end
*)