on master help sign in
Add a doc illustrating CPS/direct style streams
4c4c20bHarendra Kumar
design/cps-vs-direct.rstnew file+328
@@ -0,0 +1,328 @@
1+Structured Loops (Direct Style)
2+===============================
3+
4+::
5+
6+ -- | Result of taking a single step in a stream
7+ data Step s a where
8+ Yield :: a -> s -> Step s a
9+ Stop :: Step s a
10+
11+ -- | Representation of a loop, step and state. Step returns the next value and
12+ -- the next state. We iterate on the state to keep producing more values.
13+ data Stream m a = forall s. Stream (s -> m (Step s a)) s
14+
15+Generating
16+----------
17+
18+::
19+
20+ nil :: Monad m => Stream m a
21+ nil = Stream (const (return Stop)) ()
22+
23+ -- | A single value
24+ yield :: Applicative m => a -> Stream m a
25+ yield x = Stream step True
26+
27+ where
28+
29+ step True = return $ Yield x False
30+ step False = return $ Stop
31+
32+ fromList :: Applicative m => [a] -> Stream m a
33+ fromList = Stream step
34+
35+ where
36+
37+ step _ (x:xs) = pure $ Yield x xs
38+ step _ [] = pure Stop
39+
40+Eliminating
41+-----------
42+
43+::
44+
45+ foldrM :: Monad m => (a -> m b -> m b) -> m b -> Stream m a -> m b
46+ foldrM f z (Stream step state) = go state
47+
48+ where
49+
50+ go st = do
51+ r <- step st
52+ case r of
53+ Yield x s -> f x (go s)
54+ Stop -> z
55+
56+In the above code the state is explicit and being threaded around in a
57+recursive loop. The state from the previous iteration is to be consumed
58+by the next iteration to generate the next value. This mandates a
59+closed recursive loop. This is straightforward translation of imperative
60+loops to functional paradigm.
61+
62+Transforming Loops
63+------------------
64+
65+::
66+
67+ mapM :: Monad m => (a -> m b) -> Stream m a -> Stream m b
68+ mapM f (Stream step state) = Stream step' state
69+
70+ where
71+
72+ step' st = do
73+ r <- step st
74+ case r of
75+ Yield x s -> f x >>= \a -> return $ Yield a s
76+ Stop -> return Stop
77+
78+State wrapping
79+--------------
80+
81+Some operations like "drop" (uniq, intersperse, deleteBy, insertBy) may
82+have to introduce a branch in the code by wrapping the state in another
83+layer::
84+
85+ drop :: Monad m => Int -> Stream m a -> Stream m a
86+ drop n (Stream step state) = Stream step' (state, Just n)
87+
88+ where
89+
90+ step' (st, Just i)
91+ | i > 0 = do
92+ r <- step st
93+ return $
94+ case r of
95+ Yield _ s -> step' (s, Just (i - 1))
96+ Stop -> Stop
97+ | otherwise = step' (st, Nothing)
98+
99+ step' (st, Nothing) = do
100+ r <- step st
101+ return $
102+ case r of
103+ Yield x s -> Yield x (s, Nothing)
104+ Stop -> Stop
105+
106+Note that the branch is always checked for every element in the stream.
107+It is still quite efficient because the code fuses.
108+
109+We should use rewrite rules to fuse such consecutive operations together as
110+they introduce branching which could be costly.
111+
112+Composing Loops
113+---------------
114+
115+Composing two independent loops together is not scalable, we need
116+to create a wrapper state, wrapping the state of the old stream:
117+
118+::
119+
120+ cons :: Monad m => m a -> Stream m a -> Stream m a
121+ cons m (Stream step state) = Stream step1 Nothing
122+
123+ where
124+
125+ step1 Nothing = m >>= \x -> return $ Yield x (Just state)
126+ step1 (Just st) = do
127+ r <- step st
128+ return $
129+ case r of
130+ Yield a s -> Yield a (Just s)
131+ Stop -> Stop
132+
133+As we keep consing we keep creating more layers wrapping the state in
134+``Maybe``. These layers need to be traversed every time we run the step
135+function of the composed stream. In the above example ``step1`` needs
136+to always branch on ``Nothing/Just`` to reach to the wrapped stream. More
137+layers we add the more branching needs to occur to generate an element
138+of the stream. If we have ``n`` "cons" operations we need to go through:
139+
140+* 1 branch at the top level to generate the first element
141+* 2 branches to generate the next element
142+* 3 branches to generate the third element
143+* n branches to generate the nth element
144+
145+The total number of branches that we need to take is: ``1 + 2 + 3 ... n`` i.e.
146+``n * (n + 1)/2 = O(n^2)`` where ``n`` is the number of cons operations.
147+
148+Conceptually, to avoid the introduction of a branch we could use a
149+mutable step function and state to modify step1/state after yielding
150+the first element. The next time we call it, it would be a different
151+function that i.e. "step" and its state "st". However, that would introduce
152+an indirection and mutability. There is a better way to do it with
153+immutability i.e. CPS.
154+
155+CPS representation
156+==================
157+
158+In the direct representation we represented a stream using a step and a
159+state. This model requires us to iterate the step on the state creating
160+an explicit loop. The state machine implemented by the step function is
161+incrementally modified by adding new layers in the state which introduce
162+branches to be traversed every time we go through the loop.
163+
164+::
165+
166+ newtype Stream m a = Stream
167+ { runStream :: forall r. (a -> Stream m a -> m r) -> m r -> m r }
168+
169+Here we represent the stream as a single function. Instead, the
170+function is provided with functions to be called next.
171+
172+Notice, the function does not have to be called again and again to
173+iterate on a state for generating new values. Therefore, there is no
174+closed recursion. There is no explicit loop.
175+
176+We (the current ``runStream`` function) can choose which one of the supplied
177+functions (continuations) to call next. If we decide to terminate
178+the stream execution we call the "stop" continuation. If we decide to
179+generate a value we call the "yield" continuation.
180+
181+A stream execution is composed of a progression of such continuations
182+until one of those decides to call the stop continuation. It is a
183+composition of functions, a tree of functions composed together.
184+
185+Yield continuation
186+------------------
187+
188+The yield continuation is provided with the generated value "a" and the
189+"Stream m a", the function representing the rest of the stream. Notice
190+that the stream function is done with one shot execution, there is no
191+closed loop or recursion, the future execution of the stream is the
192+responsibility of the continuation.
193+
194+The continuation consumes the element "a" and then proceeds to call
195+"Stream m a" using a "yield" and "stop" continuation. By modifying the
196+"yield" and "stop" continuations that it passes to call "Stream m a", it
197+can control the execution of the stream.
198+
199+Generating
200+----------
201+
202+::
203+
204+ nil :: Stream m a
205+ nil = Stream $ \_ stp -> stp
206+
207+ yield :: a -> Stream m a
208+ yield a = Stream $ \yield _ -> yield a nil
209+
210+ cons :: a -> Stream m a -> Stream m a
211+ cons a r = Stream $ \yld _ -> yld a r
212+
213+ fromList :: [a] -> Stream m a
214+ fromList = Prelude.foldr cons nil
215+
216+Eliminating
217+-----------
218+
219+::
220+
221+ foldrM :: (a -> m b -> m b) -> m b -> Stream m a -> m b
222+ foldrM step acc m = go m
223+ where
224+ go m1 =
225+ let stop = acc
226+ yieldk a r = step a (go r)
227+ in runStream yieldk stop m1
228+
229+Note that unlike in direct style fold, there is no generator state being
230+threaded around here instead the function yielded by the continuation is
231+being executed.
232+
233+Transforming
234+------------
235+
236+::
237+
238+ map :: (a -> b) -> Stream m a -> Stream m b
239+ map f = go
240+
241+ where
242+
243+ go m1 =
244+ Stream $ \yld stp ->
245+ let yieldk a r = yld (f a) (go r)
246+ in runStream yieldk stp m1
247+
248+In direct style we had to examine the constructors to determine the
249+current state and execute code based on that. Here, we have to make
250+the next function call at each step. The former is much more efficient
251+because the compiler can optimize well to remove the constructors and
252+generate code with direct branches not involving the constructors. On
253+the other hand placing a function call is costlier. Though in some cases
254+it can be avoided by using foldr/build fusion but not always.
255+
256+goto
257+----
258+
259+::
260+
261+ drop :: Int -> Stream m a -> Stream m a
262+ drop n = Stream $ go n
263+
264+ where
265+
266+ go n1 m1 =
267+ Stream $ \yld stp ->
268+ let yieldk _ r = runStream yld stp $ go (n1 - 1) r
269+ in if n1 <= 0
270+ then runStream yld stp m1
271+ else runStream yieldk stp m1
272+
273+This shows a crucial difference between direct style and CPS. In direct
274+style we have to always check for the branch to determine if we are
275+dropping the elements or consuming. In this case we can see that once
276+we have taken the "then" path we never have to check the condition "n1
277+<= 0", it is out of the way.
278+
279+Basically CPS provides us the ability to take an exit path and forget
280+about the past code forever. So if we have a million "drop" composed
281+together CPS would have no problem, after the final drop there won't be
282+any branches in the way whereas direct style would introduce a million
283+branches to be traversed forever.
284+
285+Combining Streams
286+-----------------
287+
288+::
289+
290+ cons :: a -> Stream m a -> Stream m a
291+ cons a r = Stream $ \yld _ -> yld a r
292+
293+Unlike in direct style representation, the performance of stream
294+generation is independent of the number of "cons" operations. There is
295+no quadratic complexity, we simply call the next continuation at each
296+step.
297+
298+Similarly appending streams is independent of number of appends.
299+
300+::
301+
302+ serial :: Stream m a -> Stream m a -> Stream m a
303+ serial m1 m2 = go m1
304+
305+ where
306+
307+ go m =
308+ Stream $ \yld stp ->
309+ let stop = runStream yld stp m2
310+ yieldk a r = yld a (go r)
311+ in runStream yieldk stop m
312+
313+Interleaved production and consumption
314+--------------------------------------
315+
316+In the direct style we have an explicit stream generator. A consumer
317+generates values using the generator and consumes them. In CPS the
318+consumer and generator are interleaved. The ``runStream`` function is a
319+generator and the "yield" continuation is a consumer. The consumer then
320+calls the generator again and so on.
321+
322+Loop vs goto
323+------------
324+
325+The direct style representation is like a structured loop with well
326+defined exit points. Whereas the CPS representation can exit from
327+anywhere. Therefore, exception handling and resource management in
328+direct style is much simpler to implement.