1{-# OPTIONS_GHC -fno-warn-unused-imports #-}
3-- Module : Streamly.Tutorial
4-- Copyright : (c) 2017 Harendra Kumar
7-- Maintainer : harendra.kumar@gmail.com
9-- Streamly, short for stream concurrently, combines the essence of streaming
10-- and concurrency in functional programming. You can write concurrent as well
11-- as non-concurrent applications with streamly. Streaming enables writing
12-- modular, composable and scalable applications with ease and concurrency
13-- allows you to make them scale and perform well.
14-- Streamly enables writing concurrent applications without being aware of
15-- threads or synchronization. No explicit thread control is needed, where
16-- applicable the concurrency rate is automatically controlled based on the
17-- demand by the consumer. However, combinators are provided to fine tune the
18-- concurrency control.
19-- Streaming and concurrency together enable expressing reactive applications
20-- conveniently. See "Streamly.Examples" for a simple SDL based FRP example.
22-- Streamly streams are very much like the Haskell lists and most of the
23-- functions that work on lists have a counterpart that works on streams.
24-- However, streamly streams can be generated, consumed or combined
25-- concurrently. In this tutorial we will go over the basic concepts and how to
26-- use the library. The documentation of @Streamly@ module has more details on
27-- core APIs. For more APIs for constructing, folding, filtering, mapping and
28-- zipping etc. see the documentation of "Streamly.Prelude" module. For
29-- examples and other ways to use the library see the module
30-- "Streamly.Examples" as well.
32module Streamly.Tutorial
37 -- ** Generating Streams
40 -- ** Eliminating Streams
43 -- * Combining Streams
49 -- *** Serial composition ('<>')
52 -- *** Async composition ('<|')
55 -- *** Interleaved composition ('<=>')
58 -- *** Fair Concurrent composition ('<|>')
61 -- *** Custom composition
67 -- * Transforming Streams
73 -- *** Serial Composition ('StreamT')
76 -- *** Async Composition ('AsyncT')
79 -- *** Interleaved Composition ('InterleavedT')
80 -- $interleavedNesting
82 -- *** Fair Concurrent Composition ('ParallelT')
83 -- $fairlyConcurrentNesting
100 -- ** Parallel Zipping
103 -- * Summary of Compositions
104 -- $compositionSummary
106 -- * Concurrent Programming
109 -- * Reactive Programming
112 -- * State Machine Model
118 -- * Interworking with Streaming Libraries
121 -- * Comparison with Existing Packages
127import Streamly.Prelude
129import Control.Applicative
131import Control.Monad.IO.Class (MonadIO(..))
132import Control.Monad.Trans.Class (MonadTrans (lift))
136-- Streamly provides many different stream types depending on the desired
137-- composition style. The simplest type is 'StreamT'. 'StreamT' is a monad
138-- transformer, the type @StreamT m a@ represents a stream of values of type
139-- 'a' in some underlying monad 'm'. For example, @StreamT IO Int@ is a stream
140-- of 'Int' in 'IO' monad.
144-- Pure values can be placed into the stream type using 'return' or 'pure'.
145-- Effects in the IO monad can be lifted to the stream type using the 'liftIO'
146-- combinator. In a transformer stack we can lift actions from the lower monad
147-- using the 'lift' combinator. Some examples of streams with a single element:
150-- return 1 :: 'StreamT' IO Int
153-- liftIO $ putStrLn "Hello world!" :: 'StreamT' IO ()
156-- We can combine streams using '<>' to create streams of many elements:
159-- return 1 <> return 2 <> return 3 :: 'StreamT' IO Int
162-- For more ways to construct or generate a stream see the module
163-- "Streamly.Prelude".
167-- 'runStreamT' runs a composed 'StreamT' computation, lowering the type into
168-- the underlying monad and discarding the result stream:
173-- main = 'runStreamT' $ liftIO $ putStrLn "Hello world!"
176-- 'toList' runs a stream computation and collects the result stream in a list
177-- in the underlying monad. 'toList' is a polymorphic function that works on
178-- multiple stream types belonging to the class 'Streaming'. Therefore, before
179-- you run a stream you need to tell how you want to interpret the stream by
180-- using one of the stream type combinators ('serially', 'asyncly', 'parallely'
181-- etc.). The combinator 'serially' is equivalent to annotating the type as @::
188-- xs \<- 'toList' $ 'serially' $ return 1 <> return 2
192-- For other ways to eliminate or fold a stream see the module
193-- "Streamly.Prelude".
196-- Streams of the same type can be combined into a composite stream in many
197-- different ways using one of the semigroup style binary composition operators
198-- i.e. '<>', '<=>', '<|', '<|>', 'mplus'. These operators work on all stream
199-- types ('StreamT', 'AsyncT' etc.) uniformly.
201-- To illustrate the concurrent aspects, we will use the following @delay@
202-- function to introduce a delay specified in seconds.
206-- import Control.Concurrent
208-- delay n = liftIO $ do
209-- threadDelay (n * 1000000)
211-- putStrLn (show tid ++ ": Delay " ++ show n)
216-- We have already seen, the '<>' operator. It composes two streams in series
217-- i.e. the first stream is completely exhausted and then the second stream is
218-- processed. The following example prints the sequence 3, 2, 1 and takes a
219-- total of 6 seconds because everything is serial:
222-- main = 'runStreamT' $ delay 3 <> delay 2 <> delay 1
225-- ThreadId 36: Delay 3
226-- ThreadId 36: Delay 2
227-- ThreadId 36: Delay 1
231-- The '<=>' operator is serial like '<>' but it interleaves the two streams
232-- i.e. it yields one element from the first stream and then one element from
233-- the second stream, and so on. The following example prints the sequence 1,
234-- 3, 2, 4 and takes a total of 10 seconds because everything is serial:
237-- main = 'runStreamT' $ (delay 1 <> delay 2) '<=>' (delay 3 <> delay 4)
240-- ThreadId 36: Delay 1
241-- ThreadId 36: Delay 3
242-- ThreadId 36: Delay 2
243-- ThreadId 36: Delay 4
246-- Note that this operator cannot be used to fold infinite containers since it
247-- requires preserving the state until a stream is finished.
251-- The '<|' operator can run both computations concurrently, /when needed/.
252-- In the following example since the first computation blocks we start the
253-- next one in a separate thread and so on:
256-- main = 'runStreamT' $ delay 3 '<|' delay 2 '<|' delay 1
259-- ThreadId 42: Delay 1
260-- ThreadId 41: Delay 2
261-- ThreadId 40: Delay 3
264-- This is the concurrent version of the '<>' operator. The computations are
265-- triggered in the same order as '<>' except that they are concurrent. When
266-- we have a tree of computations composed using this operator, the tree is
267-- traversed in DFS style just like '<>'.
270-- main = 'runStreamT' $ (p 1 '<|' p 2) '<|' (p 3 '<|' p 4)
271-- where p = liftIO . print
280-- Concurrency provided by this operator is demand driven. The second
281-- computation is run concurrently with the first only if the first computation
282-- is not producing enough output to keep the stream consumer busy otherwise
283-- the second computation is run serially after the previous one. The number of
284-- concurrent threads is adapted dynamically based on the pull rate of the
285-- consumer of the stream.
286-- As you can see, in the following example the computations are run in a
287-- single thread one after another, because none of them blocks. However, if
288-- the thread consuming the stream were faster than the producer then it would
289-- have started parallel threads for each computation to keep up even if none
293-- main = 'runStreamT' $ traced (sqrt 9) '<|' traced (sqrt 16) '<|' traced (sqrt 25)
301-- Since the concurrency provided by this operator is demand driven it cannot
302-- be used when the composed computations have timers that are relative to each
303-- other because all computations may not be started at the same time and
304-- therefore timers in all of them may not start at the same time. When
305-- relative timing among all computations is important or when we need to start
306-- all computations at once for some reason '<|>' must be used instead.
307-- However, '<|' is useful in situations when we want to optimally utilize the
308-- resources and we know that the computations can run in parallel but we do
309-- not care if they actually run in parallel or not, that decision is left to
310-- the scheduler. Also, note that this operator can be used to fold infinite
311-- containers in contrast to '<|>', because it does not require us to run all
312-- of them at the same time.
314-- The left bias (or the DFS style) of the operator '<|' is suggested by its
315-- shape. You can also think of this as an unbalanced version of the fairly
316-- parallel operator '<|>'.
320-- The 'Alternative' composition operator '<|>', like '<|', runs the composed
321-- computations concurrently. However, unlike '<|' it runs all of the
322-- computations in fairly parallel manner using a round robin scheduling
323-- mechanism. This can be considered as the concurrent version of the fairly
324-- interleaved serial operation '<=>'. Note that this cannot be used on
325-- infinite containers, as it will lead to an infinite sized scheduling queue.
327-- The following example sends a query to three search engines in parallel and
328-- prints the name of the search engine as a response arrives:
332-- import Network.HTTP.Simple
334-- main = 'runStreamT' $ google \<|> bing \<|> duckduckgo
336-- google = get "https://www.google.com/search?q=haskell"
337-- bing = get "https://www.bing.com/search?q=haskell"
338-- duckduckgo = get "https://www.duckduckgo.com/?q=haskell"
339-- get s = liftIO (httpNoBody (parseRequest_ s) >> putStrLn (show s))
344-- The 'async' API can be used to create references to asynchronously running
345-- stream computations. We can then use 'uncons' to explore the streams
346-- arbitrarily and then recompose individual elements to create a new stream.
347-- This way we can dynamically decide which stream to explore at any given
348-- time. Take an example of a merge sort of two sorted streams. We need to
349-- keep consuming items from the stream which has the lowest item in the sort
350-- order. This can be achieved using async references to streams. See
351-- "Streamly.Examples.MergeSortedStreams".
355-- Each of the semigroup compositions described has an identity that can be
356-- used to fold a possibly empty container. An empty stream is represented by
357-- 'nil' which can be represented in various standard forms as 'mempty',
358-- 'empty' or 'mzero'.
359-- Some fold utilities are also provided by the library for convenience:
361-- * 'foldWith' folds a 'Foldable' container of stream computations using the
362-- given composition operator.
363-- * 'foldMapWith' folds like foldWith but also maps a function before folding.
364-- * 'forEachWith' is like foldMapwith but the container argument comes before
365-- the function argument.
366-- * The 'each' primitive from "Streamly.Prelude" folds a 'Foldable' container
367-- using the '<>' operator:
369-- All of the following are equivalent:
375-- 'toList' . 'serially' $ 'foldWith' (<>) (map return [1..10]) >>= print
376-- 'toList' . 'serially' $ 'foldMapWith' (<>) return [1..10] >>= print
377-- 'toList' . 'serially' $ 'forEachWith' (<>) [1..10] return >>= print
378-- 'toList' . 'serially' $ 'each' [1..10] >>= print
383-- The previous section discussed ways to merge the elements of two streams
384-- without doing any transformation on them. In this section we will explore
385-- how to transform streams using 'Functor', 'Applicative' or 'Monad' style
386-- compositions. The applicative and monad composition of all 'Streaming' types
387-- behave exactly the same way as a list transformer. For simplicity of
388-- illustration we are using streams of pure values in the following examples.
389-- However, the real application of streams arises when these streams are
390-- generated using monadic actions.
394-- In functional programmer's parlance the 'Monad' instance of 'Streaming'
395-- types implement non-determinism, exploring all possible combination of
396-- choices from both the streams. From an imperative programmer's point of view
397-- it behaves like nested loops i.e. for each element in the first stream and
398-- for each element in the second stream apply the body of the loop. If you are
399-- familiar with list transformer this behavior is exactly the same as that of
400-- a list transformer.
402-- Just like we saw in sum style compositions earlier, monadic composition also
403-- has multiple variants each of which exactly corresponds to one of the sum
404-- style composition variant.
408-- When we interpret the monadic composition as 'StreamT' we get a standard
409-- list transformer like serial composition.
412-- main = 'runStreamT' $ do
413-- x <- 'each' [3,2,1]
417-- ThreadId 30: Delay 3
418-- ThreadId 30: Delay 2
419-- ThreadId 30: Delay 1
422-- As you can see the code after the @each@ statement is run three times, once
423-- for each value of @x@. All the three iterations are serial and run in the
424-- same thread one after another. When compared to imperative programming, this
425-- can also be viewed as a @for@ loop with three iterations.
427-- When multiple streams are composed using this style they nest in a DFS
428-- manner i.e. nested iterations of an iteration are executed before we proceed
429-- to the next iteration at higher level. This behaves just like nested @for@
430-- loops in imperative programming.
433-- main = 'runStreamT' $ do
436-- liftIO $ putStrLn $ show (x, y)
445-- You will also notice that this is the monadic equivalent of the sum style
446-- composition using '<>'.
450-- When we interpret the monadic composition as 'AsyncT' we get a /concurrent/
451-- list-transformer like composition. Multiple monadic continuations (or loop
452-- iterations) may be started concurrently. Concurrency is demand driven
453-- i.e. more concurrent iterations are started only if the previous iterations
454-- are not able to produce enough output for the consumer of the output stream.
455-- This is the concurrent version of 'StreamT'.
458-- main = 'runAsyncT' $ do
459-- x <- 'each' [3,2,1]
463-- ThreadId 40: Delay 1
464-- ThreadId 39: Delay 2
465-- ThreadId 38: Delay 3
468-- As you can see the code after the @each@ statement is run three times, once
469-- for each value of @x@. All the three iterations are concurrent and run in
470-- different threads. The iteration with least delay finishes first. When
471-- compared to imperative programming, this can be viewed as a @for@ loop
472-- with three concurrent iterations.
474-- Concurrency is demand driven just as in the case of '<|'. When multiple
475-- streams are composed using this style the iterations are triggered in a DFS
476-- manner just like 'StreamT' i.e. nested iterations are executed before we
477-- proceed to the next iteration at higher level. However, unlike 'StreamT'
478-- more than one iterations may be started concurrently, and based on the
479-- demand from the consumer.
482-- main = 'runAsyncT' $ do
485-- liftIO $ putStrLn $ show (x, y)
494-- You will notice that this is the monadic equivalent of the '<|' style
495-- sum composition. The same caveats apply to this as the '<|' operation.
497-- $interleavedNesting
499-- When we interpret the monadic composition as 'InterleavedT' we get a serial
500-- but fairly interleaved list-transformer like composition. The monadic
501-- continuations or iterations of the outer loop are fairly interleaved with
502-- the continuations or iterations of the inner loop.
505-- main = 'runInterleavedT' $ do
508-- liftIO $ putStrLn $ show (x, y)
517-- You will notice that this is the monadic equivalent of the '<=>' style
518-- sum composition. The same caveats apply to this as the '<=>' operation.
520-- $fairlyConcurrentNesting
522-- When we interpret the monadic composition as 'ParallelT' we get a
523-- /concurrent/ list-transformer like composition just like 'AsyncT'. The
524-- difference is that this is fully parallel with all iterations starting
525-- concurrently instead of the demand driven concurrency of 'AsyncT'.
528-- main = 'runParallelT' $ do
529-- x <- 'each' [3,2,1]
533-- ThreadId 40: Delay 1
534-- ThreadId 39: Delay 2
535-- ThreadId 38: Delay 3
538-- You will notice that this is the monadic equivalent of the '<|>' style
539-- sum composition. The same caveats apply to this as the '<|>' operation.
543-- The streamly code is usually written in a way that is agnostic of the
544-- specific monadic composition type. We use a polymorphic type with a
545-- 'Streaming' type class constraint. When running the stream we can choose the
546-- specific mode of composition. For example look at the following code.
551-- composed :: 'Streaming' t => t m a
556-- liftIO $ putStrLn $ show (sz, cl, sh)
560-- sizes = 'each' [1, 2, 3]
561-- colors = 'each' ["red", "green", "blue"]
562-- shapes = 'each' ["triangle", "square", "circle"]
565-- Now we can interpret this in whatever way we want:
568-- main = 'runStreamT' composed
569-- main = 'runAsyncT' composed
570-- main = 'runInterleavedT' composed
571-- main = 'runParallelT' composed
574-- Equivalently, we can also write it using the type adapter combinators, like
578-- main = 'runStreaming' $ 'serially' $ composed
579-- main = 'runStreaming' $ 'asyncly' $ composed
580-- main = 'runStreaming' $ 'interleaving' $ composed
581-- main = 'runStreaming' $ 'parallely' $ composed
584-- As an exercise try to figure out the output of this code for each mode of
589-- 'fmap' transforms a stream by mapping a function on all elements of the
590-- stream. The functor instance of each stream type defines 'fmap' to be
591-- precisely the same as 'liftM', and therefore 'fmap' is always serial
592-- irrespective of the type. For concurrent mapping, alternative versions of
593-- 'fmap', namely, 'asyncMap' and 'parMap' are provided.
598-- main = ('toList' $ 'serially' $ fmap show $ 'each' [1..10]) >>= print
601-- Also see the 'mapM' and 'sequence' functions for mapping actions, in the
602-- "Streamly.Prelude" module.
606-- Applicative is precisely the same as the 'ap' operation of 'Monad'. For
607-- zipping and parallel applicatives separate types 'ZipStream' and 'ZipAsync'
610-- The following example runs all iterations serially and takes a total 17
611-- seconds (1 + 3 + 4 + 2 + 3 + 4):
615-- import "Streamly.Prelude"
616-- import Control.Concurrent
620-- d n = delay n >> return n
622-- main = ('toList' . 'serially' $ (,) \<$> s1 \<*> s2) >>= print
625-- ThreadId 36: Delay 1
626-- ThreadId 36: Delay 3
627-- ThreadId 36: Delay 4
628-- ThreadId 36: Delay 2
629-- ThreadId 36: Delay 3
630-- ThreadId 36: Delay 4
631-- [(1,3),(1,4),(2,3),(2,4)]
634-- Similalrly interleaving runs the iterations in an interleaved order but
635-- since it is serial it takes a total of 17 seconds:
638-- main = ('toList' . 'interleaving' $ (,) \<$> s1 \<*> s2) >>= print
641-- ThreadId 36: Delay 1
642-- ThreadId 36: Delay 3
643-- ThreadId 36: Delay 2
644-- ThreadId 36: Delay 3
645-- ThreadId 36: Delay 4
646-- ThreadId 36: Delay 4
647-- [(1,3),(2,3),(1,4),(2,4)]
650-- 'AsyncT' can run the iterations concurrently and therefore takes a total
651-- of 10 seconds (1 + 2 + 3 + 4):
654-- main = ('toList' . 'asyncly' $ (,) \<$> s1 \<*> s2) >>= print
657-- ThreadId 34: Delay 1
658-- ThreadId 36: Delay 2
659-- ThreadId 35: Delay 3
660-- ThreadId 36: Delay 3
661-- ThreadId 35: Delay 4
662-- ThreadId 36: Delay 4
663-- [(1,3),(2,3),(1,4),(2,4)]
666-- Similalrly 'ParallelT' as well can run the iterations concurrently and
667-- therefore takes a total of 10 seconds (1 + 2 + 3 + 4):
670-- main = ('toList' . 'parallely' $ (,) \<$> s1 \<*> s2) >>= print
673-- ThreadId 34: Delay 1
674-- ThreadId 36: Delay 2
675-- ThreadId 35: Delay 3
676-- ThreadId 36: Delay 3
677-- ThreadId 35: Delay 4
678-- ThreadId 36: Delay 4
679-- [(1,3),(2,3),(1,4),(2,4)]
682-- $compositionSummary
684-- The following table summarizes the types for monadic compositions and the
685-- operators for sum style compositions. This table captures the essence of
689-- +-----+--------------+------------+
690-- | | Serial | Concurrent |
691-- +=====+==============+============+
692-- | DFS | 'StreamT' | 'AsyncT' |
693-- | +--------------+------------+
695-- +-----+--------------+------------+
696-- | BFS | 'InterleavedT' | 'ParallelT' |
697-- | +--------------+------------+
698-- | | '<=>' | '<|>' |
699-- +-----+--------------+------------+
704-- Zipping is a special transformation where the corresponding elements of two
705-- streams are combined together using a zip function producing a new stream of
706-- outputs. Two different types are provided for serial and concurrent zipping.
707-- These types provide an applicative instance that zips the argument streams.
708-- Also see the zipping function in the "Streamly.Prelude" module.
712-- 'ZipStream' zips streams serially:
716-- import "Streamly.Prelude"
717-- import Control.Concurrent
719-- d n = delay n >> return n
720-- s1 = 'adapt' . 'serially' $ d 1 <> d 2
721-- s2 = 'adapt' . 'serially' $ d 3 <> d 4
723-- main = ('toList' . 'zipping' $ (,) \<$> s1 \<*> s2) >>= print
726-- This takes total 10 seconds to zip, which is (1 + 2 + 3 + 4) since
727-- everything runs serially:
730-- ThreadId 29: Delay 1
731-- ThreadId 29: Delay 3
732-- ThreadId 29: Delay 2
733-- ThreadId 29: Delay 4
739-- 'ZipAsync' zips streams concurrently:
743-- import "Streamly.Prelude"
744-- import Control.Concurrent
745-- import System.IO (stdout, hSetBuffering, BufferMode(LineBuffering))
747-- d n = delay n >> return n
748-- s1 = 'adapt' . 'serially' $ d 1 <> d 2
749-- s2 = 'adapt' . 'serially' $ d 3 <> d 4
752-- liftIO $ hSetBuffering stdout LineBuffering
753-- ('toList' . 'zippingAsync' $ (,) \<$> s1 \<*> s2) >>= print
756-- This takes 7 seconds to zip, which is max (1,3) + max (2,4) because 1 and 3
757-- are produced concurrently, and 2 and 4 are produced concurrently:
760-- ThreadId 32: Delay 1
761-- ThreadId 32: Delay 2
762-- ThreadId 33: Delay 3
763-- ThreadId 33: Delay 4
769-- When writing concurrent programs there are two distinct places where the
770-- programmer chooses the type of concurrency. First, when /generating/ a
771-- stream by combining other streams we can use one of the sum style operators
772-- to combine them concurrently or serially. Second, when /processing/ a stream
773-- in a monadic composition we can choose one of the monad composition types to
774-- choose the desired type of concurrency.
776-- In the following example the squares of @x@ and @y@ are computed
777-- concurrently using the '<|' operator and the square roots of their sum are
778-- also computed concurrently by using the 'asyncly' combinator. We can choose
779-- different combinators e.g. '<>' and 'serially', to control the concurrency.
783-- import "Streamly.Prelude" (toList)
784-- import Data.List (sum)
788-- $ 'asyncly' -- Concurrent monadic processing (sqrt below)
790-- x2 \<- 'forEachWith' ('<|') [1..100] $ -- Concurrent @for@ loop
791-- \\x -> return $ x * x -- body of the loop
792-- y2 \<- 'forEachWith' ('<|') [1..100] $
793-- \\y -> return $ y * y
794-- return $ sqrt (x2 + y2)
798-- You can see how this directly maps to the imperative style
799-- <https://en.wikipedia.org/wiki/OpenMP OpenMP> model, we use combinators
800-- and operators instead of the ugly pragmas.
802-- For more concurrent programming examples see,
803-- "Streamly.Examples.ListDirRecursive", "Streamly.Examples.MergeSortedStreams"
804-- and "Streamly.Examples.SearchEngineQuery".
808-- Reactive programming is nothing but concurrent streaming which is what
809-- streamly is all about. With streamly we can generate streams of events,
810-- merge streams that are generated concurrently and process events
811-- concurrently. We can do all this without any knowledge about the specifics
812-- of the implementation of concurrency. In the following example you will see
813-- that the code is just regular Haskell code without much streamly APIs used
814-- (active hyperlinks are the streamly APIs) and yet it is a reactive
818-- This application has two independent and concurrent sources of event
819-- streams, @acidRain@ and @userAction@. @acidRain@ continuously generates
820-- events that deteriorate the health of the game character. @userAction@ can
821-- be "potion" or "quit". When the user types "potion" the health improves and
822-- the game continues.
825-- {-\# LANGUAGE FlexibleContexts #-}
828-- import Control.Concurrent (threadDelay)
829-- import Control.Monad (when)
830-- import Control.Monad.State
831-- import Data.Semigroup (cycle1)
833-- data Event = Harm Int | Heal Int | Quit deriving (Show)
835-- userAction :: MonadIO m => 'StreamT' m Event
836-- userAction = cycle1 $ liftIO askUser
841-- "potion" -> return (Heal 10)
842-- "quit" -> return Quit
843-- _ -> putStrLn "What?" >> askUser
845-- acidRain :: MonadIO m => 'StreamT' m Event
846-- acidRain = cycle1 $ liftIO (threadDelay 1000000) >> return (Harm 1)
848-- game :: ('MonadAsync' m, MonadState Int m) => 'StreamT' m ()
850-- event \<- userAction \<|> acidRain
852-- Harm n -> modify $ \\h -> h - n
853-- Heal n -> modify $ \\h -> h + n
854-- Quit -> fail "quit"
857-- when (h <= 0) $ fail "You die!"
858-- liftIO $ putStrLn $ "Health = " ++ show h
861-- putStrLn "Your health is deteriorating due to acid rain,\\
862-- \\ type \\"potion\\" or \\"quit\\""
863-- _ <- runStateT ('runStreamT' game) 60
867-- You can also find the source of this example in
868-- "Streamly.Examples.AcidRainGame". It has been adapted from Gabriel's
869-- <https://hackage.haskell.org/package/pipes-concurrency-2.0.8/docs/Pipes-Concurrent-Tutorial.html pipes-concurrency>
871-- This is much simpler compared to the pipes version because of the builtin
872-- concurrency in streamly. You can also find a SDL based reactive programming
873-- example adapted from Yampa in "Streamly.Examples.CirclingSquare".
876-- State machine stuff
880-- Even though streamly covers all that is provided by the 'async' package or
881-- most of what is provided by the 'streaming', 'pipes' or 'conduit' packages,
882-- I would not say that it renders those useless. Streamly is like monad if
883-- 'async' is applicative and monads and applicatives both have their use
884-- cases. It can completely repalce 'async', the ZipAsync type is equivalent to
885-- the functionality provided by 'async'.
887-- pipes and conduit are like Arrows and streamly is like monad to them. You
888-- would use pipes and conduit when you do not need the product style
889-- composition and the implicit concurrency.
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list