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
LazyListはHaskellなどでもお馴染みの遅延リストだ。作成時には要素がメモリに乗っておらず、必要に応じて算出される。
遅延のポイントは
- 必要に応じて算出される
- 一度算出された値はメモ化され、再度計算が必要になることはない
の二点だ。
例えばこんな副作用のある関数から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にこんなものが立っていたが:
ちょっと無理筋じゃないかな?という気がする。