fetburner.core

コアダンプ

List.fold_right で List.fold_left を書く

 この記事は ML Advent Calendar 2020 の14日目の記事です.

 リストの Church encoding と言えば List.fold_right で表現することが多いように思いますが,果たして List.fold_right だけしか使わずに様々なリストについての再帰関数を表現できるのか疑問に思う人も多いのではないでしょうか? 本記事では,List.fold_right を用いて List.fold_left を実装する方法を紹介します.後者は末尾再帰なのでわざわざ回りくどいことをせずに直接使った方が良いのはもっともなのですが,foldr/build なんかを考える時に頭の片隅にあると便利かもしれません.

List.fold_rightList.fold_left

 OCaml 標準ライブラリで提供されている List.fold_right とは読んで字のごとくリストを右から畳み込む関数で,例えば [1; 2; 3; 4] みたいなリストがあった時,List.fold_right f [1; 2; 3; 4] accuf 1 (f 2 (f 3 (f 4 accu)))) を返してくれます.List.fold_left は反対に左から畳み込む関数で,例えば List.fold_left f accu [1; 2; 3; 4]f (f (f (f accu 1) 2) 3) 4 を返してくれます.

 List.fold_right は末尾再帰になっていないので,OCaml では List.fold_left を用いた方が良いとよく言われます.実装を見てみましょう.

let rec fold_left f accu l =
  match l with
    [] -> accu
  | a::l -> fold_left f (f accu a) l

let rec fold_right f l accu =
  match l with
    [] -> accu
  | a::l -> f a (fold_right f l accu)

fold_left の方の再帰呼び出しは関数の返り値を即座に返しているので末尾再帰最適化が行われるのに対し,fold_right の方は関数の返り値に対して f a を適用しているので最適化ができず,スタックを消費してしまいます. これが Haskell のような言語であれば,計算を途中で打ち切れたり無限リストを扱う時に便利だったり,スペースリークしなかったりと fold_right が輝ける場面もあるのですが,OCaml は基本正格評価なので要らない子扱いされることが多いです.かなしい.

 ただまあ,List.fold_right が完全に要らない子かと言えばそうでもなくて,理論的には面白い性質があります. リストとは空リストに対してコンスを繰り返したものと言うのを思い出せば,List.fold_right f l accul のコンスを f に,l の空リストを accu に置き換えた式に対応します. これはかなり綺麗な対応関係で,じゃあそのリストを定義してる所でコンスを f に,空リストを accu に置き換えてやれば計算途中でリストを作れるじゃんって最適化があって,foldr/build と呼ばれています.

また,プログラミング言語理論の研究の文脈で Church encoding と言うのがあって,数値とか真偽値とかみたいなデータを関数だけを使って表して,λ計算の計算能力について議論したりするんですが,その時に 「関数 faccu を受け取って List.fold_right f l accu の値を返す関数」でリスト l を表現したりもします.組があったらそれでリストを表現できるじゃんと思うかもしれないですが,List.fold_right を使った定義は rank-2 types があったら型が付く嬉しさがあります.

fold_right による fold_left の表現

 そんなこんなで List.fold_right にも理論的な面白さがあるので,明らかにそれを使って書くのは難しそうな List.fold_left も書き直してみようと言うのがこの記事の趣旨です. ちょっと遠くなってしまったので再掲しますが,どうやって下を使って上を書き直したものでしょうか?

let rec fold_left f accu l =
  match l with
    [] -> accu
  | a::l -> fold_left f (f accu a) l

let rec fold_right f l accu =
  match l with
    [] -> accu
  | a::l -> f a (fold_right f l accu)

一番困った点は,fold_left の方ではアキュムレータ accu再帰呼び出しの度に変化しているのに対し,fold_right の方では完全に固定されてしまっていることです. 再帰呼び出し f a (fold_right f l accu) の際,リストの先頭要素が a であることを知っている f afold_right f l accuaccu を書き換えさせてやりたいのですが,どう実現したものでしょうか?

 多分一番自然な実装方法は,fold_left の方でアキュムレータに入るはずだった値を受け取って,fold_left の返り値を返す関数を fold_right で書いてやることです.これなら f a から accu の値を受け渡すことができますね.再帰の都合上引数 laccu の順序が変わっていることにご注意を.

let rec fold_left f l =
  match l with
  | [] -> fun accu -> (* TODO *)
  | a::l -> fun accu -> (* TODO *)

まず,空リストの場合は普通に accu を返すだけなので,fold_right で生成する関数は恒等関数になります.

let rec fold_left f l =
  match l with
  | [] -> fun accu -> accu
  | a::l -> fun accu -> (* TODO *)

次にコンスの場合ですが,再帰呼び出しで帰ってきた関数に,fold_left で引き回すはずだったアキュムレータを渡してやれば良いです.

let rec fold_left f l =
  match l with
  | [] -> fun accu -> accu
  | a::l -> fun accu -> fold_left f l (f accu a)

よって,これを fold_right に落とし込んでやれば,fold_left を書き換えることができますね.ここでは引数 laccu の順序を元に戻していることに注意して下さい.

let fold_left f accu l =
  List.fold_right (fun a self accu -> self (f accu a)) l (fun accu -> accu) accu

List.fold_right の定義を見返せば分かるのですが,引数 l がコンス a::l のとき self には List.fold_right で書き直す前の fold_left f l が返していた関数が入ります.

実は rev して fold_right しているだけ?

 List.fold_right を用いて List.fold_left を実装できたのでめでたしめでたし,と言いたいところですが,ちょっと待ってください. List.fold_right に渡した関数 fun a self accu -> self (f accu a)) って3引数関数なのに対して毎回の再帰呼び出しでは2引数しか与えられないので,これは部分適用の状態にあります.この関数に最後の引数が渡されるのは List.fold_right が関数を返した後,つまりリストを最後まで走査した後な訳ですから,リストの長さと同じ数のクロージャが生成されてしまっています.

 副作用とかを無視すると List.fold_leftList.fold_right の間には List.fold_left f accu l = List.fold_right (Fun.flip f) (List.rev l) accu が成り立つので,結局この実装って非末尾再帰でリストを反転して List.fold_right する,かなり迂遠なことをしているのは否めないですね…