The only resources on Stream Fusion I can find are papers introducing it, which aren't really the best learning sources. How exactly does stream fusion work?
More specifially, as this is the part the paper didn't explain well: how are the co-structures generated after the list->stream conversion (ie, maps f . maps g
) themselves folded?
Here's the definition of maps
from Duncan Coutt's thesis (section 1.4.2):
maps :: (a → b) → Stream a → Stream b
maps f (Stream next0 s0) = Stream next s0
where
next s = case next0 s of
Done → Done
Skip s′ → Skip s′
Yield x s′ → Yield (f x) s′
Now consider the expression
maps f . maps g
A compiler can inline (.)
to get
\x -> maps f (maps g x)
We can see from the definition of Stream
that it has only one constructor:
data Stream a = ∃ s . Stream (s → Step a s) s
So the previous result is equivalent to:
\(Stream next0 s) -> maps f (maps g (Stream next0 s))
Inlining maps g
, which is safe to do as maps
is non-recursive (this is the key insight of stream fusion):
\(Stream next0 s) -> maps f (Stream next1 s)
where
next1 s = case next0 s of
Done → Done
Skip s′ → Skip s′
Yield x s′ → Yield (g x) s′
Inlining maps f
:
\(Stream next0 s) -> Stream next2 s
where
next1 s = case next0 s of
Done → Done
Skip s′ → Skip s′
Yield x s′ → Yield (g x) s′
next2 s = case next1 s of
Done → Done
Skip s′ → Skip s′
Yield x s′ → Yield (f x) s′
Next we can inline next1
into next2
and simplify the case
expressions with "case-of-case" - again note that next1
is non-recursive - and delete the now dead next1
:
\(Stream next0 s) -> Stream next2 s
where
next2 s = case next0 s of
Done → Done
Skip s′ → Skip s′
Yield x s′ → Yield (f (g x)) s′
The key point is that these steps are all small optimisations that make sense in isolation and that don't require special compiler knowledge of either stream fusion itself, or the Stream
type or maps
function.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With