OCamlでカウンターモジュール
副作用はなるべくないほうがよい。
それはそうなんですが、副作用を使ったほうが効率がよかったり、コードの見通しが良くなったりすることもよくある話です。
例えば、以下のような単純な表示関数printを考えます。
let print s = Format.printf "%s@." s |
当然ながら、呼び出すと以下のように引数の文字列をそのまま表示します。
# print "a";; a - : unit = () # print "b";; b - : unit = () # print "c";; c |
さて、この関数に、「毎回一意な数字ラベルを表示する」という機能を追加してみましょう。
要するに、以下のような出力が欲しいわけです。
# print "a";; 1:a - : unit = () # print "b";; 2:b - : unit = () # print "c";; 3:c |
この場合、副作用なしにするためには、引数をひとつ増やしてやる必要があります。
let print i s = Format.printf "%d:%s@." i s |
で、これで万事解決なら楽なんですが、実際にはこれだけだと「次にラベルがいくつか」という情報を伝えられなくなってしまうので、ラベルの情報を返す必要があります。
let print i s = Format.printf "%d:%s@." i s; i + 1 |
使うときには毎回戻り値を受け取る必要がありますね。
なので、以下のようにカウンタ変数を用意し、毎回戻り値で更新(というか定義を上書き)していく必要があります。
# let i = ;; val i : int = # let i = print i "a";; :a val i : int = # let i = print i "b";; :b val i : int = # let i = print i "c";; :c val i : int = |
なるほど、確かに参照透過性とか考えると実に関数型らしい気がしますが、とても面倒ですね。
これくらいならまだしも、もし返り値を受け取る必要のある関数でこのprintを呼び出す場合、戻り値もタプルにしてやる必要が出てきます。
関数がネストしていたりするとそれら全部に引数を追加して、関数の戻り値もタプルで受け取るようにして、もちろん戻り値を直接別の引数に渡しているような場合はletで分割しないといけません。
余りにも面倒ですね……
というわけで、関数型としては問題ありでしょうが、使っている数値をカウントするモジュールを作ってみましょう。
refを使えば簡単ですね。
module Counter = struct let t = ref let reset () = t := let get () = t := !t + 1; !t - 1 end |
get ()を呼べば、毎回新しい数字を得ることができます。
これを使うと、printは以下のような感じになりますね。
let print s = Format.printf "%d:%s@." (Counter.get ()) s |
この場合、呼び出し側ではカウンタを意識することなく、文字列だけを引数に呼び出すことができます。
# print "a";; :a - : unit = () # print "b";; 1:b - : unit = () # print "c";; 2:c - : unit = () |
「さすがにプログラム全体通してグローバルなのはちょっと……」という場合もありますよね。
モジュールの中でだけ、一意な整数が欲しい場合とか。
例えば、さっきのprintをPrinterというモジュールにくるんで、この中でだけ使えるカウンタモジュールを定義するとしましょう。
module Printer = struct module C = Counter let print s = Format.printf "%d:%s@." (C.get ()) s end |
こんな感じでしょうか?
……が、これだとCはCounterと物理等価になってしまいます。
要は、エイリアスを定義しているだけになっているわけです。
以下のようにすると、外部のCounterを使っていることがよくわかります。
# Printer.print "a";; :a - : unit = () # Counter.get ();; - : int = 1 # Printer.print "b";; 2:b - : unit = () |
これではモジュールの中で閉じたカウンターになりませんね。ダメダメです。
このような場合は、毎回違ったモジュールの実体を生成する必要があるので、ファンクタを使えばよさそうです。
ただし、別に型情報が必要なわけではないので、ファンクタ引数にはダミーのモジュールとシグネチャを与えておきます。
module type DummyType = sig end module Dummy = struct end module Counter = struct let t = ref let reset () = t := let get () = t := !t + 1; !t - 1 module Make(D:DummyType) = struct let t = ref let reset () = t := let get () = t := !t + 1; !t - 1 end end |
こんな感じですね。
このCounter.Makeを使うと、Printer.printは以下のようになります。
module Printer = struct module C = Counter.Make(Dummy) let print s = Format.printf "%d:%s@." (C.get ()) s end |
先ほどと同じように実験してみましょう。
# Printer.print "a";; :a - : unit = () # Counter.get ();; - : int = # Printer.print "b";; 1:b - : unit = () # Counter.reset ();; - : unit = () # Printer.print "c";; 2:c - : unit = () |
外部のCounterとPrinter内部のCが完全に独立していますね。
ちなみに、グローバル用とファンクタ用で2回、同じ内容を書いています。
ちょっと面倒ですし、保守性も悪いですね。
だからといって以下のようにすると、参照しているtが物理等価になってしまいファンクタにする意味がなくなってしまいます。
(* これは無意味 *) module Counter = struct module Core = struct let t = ref let reset () = t := let get () = t := !t + 1; !t - 1 end include Core module Make(D:DummyType) = struct include Core end end |
この場合、先にファンクタを定義しておいて、ファンクタで作ったモジュールをincludeすれば、物理等価でない定義になります。
ま、ファンクタで作成してるんだから当然ですが。
module Counter = struct module Make(D:DummyType) = struct let t = ref let reset () = t := let get () = t := !t + 1; !t - 1 end include Make(Dummy) end |
「モジュールカウンタ便利だね! 関数に渡したりレコードに含めたりできたらもっと便利だよね!」という局面がきっと出てくるはずです。
OCaml3.12からサポートされた第一級のモジュールを使えばそんな夢も叶えることができます。
(Counter.Sにファンクターの返り値シグネチャを書いておく必要がありますが)
ところで、モジュールを変数に変換するたびに(module Counter:Counter.S)って書くのはちょっと面倒ですよね。
どうせなら、モジュールが自身の第一級モジュールを表す変数を持っていたら便利そうです。
ちょっと回りくどいですが、以下のようにすればpackが自身の第一級モジュールを表現する変数になります。
module Counter = struct module type S = sig val t : int ref val reset : unit -> unit val get : unit -> int end module Make(D:DummyType) = struct module Core = struct let t = ref let reset () = t := let get () = t := !t + 1; !t - 1 end include Core let pack = (module Core:S) end include Make(Dummy) end |
というわけで、カウンタモジュールの定義を通して、参照を持ったモジュールを複製する方法や、自身の第一級モジュールを表す変数を持たせる方法などを思いついたのでメモがてらに記事にしてみました。
まぁ、正直ここまでするならクラスの方がいいんじゃないか、と思わなくもないですが、きっとモジュールで実現したい人もいますよね。
逆転の発想で、こんな感じで参照をもたせたモジュールを作っていけば、モジュールでOOPしてしまうということも出来そうな気がします。それで何が嬉しいのか、と問われると難しいところですが。
最後に、ユーティリティを幾つか足したバージョンのCounterを、以下に置いておきます。
前回の標準ライブラリ拡張と同じく、興味のある方は煮るなり焼くなりご自由にお使い下さい。
ちなみに、個人的な好みにより自身の第一級モジュールを表すものが変数packではなく関数pack ()になっています。
相変わらずあまりチェックしていないので、バグなどありましたらご一報いただけると助かります。
(* Util: List Utility *) let rec (--) x y = if x > y then [] else x::((--) (x+1) y) (* Util: Dummy Module and Signature *) module type DummyType = sig end module Dummy = struct end (* Util: Counter *) module Counter = struct module type S = sig val reset_time : int ref val t : int ref val init : int -> unit val set : int -> unit val reset : unit -> unit val get : unit -> int val next : unit -> int val next_ : unit -> unit val check : unit -> int val used_list : unit -> int list end module Make(D:DummyType) = struct module Core = struct let reset_time = ref let t = ref let init i = reset_time := i; t := i let set i = t := i let reset () = t := !reset_time let get () = t := !t + 1; !t - 1 let next () = t := !t + 1; !t let next_ () = t := !t + 1 let check () = !t let used_list () = !reset_time -- (!t - 1) end include Core let pack () = (module Core:S) end include Make(Dummy) end |