OCamlでも(オブジェクトもrefも使わずに)Open Recursionしたい!!・・・したい??
前回の記事をツイートしたら@dico_lequeさんからこういう話を教えていただいた:
多相バリアントでのやり方を考えるとファンクタ経由で非再帰で書いておいて
— でこれき (@dico_leque) March 9, 2021
module rec M = F(M)
みたいな、と思ってググったところ
Open Recursion with Modules | bcc32https://t.co/95eplP4K9k
みたいなのに行き当たったりした
(このブログの半分は@dico_lequeさんからのご指摘でできています・・・)
この記事ではref
を使うことなく、OCamlのファンクタと再帰モジュールを使ってOpen Recursionを実装している。
かなりちゃんとした実装で、単に再帰を分割するのではなく、モジュールをクラス、M.t
型をインスタンス、としっかりオブジェクト指向にも対応するような形になっている。
その分実装がちょっと複雑なので、自分の理解するためにも簡略化して追ってみたい。
というわけで前回と同じ非常に単純な再帰関数をわけるのにこの手法を使ってみる。
let rec f n = print_int n; if n > 0 then g (n-1) else () and g n = print_int n; if n > 0 then f (n-2) else ()
失敗例
とりあえず前回からの失敗例を再掲:
module A = struct let g _ = () let f n = print_int n; if n > 0 then g (n-1) else () end module B = struct include A let g n = print_int n; if n > 0 then f (n-2) else () end
B.f
はAモジュールで定義されたf
をinclude A
で追加している。なのでf
の定義で呼び出されているg
はB.f
ではなくA.f
になってしまう。
全体図
考え方としてはSelf
という引数をとるファンクタを作り、module rec X = F(X)
でそのSelf
が自分自身であるようなモジュールを作成する。
module type MT = sig val f : int -> unit val g : int -> unit end module A_ (Self : MT) : MT = struct let f n = print_int n; if n > 0 then Self.g (n-1) else () let g n = () end module rec A : MT = A_(A) module B_ (Self : MT) : MT = struct include A_(Self) let g n = print_int n; if n > 0 then Self.f (n-2) else () end module rec B : MT = B_(B)
意外と失敗例に比べて記述量が激増しているわけではない。(モジュール内で型を定義していて、モジュール間でその型の整合性を取ろうとすると色々と型制約を書く必要が増えてごちゃごちゃしてくる)
個別に詳細を追っていく。
モジュールのシグニチャ
まずは最終的に作成したいモジュールのシグニチャ(型)を定義する:
module type MT = sig val f : int -> unit val g : int -> unit end
このMT
(Module Typeの意)というシグニチャはファンクタの引数と再帰的モジュールの型を指定する為に必要になる。(ファンクタの戻り値は型で指定する必要はないのだが、書いておいた方がわかりやすい)
ベースクラス
ベースクラスにあたるA
モジュールを定義するのに、ファンクタと再帰的モジュールを使う:
module A_ (Self : MT) : MT = struct let f n = print_int n; if n > 0 then Self.g (n-1) else () let g n = () end module rec A : MT = A_(A)
まずはMT
型のモジュールを受け取りMT
型のモジュールを返すファンクタA_
を定義している。A_
内で定義されている関数が「再帰的に」同じモジュールで定義されている関数を呼び出したい時は、Self.f
、Self.g
のように引数として受け取ったMT
型のモジュールの関数を代わりに使う。
その上でmodule rec A : MT = A_(A)
で「自分自身がSelfなMT
型モジュール」をA_
を使って定義する。
子クラス
子クラスにあたるB
モジュールも形はほとんど同じ:
module B_ (Self : MT) : MT = struct include A_(Self) let g n = print_int n; if n > 0 then Self.f (n-2) else () end module rec B : MT = B_(B)
唯一違うのはB_
ファンクタのなかで、f
を定義する代わりにinclude A_(Self)
していること。なのでここでinclude
されるモジュールの中の定義で使われるSelf
はB_
ファンクタの引数のSelf
となる。
include
の時点ではf
もg
もA_
で定義されたものなのだが、そのすぐ後にg
を定義し直している。
そして再帰的モジュール定義でB_
を使って作成するモジュールB
をB_
に渡す。これでB_
の中のSelf
はB
、そしてinclude A_(Self)
の部分のおかげでA_
の中のSelf
もB
となる。なのでA_
のなかで定義されているf
がSelf.g
としてB.g
を使えるようになる。
使ってみる
A.f 10;; (* 10 が出力され結果はunit *) B.f 10;; (* 109764310 が出力され結果はunit *)
成功。ref
による破壊的変更に頼ることなく再帰的関数を二つのモジュールに分割できた。
追記:
@dico_lequeさんが多相バリアントを使ったopen recursionの解説を書かれていた:
多相バリアントを使う&最終的に作成するeval
を(再帰的に)引数として渡すことにすれば、「バリアントのパターンごとの関数を個別に定義する」という分割が可能。なるほど・・・