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 = 0;;
val i : int = 0
# let i = print i "a";;
0:a
val i : int = 0
# let i = print i "b";;
0:b
val i : int = 0
# let i = print i "c";;
0:c
val i : int = 0

なるほど、確かに参照透過性とか考えると実に関数型らしい気がしますが、とても面倒ですね。
これくらいならまだしも、もし返り値を受け取る必要のある関数でこのprintを呼び出す場合、戻り値もタプルにしてやる必要が出てきます。
関数がネストしていたりするとそれら全部に引数を追加して、関数の戻り値もタプルで受け取るようにして、もちろん戻り値を直接別の引数に渡しているような場合はletで分割しないといけません。
余りにも面倒ですね……

というわけで、関数型としては問題ありでしょうが、使っている数値をカウントするモジュールを作ってみましょう。
refを使えば簡単ですね。

module Counter =
struct
  let t = ref 0
  let reset () =
    t := 0
  let get () =
    t := !t + 1;
    !t - 1
end

get ()を呼べば、毎回新しい数字を得ることができます。
これを使うと、printは以下のような感じになりますね。

let print s =
  Format.printf "%d:%s@." (Counter.get ()) s

この場合、呼び出し側ではカウンタを意識することなく、文字列だけを引数に呼び出すことができます。

# print "a";;
0: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";;
0: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 0
  let reset () =
    t := 0
  let get () =
    t := !t + 1;
    !t - 1
  module Make(D:DummyType) =
  struct
    let t = ref 0
    let reset () =
      t := 0
    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";;
0:a
- : unit = ()
# Counter.get ();;
- : int = 0
# 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 0
    let reset () =
      t := 0
    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 0
    let reset () =
      t := 0
    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 0
      let reset () =
	t := 0
      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 0
      let t = ref 0
      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

2 Comments

  1. 素晴らしい。モジュールを使いこなしてらっしゃる。記事の実装では新しいモジュールの作成にファンクターを使ってますが、あえて関数を使うなら下記のような感じでしょうか。ご参考まで。あとDummyモジュールはUnitモジュールという名前の方がちょっとかっこいいかも。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 let make () = (module struct let reset_time = ref 0 let t = ref 0 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 : S) include (val (make ()) : S) endlet print = let module C = (val (Counter.make ()) : Counter.S) in fun s -> Format.printf “%d:%s@.” (C.get ()) s

    • >osiireさんありがとうございます!なるほど、確かに第一級のモジュールを返すようにすれば、関数で書くことができますね。静的な使い方をせず、関数に渡していく使い方をメインにする場合はこちらの方が便利そうです。printの実装も、letで引数を定義せずfunを使って定義することで、let moduleが静的定義になっているのですね……この発想は自分にはありませんでした。Dummyについては、確かに意味合いとしてもUnitの方がしっくり来る気がします。正直、引数省略やアンダーバーなどでどうにかできないかと足掻いた末に諦めて、かなり適当に付けたモジュール名でした。我ながらネーミングセンスがないので、有り難く使わせて頂きたいと思います。

コメントを残す

Your email address will not be published.