Arantium Maestum

プログラミング、囲碁、読書の話題

OCaml Batteries IncludedのLazyList/Seq/Enumについて

OCamlの標準ライブラリは貧弱なことで知られている。

OCamlコミュニティの間でも「あれはOCamlコンパイラを書くためのライブラリだ」と言われていて、実際INRIAで必要最低限(つまりコンパイラを書くのに必要な分だけ)提供している側面がある。

基本的なデータ構造に関しても、用意されている関数群は微妙に足りなかったりする。(例えばList.rangeのような数字のリストを作成する関数がなかったり)

大抵は自分で3行ほどで実装できるのだが、それにしても頻出するのでめんどくさい。さらにいえばもうちょっと他にもデータ構造がほしいと思うことも多い。(例えばdequeなど)

そういう穴を埋める「準標準ライブラリ」がいくつか存在する。Jane StreetのCore、コミュニティによって維持されているBatteries Included、そして比較的新しいcontainersあたりが有名どころだ。

私はBatteries Includedを使うことが多いが好みの問題だと思う。

そのBatteries IncludedのモジュールLazyList、SeqそしてEnumに関して最近調べたので記録しておく。

これらすべて「作成時にすべての要素がメモリに乗らないリスト」的なものである。なので具体的にどう違うのかを認識しておきたい。

LazyList

LazyListHaskellなどでもお馴染みの遅延リストだ。作成時には要素がメモリに乗っておらず、必要に応じて算出される。

遅延のポイントは

  • 必要に応じて算出される
  • 一度算出された値はメモ化され、再度計算が必要になることはない

の二点だ。

例えばこんな副作用のある関数からLazyListを作るとする:

let f () = print_endline "hello"; 0;;
let ll = BatLazyList.from f;;

llが作成された時点ではなんの計算も起きず、副作用も起きない。

第一要素を初めて求めてみる:

utop # BatLazyList.first ll;;
hello
- : int = 0

しっかり副作用が起きる。

もう一度第一要素を求めてみる:

utop # BatLazyList.first ll;;
- : int = 0

今度は副作用なし。

Seq

SeqはLazyListに似ているが、重要なポイントはdelayedだがlazyではないというところ。

作成時には要素が算出されず、必要に応じて計算が行われるのは同じだが、メモ化が起こらない。

let f n = print_endline @@ string_of_int n; Some (0, n+1);;
utop # let s = BatSeq.unfold f 0;;
0
val s : int Seq.t = <fun>

実際には作成時に第一要素だけは算出されているようだ。

第一要素を初めて求めてみる:

utop # BatSeq.first s;;
1
- : int = 0

しっかり副作用が起きる。(多分第一要素をBatSeq.firstで求めた時点で第二要素を算出している)

もう一度第一要素を求めてみる:

utop # BatSeq.first s;;
1
- : int = 0

また同じ副作用が起きる。

Seqを作成した時点で第一要素が作成されている(そのための関数適用が起きている)というのは記事を書き出す時点では知らなかった。副作用がある場合はちょっとしたハマりどころかもしれない。

Enum

EnumはSeqと同じくlazyではなくdelayedだが、さらに重要なポイントとしては、Enumの要素を算出することは破壊的だという点。Seqがイミュータブルなのに対してEnumはミュータブルなのだ。

let f n = Some (n, n+1);;
let e = BatEnum.unfold 0 f;;
utop # BatEnum.get e;;
- : int option = Some 0

utop # BatEnum.get e;;
- : int option = Some 1

utop # BatEnum.get e;;
- : int option = Some 2

関数型プログラミングファンなら「参照透明性とは・・・」と言いたくなるかもしれない挙動。

これは例えばPythonのジェネレータと同一の挙動で、「一回だけ消費する」ことに特化している。Python2から3への変更の一つに、多くの関数がリストではなくジェネレータを返すようになった点があることからもわかる通り、意外とほとんどのケースでは「一回だけ消費する」場合が多い。

BatteriesでもEnumはちょっと特別扱いされていて、すべてのデータ構造から「要素に順次アクセスする」ためのインタフェースとしてEnumを返す関数M.enumが定義されている。

BatteriesのGithub Issuesにこんなものが立っていたが:

github.com

ちょっと無理筋じゃないかな?という気がする。