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