open recursionなモジュールに型定義を持たせる(中)
今回は今まで散々参考にしてきたこの記事の内容に近い形でopen recursionを実装してみる:
題材もまったく同一なのでパクリと言われても仕方ない・・・
前回までの記事の内容を踏まえて少し整理したり、スタイルをいじったりしてあるくらい。もう一歩進めた(より継承チックな)実装も出来たが長くなるので次の記事に持ち越し。
まずはコード全体:
module type GT = sig type t type ctor val create : ctor -> t val addressee : t -> string val greet : t -> string end module GreetF(Self : GT) : GT with type t = Self.t and type ctor = Self.ctor = struct include Self let addressee _t = "world" let greet t = "Hello " ^ Self.addressee t end module rec Greet : GT with type t = unit and type ctor = unit = GreetF(struct include Greet let create () = () end) module NamedF(Self : GT) : GT with type t = Self.t and type ctor = Self.ctor = struct include GreetF(Self) let addressee = Self.addressee end type named = { name : string } module rec Named : GT with type t = named and type ctor = string = NamedF(struct include Named let create name = { name=name } let addressee named = named.name end)
個別に詳細をみていく。
シグニチャ
module type GT = sig type t type ctor val create : ctor -> t val addressee : t -> string val greet : t -> string end
今回のコードはOCamlのモジュールらしく、モジュールの中心となるイミュータブルなデータt
型、そのt
型のコンストラクタとt
型に対して使える関数群を定義するようになる。
そこがシグニチャにもしっかり表れている。create
で何らかのctor
型の引数からt
型のデータを作り、addressee
やgreet
という関数をそのデータに適用して文字列を得る。
ベースクラス
まずはファンクタ:
module GreetF(Self : GT) : GT with type t = Self.t and type ctor = Self.ctor = struct include Self let addressee _t = "world" let greet t = "Hello " ^ Self.addressee t end
これは前回の記事のファンクタとかなり近い。Self
というモジュールを引数にGT
型のモジュール(ただしt
やctor
が何の型か外部に見えるようにしてある)を返す。
モジュール内の定義に関しては、type t
、type ctor
、let create
などといろいろSelf
の中身が多かったのでいちいちtype t = Self.t
などとせず、include Self
で一括でコピーしている。addressee
やgreet
は外部では定義されないのでファンクタ内でちゃんと定義する。
次に再帰モジュール:
module rec Greet : GT with type t = unit and type ctor = unit = GreetF(struct include Greet let create () = () end)
前回と同じく、t
とctor
の指定はGreet
のシグニチャ部分だけで十分でモジュール本体では必要ない(再帰モジュールのおかげでinclude Greet
で入ってくる。なんか不思議な気もするが・・・)
前回はmodule rec Greet : ... = GreetF(Greet)
という形で完結していたが、今回はcreate
関数を定義する必要があるのでGreetF(struct include Greet let create () = () end
と無名モジュールを作ってその中でinclude
とcreate
定義をしている。
これで
utop # let g = Greet.create ();; val g : unit = () utop # Greet.greet g;; - : string = "Hello world"
のようにできる。
子クラス
流石にこれではつまらないので、せめてt
型に固有の名前を持たせてgreet
で"Hello <名前>"と返すようにしたい、のでファンクタNamedF
と再帰モジュールNamed
を作ってみる。
ファンクタ:
module NamedF(Self : GT) : GT with type t = Self.t and type ctor = Self.ctor = struct include GreetF(Self) let addressee = Self.addressee end
モジュール内の2行で
GreetF
からの継承addressee
関数だけoverride
ということを指定している。addressee
の中身は外部から注入する。(ここは元ネタの記事準拠だが、個人的には少し不満があるところでaddressee
の実装がファンクタにないとNamedF
を継承できなくなってしまう。次回の記事ではこのポイントを修正する)
type named = { name : string }
Named.t
として一つの文字列型フィールドname
を持つレコードを定義。(元記事だとここにsuper
というフィールドも付いていたのだが、このフィールドまったく使われていなかった上に原理的に使えなさそうだったので今回は省略。次回はこの点についても探っていく)
再帰モジュール:
module rec Named : GT with type t = named and type ctor = string = NamedF(struct include Named let create name = { name=name } let addressee named = named.name end)
t
型はnamed
レコード、ctor
は文字列型とシグニチャで定義、create
とaddressee
を(t
がnamed
レコード型であるという情報を使って)定義している。
前述の通りNamedF
からはt
型の形が見えないのでaddressee
の定義の中でフィールドアクセスが使えないのでNamed
で定義してNamedF
に注入する形になっている。
使ってみる
utop # let g = Greet.create ();; val g : unit = () utop # Greet.greet g;; - : string = "Hello world" utop # let n = Named.create "Bob";; val n : named = {name = "Bob"} utop # Named.greet n;; - : string = "Hello Bob"
成功。
次回
今回のコードの問題点2つはどちらも「ベースクラスのt
型の情報が子クラスから使えなさそう」ということに繋がる。
次回はその解決法を探るため、「ベースクラスでは挨拶の文字列、子クラスではそれに追加で相手の名前がt
型に格納されている」というようなコードを書いてみる。