2010-01-27 [長年日記]
λ. “Stream fusion: from lists to streams to nothing at all” by Duncan Coutts, Roman Leshchinskiy, Don Stewart
リスト処理を、以下のようなストリーム型*1に対する処理に置き換えることによって、foldl等の最終的にリストを消費する関数以外は非再帰的な定義にする。そして、通常の最適化によって、中間データ構造を削除する。
data Stream a = ∃s. Stream (s -> Step a s) s data Step a s = Done | Yield a s | Skip s
例えば、sum (xs ++ ys) は、 sumの定義の展開と書き換え規則の適用によって foldl_s (+) 0 (append_s (stream xs) (stream ys)) となり、 定義を展開することで以下のようになる。
let next_stream xs = case xs of [] -> Done x : xs' -> Yield x xs' next_append (Left xs) = case next_stream xs of Done -> Skip (Right ys) Skip xs' -> Skip (Left xs') Yield x xs' -> Yield x (Left xs') next_append (Right ys) = case next_stream ys of Done -> Done Skip ys' -> Skip (Right ys') Yield y ys' -> Yield (Right ys') go z s = case next_append s of Done -> z Skip s' -> go z s' Yield x s' -> go (z + x) s' in go 0 (Left xs)
そして、ここからGHCのごく普通の最適化によって以下と同等のプログラムになる。
let go z (Left xs) = case xs of [] -> go z (Right ys) x:xs' -> go (z+x) (Left xs') go z (Right ys) = case ys of [] -> z y:ys' -> go (z+y) (Right ys') in go 0 (Left xs)
さらに、Constructor Specialization (Call-pattern specialisation) と呼ばれる最適化を用いると、以下と同等になる。
let go1 z xs = case xs of [] -> go2 z ys x:xs' -> go1 (z+x) xs' go2 z ys = case ys of [] -> z y:ys' -> go2 (z+y) ys' in go1 0 xs
簡単な書き換え規則とごく普通の最適化機能だけで、中間データ構造を除去できているのが素晴らしい。 また、foldr/build の short-cut fusion と比べても、zipWithのような複数引数のものや、concatMapのようなネストしたリスト構造を扱うものも融合出来ると。
*1 Haskell的には∃じゃなくて∀が正しいけど