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