on eaaefcf help sign in
519 lines 17 kB history raw
1{-# OPTIONS_GHC -fno-warn-unused-imports #-}
2-- |
3-- Module : User.Tutorials.Tutorial
4-- Copyright : (c) 2017 Composewell Technologies
5--
6-- License : BSD3
7-- Maintainer : streamly@composewell.com
8--
9-- In this tutorial we will show how Streamly can be used for idiomatic
10-- dataflow programming. Before you go through this tutorial we recommend that
11-- you take a look at the Streamly Getting Started guide so that you are ready
12-- to try out the examples.
14module User.Tutorials.Tutorial
15 (
16 -- * Imports
17 -- $setup
19 -- * Stream Types
20 -- $streams
22 -- * Imports and Supporting Code
23 -- $imports
25 -- * Generating Streams
26 -- $generating
28 -- * Eliminating Streams
29 -- $eliminating
31 -- * Transforming Streams
32 -- $transformation
34 -- * Merging Streams
36 -- ** Semigroup Style
37 -- $semigroup
39 -- *** Deep Serial Composition ('Serial')
40 -- $serial
42 -- *** Wide Serial Composition ('WSerial')
43 -- $interleaved
45 -- ** Monoid Style
46 -- $monoid
48 -- * Nesting Streams
49 -- $nesting
51 -- ** Monad
52 -- $monad
54 -- *** Deep Serial Nesting ('Serial')
55 -- $regularSerial
57 -- *** Wide Serial Nesting ('WSerial')
58 -- $interleavedNesting
60 -- *** Exercise
61 -- $monadExercise
63 -- ** Applicative
64 -- $applicative
66 -- ** Functor
67 -- $functor
69 -- * Zipping Streams
70 -- $zipping
72 -- ** Serial Zipping
73 -- $serialzip
75 -- * Where to go next?
76 -- $furtherReading
77 )
78where
80import Streamly.Data.Stream
81import Data.Semigroup
82import Control.Applicative
83import Control.Monad
84import Control.Monad.IO.Class (MonadIO(..))
85import Control.Monad.Trans.Class (MonadTrans (lift))
87-- CAUTION: please keep setup and imports sections in sync
89-- XXX This tutorial has to be re-written.
91-- $setup
92--
93-- In most of example snippets we do not repeat the imports. Where
94-- imports are not explicitly specified use the imports shown below.
95--
96-- >>> :m
97-- >>> :set -fno-warn-deprecations
98-- >>> import Data.Function ((&))
99-- >>> import Streamly.Prelude ((|:), (|&))
100-- >>> import Streamly.Data.Stream (Stream)
101-- >>> import Streamly.Internal.Data.Stream.Cross (CrossStream(..))
102-- >>> import qualified Streamly.Prelude as Stream
103-- >>> import qualified Streamly.Data.Fold as Fold
104--
106-- $streams
107--
108-- The monadic stream API offered by Streamly is very close to the Haskell
109-- "Prelude" pure lists' API, it can be considered as a natural extension of
110-- lists to monadic actions. Streamly streams provide concurrent composition
111-- and merging of streams. It can be considered as a concurrent list
112-- transformer.
113--
114-- The 'Serial' type is almost a drop in replacement for pure lists,
115-- pure lists are a special case of monadic streams. If you use 'nil' in place
116-- of '[]' and '|:' in place ':' you can replace a list with a 'Serial' stream.
117-- The only difference is that the elements must be monadic type and to operate
118-- on the streams we must use the corresponding functions from
119-- "Streamly.Prelude" instead of using the base "Prelude".
121-- $generating
122--
123-- 'nil' represents an empty stream and 'consM' or its operator form '|:' adds
124-- a monadic action at the head of the stream.
125--
126-- >>> Stream.toList Stream.nil
127-- []
128--
129-- Stream.toList $ getLine |: getLine |: Stream.nil
130-- hello
131-- world
132-- ["hello","world"]
133--
134-- To create a singleton stream from a pure value use 'fromPure' and to
135-- create a singleton stream from a monadic action use 'fromEffect'.
136--
137-- >>> Stream.toList $ Stream.fromPure 1
138-- [1]
139--
140-- Stream.toList $ Stream.fromEffect getLine
141-- hello
142-- ["hello"]
143--
144-- To create a stream from pure values in a 'Foldable' container use
145-- 'fromFoldable' which is equivalent to a fold using 'cons' and 'nil':
146--
147-- >>> Stream.toList $ Stream.fromFoldable [1..3]
148-- [1,2,3]
149--
150-- >>> Stream.toList $ Prelude.foldr Stream.cons Stream.nil [1..3]
151-- [1,2,3]
152--
153-- To create a stream from monadic actions in a 'Foldable' container just use a
154-- right fold using 'consM' and 'nil':
155--
156-- >>> Stream.drain $ Prelude.foldr (|:) Stream.nil [putStr "Hello ", putStrLn "world!"]
157-- Hello world!
158--
159-- For more ways to construct a stream see the module "Streamly.Prelude".
161-- $eliminating
162--
163-- We have already seen 'drain' and toList to eliminate a stream in the
164-- examples above. 'drain' runs a stream discarding the results i.e. only
165-- for effects. 'toList' runs the stream and collects the results in a list.
166--
167-- For other ways to eliminate a stream see the @Folding@ section in
168-- "Streamly.Prelude" module.
170-- $transformation
171--
172-- Transformation over a stream is the equivalent of a @for@ loop construct in
173-- imperative paradigm. We iterate over every element in the stream and perform
174-- certain transformations for each element. Transformations may involve
175-- mapping functions over the elements, filtering elements from the stream or
176-- folding all the elements in the stream into a single value. Streamly streams
177-- are exactly like lists and you can perform all the transformations in the
178-- same way as you would on lists.
179--
180-- Here is a simple console echo program that just echoes every input line,
181-- forever:
182--
183-- >>> :{
184-- echo =
185-- Stream.repeatM getLine
186-- & Stream.mapM putStrLn
187-- & Stream.drain
188-- :}
189--
190-- The following code snippet reads lines from standard input, filters blank
191-- lines, drops the first non-blank line, takes the next two, up cases them,
192-- numbers them and prints them:
193--
194-- >>> import Data.Char (toUpper)
195-- >>> :{
196-- main =
197-- Stream.repeatM getLine
198-- & Stream.filter (not . null)
199-- & Stream.drop 1
200-- & Stream.take 2
201-- & fmap (map toUpper)
202-- & Stream.zipWith (\n s -> show n ++ " " ++ s) (Stream.fromFoldable [1..])
203-- & Stream.mapM putStrLn
204-- & Stream.drain
205-- :}
206--
208-- $semigroup
209--
210-- We can combine two streams into a single stream using semigroup composition
211-- operation '<>'. Streams can be combined in many different ways as described
212-- in the following sections, the '<>' operation behaves differently depending
213-- on the stream type in effect. The stream type and therefore the composition
214-- style can be changed at any point using one of the type combinators as
215-- discussed earlier.
217-- $serial
218--
219-- The 'Semigroup' operation '<>' of the 'Serial' type combines the two streams
220-- in a /serial depth first/ manner. We use the 'fromSerial' type combinator to
221-- effect 'Serial' style of composition. We can also use an explicit 'Serial'
222-- type annotation for the stream to achieve the same effect. However, since
223-- 'Serial' is the default type unless explicitly specified by using a
224-- combinator, we can omit using an explicit combinator or type annotation for
225-- this style of composition.
226--
227-- When two streams with multiple elements are combined in this manner, the
228-- monadic actions in the two streams are performed sequentially i.e. first all
229-- actions in the first stream are performed sequentially and then all actions
230-- in the second stream are performed sequentially. We call it
231-- /serial depth first/ as the full depth of one stream is fully traversed
232-- before we move to the next. The following example prints the sequence 1, 2,
233-- 3, 4:
234--
235-- >>> stream1 = print 1 |: print 2 |: Stream.nil
236-- >>> stream2 = print 3 |: print 4 |: Stream.nil
237-- >>> Stream.drain $ stream1 <> stream2
238-- 1
239-- 2
240-- 3
241-- 4
242--
243-- All actions in both the streams are performed serially in the same thread.
244--
245-- The polymorphic version of the binary operation '<>' of the 'Serial' type is
246-- 'serial'. We can use 'serial' to join streams in a sequential manner
247-- irrespective of the type of stream:
248--
249-- >>> Stream.drain $ stream1 `Stream.serial` stream2
250-- 1
251-- 2
252-- 3
253-- 4
254--
256-- $interleaved
257--
258-- The 'Semigroup' operation '<>' of the 'WSerial' type combines the two
259-- streams in a /serial breadth first/ manner. We use the fromWSerial type
260-- combinator to effect 'WSerial' style of composition. We can also use the
261-- 'WSerial' type annotation for the stream to achieve the same effect.
262--
263-- When two streams with multiple elements are combined in this manner, we
264-- traverse all the streams in a breadth first manner i.e. one action from each
265-- stream is performed and yielded to the resulting stream before we come back
266-- to the first stream again and so on.
267-- The following example prints the sequence 1, 3, 2, 4
268--
269-- >>> stream1 = print 1 |: print 2 |: Stream.nil
270-- >>> stream2 = print 3 |: print 4 |: Stream.nil
271-- >>> Stream.drain $ Stream.fromWSerial $ stream1 <> stream2
272-- 1
273-- 3
274-- 2
275-- 4
276--
277-- Even though the monadic actions of the two streams are performed in an
278-- interleaved manner they are all performed serially in the same thread.
279--
280-- The polymorphic version of the 'WSerial' binary operation '<>' is called
281-- 'wSerial'. We can use 'wSerial' to join streams in an interleaved manner
282-- irrespective of the type, notice that we have not used the fromWSerial
283-- combinator in the following example:
284--
285-- >>> Stream.drain $ stream1 `Stream.wSerial` stream2
286-- 1
287-- 3
288-- 2
289-- 4
290--
291-- Note that this composition cannot be used to fold infinite number of streams
292-- since it requires preserving the state until a stream is finished.
294-- $monoid
295--
296-- We can use 'Monoid' instances to fold a container of streams in the desired
297-- style using 'fold' or 'foldMap'. We have also provided some fold utilities
298-- to fold streams using the polymorphic combine operations:
299--
300-- * 'concatFoldableWith' is like 'fold', it folds a 'Foldable' container of
301-- streams using the given composition operator.
302-- * 'concatMapFoldableWith' is like 'foldMap', it folds like
303-- @concatFoldableWith@ but also maps a function before folding.
304-- * 'concatForFoldableWith' is like @concatMapFoldableWith@ but the container
305-- argument comes before the function argument.
306--
307-- All of the following are equivalent:
308--
309-- >>> :{
310-- traced = Stream.fromEffect . print
311-- main = do
312-- Stream.drain $ foldMap traced [1..10]
313-- Stream.drain $ Stream.concatFoldableWith Stream.serial (map traced [1..10])
314-- Stream.drain $ Stream.concatMapFoldableWith Stream.serial traced [1..10]
315-- Stream.drain $ Stream.concatForFoldableWith Stream.serial [1..10] traced
316-- :}
317--
319-- $nesting
320--
321-- Till now we discussed ways to apply transformations on a stream or to merge
322-- streams together to create another stream. We mentioned earlier that
323-- transforming a stream is similar to a @for@ loop in the imperative paradigm.
324-- We will now discuss the concept of a nested composition of streams which is
325-- analogous to nested @for@ loops in the imperative paradigm. Functional
326-- programmers call this style of composition a list transformer or @ListT@.
327-- Logic programmers call it a logic monad or non-deterministic composition,
328-- but for ordinary imperative minded people like me it is easier to think in
329-- terms of good old nested @for@ loops.
330--
331-- $monad
332--
333-- In functional programmer's parlance the 'Monad' instances of different
334-- 'IsStream' types implement non-determinism, exploring all possible
335-- combination of choices from both the streams. From an imperative
336-- programmer's point of view it behaves like nested loops i.e. for each
337-- element in the first stream and for each element in the second stream
338-- execute the body of the loop.
339--
340-- The 'Monad' instances of 'Serial', 'WSerial', 'Async' and 'WAsync'
341-- stream types support different flavors of nested looping. In other words,
342-- they are all variants of list transformer. The nesting behavior of these
343-- types correspond exactly to the way they merge streams as we discussed in
344-- the previous section.
345--
347-- $regularSerial
348--
349-- The 'Monad' composition of the 'CrossStream' type behaves like a standard list
350-- transformer.
351--
352-- Let's start with an example with a simple @for@ loop without any nesting.
353-- For simplicity of illustration we are using streams of pure values in all
354-- the examples. However, the streams could also be made of monadic actions
355-- instead.
356--
357-- >>> :{
358-- Stream.drain $ unCrossStream $ do
359-- x <- CrossStream (Stream.fromFoldable [3,2,1])
360-- CrossStream (Stream.fromEffect $ print x)
361-- :}
362-- 3
363-- 2
364-- 1
365--
366-- As we can see, the code after the @fromFoldable@ statement is run three
367-- times, once for each value of @x@ drawn from the stream. All the three
368-- iterations are serial and run in the same thread one after another. In
369-- imperative terms this is equivalent to a @for@ loop with three iterations.
370--
371-- We can write the console echo program that we wrote earlier using the monad
372-- instance:
373--
374-- >>> :{
375-- main =
376-- Stream.drain $ unCrossStream $ do
377-- x <- CrossStream (Stream.repeatM getLine)
378-- CrossStream (Stream.fromEffect $ putStrLn x)
379-- :}
380--
381-- When multiple streams are composed using this style they nest in a DFS
382-- manner:
383--
384-- >>> :{
385-- Stream.drain $ unCrossStream $ do
386-- x <- CrossStream (Stream.fromFoldable [1,2])
387-- y <- CrossStream (Stream.fromFoldable [3,4])
388-- CrossStream (Stream.fromEffect $ print (x, y))
389-- :}
390-- (1,3)
391-- (1,4)
392-- (2,3)
393-- (2,4)
394--
395-- i.e. inner loop iterations ((1,3), (1,4)) are executed before we proceed to
396-- the next iteration of the outer loop ((2,3), (2,4)). This behaves just like
397-- nested @for@ loops in imperative programming.
398--
399-- Notice that this is analogous to merging streams of type 'Serial' or merging
400-- streams using 'serial'.
402-- $interleavedNesting
403--
404-- TBD: generate the WSerial type using the TH utils.
405--
406-- The 'Monad' composition of 'WSerial' type interleaves the iterations of
407-- outer and inner loops in a nested loop composition.
408--
409-- >> :{
410-- Stream.drain $ getWSerial $ do
411-- x <- WSerialStream (Stream.fromFoldable [1,2])
412-- y <- WSerialStream (Stream.fromFoldable [3,4])
413-- WSerialStream (Stream.fromEffect $ print (x, y))
414-- :}
415-- (1,3)
416-- (2,3)
417-- (1,4)
418-- (2,4)
419--
420-- Note that (2,3) is preferred to (1,4). This works exactly the same way as
421-- the merging of two streams using 'wSerial' works.
422--
423-- The fromWSerial type combinator can be used to switch to this style of
424-- composition. Alternatively, a type annotation can be used to specify the
425-- type of the stream as 'WSerial'.
426--
428-- $monadExercise
429--
430-- >>> :{
431-- composed :: Stream IO ()
432-- composed = unCrossStream $ do
433-- sz <- CrossStream sizes
434-- cl <- CrossStream colors
435-- sh <- CrossStream shapes
436-- CrossStream (Stream.fromEffect $ print (sz, cl, sh))
437-- where
438-- sizes = Stream.fromFoldable [1, 2, 3]
439-- colors = Stream.fromFoldable ["red", "green", "blue"]
440-- shapes = Stream.fromFoldable ["triangle", "square", "circle"]
441-- :}
442--
444-- $functor
445--
446-- 'fmap' transforms a stream by mapping a function on all elements of the
447-- stream. 'fmap' behaves in the same way for all stream types, it is always
448-- serial.
449--
450-- >>> (Stream.toList $ fmap show $ Stream.fromFoldable [1..10]) >>= print
451-- ["1","2","3","4","5","6","7","8","9","10"]
452--
453-- Also see functions 'mapM' and 'sequence' from "Streamly.Prelude" module
454-- which can map actions concurrently depending on the type of the input stream.
456-- $applicative
457--
458-- Applicative is precisely the same as the 'ap' operation of 'Monad'. For
459-- zipping applicatives separate types 'ZipStream' and 'ZipAsync' are
460-- provided.
461--
462-- The following is an example of 'CrossStream' applicative, it runs all iterations
463-- serially:
464--
465-- >>> p n = Stream.fromEffect (print n >> pure n)
466-- >>> s1 = p 1 <> p 2
467-- >>> s2 = p 3 <> p 4
468--
469-- >>> (Stream.toList $ unCrossStream $ (,) <$> CrossStream s1 <*> CrossStream s2) >>= print
470-- 1
471-- 3
472-- 4
473-- 2
474-- 3
475-- 4
476-- [(1,3),(1,4),(2,3),(2,4)]
477--
478-- Similarly, 'WSerial' applicative runs the iterations in an interleaved
479-- order but being serial it too takes a total of 17 seconds:
480--
481-- >> (Stream.toList $ Stream.WSerialStream $ (,) <$> WSerialStream s1 <*> WSerialStream s2) >>= print
482-- 1
483-- 3
484-- 2
485-- 3
486-- 4
487-- 4
488-- [(1,3),(2,3),(1,4),(2,4)]
490-- $zipping
491--
492-- Zipping is a special transformation where the corresponding elements of two
493-- streams are combined together using a zip function producing a new stream of
494-- outputs. Two different types are provided for serial and concurrent zipping.
495-- These types provide an applicative instance that can be used to lift
496-- functions to zip the argument streams.
497-- Also see the zipping functions in the "Streamly.Prelude" module.
499-- $serialzip
500--
501-- The applicative instance of 'ZipSerial' type zips streams serially.
502-- 'fromZipSerial' type combinator can be used to switch to serial applicative
503-- zip composition:
504--
505-- >>> p n = Stream.fromEffect (print n >> pure n)
506-- >>> s1 = Stream.fromSerial $ p 1 <> p 2
507-- >>> s2 = Stream.fromSerial $ p 3 <> p 4
508-- >>> (Stream.toList $ Stream.fromZipSerial $ (,) <$> s1 <*> s2) >>= print
509-- 1
510-- 3
511-- 2
512-- 4
513-- [(1,3),(2,4)]
514--
516-- $furtherReading
517--
518-- * Read the concurrent streams tutorial
519-- * See the examples in <https://github.com/composewell/streamly-examples streamly-examples> repo.