@@ -6,40 +6,41 @@
66-- License : BSD3
77-- Maintainer : harendra.kumar@gmail.com
88--
9−-- Streamly, short for stream concurrently, combines the essence of
10−-- non-determinism, streaming and concurrency in functional programming.
11−-- Concurrent and non-concurrent applications are almost indistinguishable,
12−-- concurrency capability does not at all impact the performance of
13−-- non-concurrent case.
14−-- Streaming enables writing modular, composable and scalable applications with
15−-- ease and concurrency allows you to make them scale and perform well.
16−-- Streamly enables writing concurrent applications without being aware of
17−-- threads or synchronization. No explicit thread control is needed, where
18−-- applicable the concurrency rate is automatically controlled based on the
19−-- demand by the consumer. However, combinators are provided to fine tune the
20−-- concurrency control.
9+-- Streamly is a general computing framework based on streaming IO. The IO
10+-- monad and pure lists are a special case of streamly. On one hand, streamly
11+-- extends the lists of pure values to lists of monadic actions, on the other
12+-- hand it extends the IO monad with concurrrent non-determinism. In simple
13+-- imperative terms we can say that streamly extends the IO monad with @for@
14+-- loops and nested @for@ loops with concurrency support. You can understand
15+-- this analogy better once you can go through this tutorial.
16+--
17+-- Streaming in general enables writing modular, composable and scalable
18+-- applications with ease, and concurrency allows you to make them scale and
19+-- perform well. Streamly enables writing scalable concurrent applications
20+-- without being aware of threads or synchronization. No explicit thread
21+-- control is needed, where applicable the concurrency rate is automatically
22+-- controlled based on the demand by the consumer. However, combinators can be
23+-- used to fine tune the concurrency control.
24+--
2125-- Streaming and concurrency together enable expressing reactive applications
22−-- conveniently. See "Streamly.Examples" for a simple SDL based FRP example.
23−--
24−-- Streamly streams are very much like the Haskell lists and most of the
25−-- functions that work on lists have a counterpart that works on streams.
26−-- However, streamly streams can be generated, consumed or combined
27−-- concurrently. In this tutorial we will go over the basic concepts and how to
28−-- use the library. The documentation of @Streamly@ module has more details on
29−-- core APIs. For more APIs for constructing, folding, filtering, mapping and
30−-- zipping etc. see the documentation of "Streamly.Prelude" module. For
31−-- examples and other ways to use the library see the module
32−-- "Streamly.Examples" as well.
26+-- conveniently. See the @CirclingSquare@ example in the examples directory for
27+-- a simple SDL based FRP example. To summarize, streamly provides a unified
28+-- computing framework for streaming, non-determinism and functional reactive
29+-- programming in an elegant and simple API that is a natural extension of pure
30+-- lists to monadic streams.
31+--
32+-- In this tutorial we will go over the basic concepts and how to
33+-- use the library. See the last section for further reading resources.
3334
3435module Streamly.Tutorial
3536 (
3637 -- * Streams
3738 -- $streams
3839
39− -- ** Generating Streams
40+ -- * Generating Streams
4041 -- $generating
4142
42− -- ** Eliminating Streams
43+ -- * Eliminating Streams
4344 -- $eliminating
4445
4546 -- * Transforming Streams
@@ -125,6 +126,9 @@ module Streamly.Tutorial
125126
126127 -- * Comparison with Existing Packages
127128 -- $comparison
129+
130+ -- * Where to go next?
131+ -- $furtherReading
128132 )
129133where
130134
@@ -138,12 +142,43 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
138142
139143-- $streams
140144--
141−-- Streamly provides many different stream types depending on the desired
142−-- composition style. The simplest type is 'Stream', @Stream a@ represents a
143−-- stream of elements of type @a@ in IO monad. Streams are very much like lists
144−-- the only difference is that they are lists of monadic actions instead of
145−-- pure values. We can call them monadic lists. You can apply all the
146−-- constructs related to lists to monadic streams as well.
145+-- The way a list represents a sequence of pure values, a stream represents a
146+-- sequence of monadic actions. The monadic stream API offered by Streamly is
147+-- very close to the Haskell "Prelude" pure lists' API, it can be considered as a
148+-- natural extension of lists to monadic actions. Streamly streams provide
149+-- concurrent composition and merging of streams. It can be considered as a
150+-- concurrent list transformer. In contrast to the "Prelude" lists, merging or
151+-- appending streams of arbitrary length is scalable and inexpensive.
152+--
153+-- The basic stream type is 'Stream', it represents a sequence of IO actions,
154+-- and is a 'Monad'. The type 'StreamT' is a monad transformer that can
155+-- represent a sequence of actions in an arbitrary monad. The type 'Stream' is
156+-- in fact a synonym for @StreamT IO@. There are a few more types similar to
157+-- 'StreamT' that all represent streams and differ only in the 'Semigroup',
158+-- 'Applicative' and 'Monad' compositions of the streams. 'Stream' and
159+-- 'Costream' types compose serially whereas 'Coparallel' and 'Parallel' types
160+-- compose concurrently. All these types can be freely inter-converted using
161+-- type combinators without any cost. You can freely switch to any type of
162+-- composition at any point in the program. When no type annotation or
163+-- explicit stream type combinators are used, the default stream type is
164+-- inferred as 'Stream'.
165+--
166+-- The 'Stream' monad is almost a drop in replacement for the 'IO' monad, IO
167+-- monad is a special case of the 'Stream' monad; IO monad represents a single
168+-- IO action whereas the 'Stream' monad represents a sequence of IO actions.
169+-- The only change you need to make to go from 'IO' to 'Stream' is to use
170+-- 'runStream' to run the monad and to prefix the IO actions with 'once'. You
171+-- can also use 'liftIO' instead of 'once'. In fact, if you use liftIO you can
172+-- switch from 'Stream' to IO monad by simply removing the 'runStream'
173+-- function; no other changes are needed unless you have used some stream
174+-- specific composition or combinators.
175+--
176+-- Similarly, the 'Stream' type is almost a drop in replacement for pure lists,
177+-- pure lists are a special case of monadic streams. If you use 'nil' in place
178+-- of '[]' and '|:' in place ':' you can replace a list with a 'Stream'. The
179+-- only difference is that the elements must be monadic type and to operate on
180+-- the streams we must use the corresponding functions from "Streamly.Prelude"
181+-- instead of using the base "Prelude".
147182
148183-- $monadtransformers
149184--
@@ -161,22 +196,31 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
161196-- singleton stream use 'lift' and to lift from an IO action use 'liftIO'.
162197--
163198-- @
164−-- > runStream $ liftIO $ putStrLn "Hello world!"
199+-- > 'runStream' $ liftIO $ putStrLn "Hello world!"
165200-- Hello world!
166−-- > runStream $ lift $ putStrLn "Hello world!"
201+-- > 'runStream' $ lift $ putStrLn "Hello world!"
167202-- Hello world!
168203-- @
169204--
170205
171206-- $generating
172207--
208+-- We will assume the following imports in this tutorial. Go ahead, fire up a
209+-- GHCi session and import these lines to start playing.
210+--
211+-- @
212+-- > import "Streamly"
213+-- > import "Streamly.Prelude" (|:)
214+-- > import qualified "Streamly.Prelude" as S
215+-- @
216+--
173217-- 'nil' represents an empty stream and 'consM' or its operator form '|:' adds
174218-- a monadic action at the head of the stream.
175219--
176220-- @
177−-- > toList nil
221+-- > S.'toList' S.'nil'
178222-- []
179−-- > toList $ getLine |: getLine |: nil
223+-- > S.'toList' $ 'getLine' |: 'getLine' |: S.'nil'
180224-- hello
181225-- world
182226-- ["hello","world"]
@@ -186,9 +230,9 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
186230-- singleton stream from a monadic action use 'once'.
187231--
188232-- @
189−-- > toList $ pure 1
233+-- > S.'toList' $ 'pure' 1
190234-- [1]
191−-- > toList $ once getLine
235+-- > S.'toList' $ S.'once' 'getLine'
192236-- hello
193237-- ["hello"]
194238-- @
@@ -197,9 +241,9 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
197241-- 'fromFoldable' which is equivalent to a fold using 'cons' and 'nil':
198242--
199243-- @
200−-- > toList $ fromFoldable [1..3]
244+-- > S.'toList' $ S.'fromFoldable' [1..3]
201245-- [1,2,3]
202−-- > toList $ Prelude.foldr cons nil [1..3]
246+-- > S.'toList' $ 'Prelude.foldr' S.'cons' S.'nil' [1..3]
203247-- [1,2,3]
204248-- @
205249--
@@ -207,7 +251,7 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
207251-- right fold using 'consM' and 'nil':
208252--
209253-- @
210−-- > runStream $ Prelude.foldr (|:) nil [putStr "Hello ", putStrLn "world!"]
254+-- > 'runStream' $ 'Prelude.foldr' ('|:') S.'nil' ['putStr' "Hello ", 'putStrLn' "world!"]
211255-- Hello world!
212256-- @
213257--
@@ -236,7 +280,7 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
236280-- forever:
237281--
238282-- @
239−-- > runStream $ S.repeatM getLine & S.mapM putStrLn
283+-- > 'runStream' $ S.'repeatM' getLine & S.'mapM' putStrLn
240284-- @
241285--
242286-- The following code snippet reads lines from standard input, filters blank
@@ -244,19 +288,19 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
244288-- numbers them and prints them:
245289--
246290-- @
247−-- import Streamly
248−-- import qualified Streamly.Prelude as S
291+-- import "Streamly"
292+-- import qualified "Streamly.Prelude" as S
249293-- import Data.Char (toUpper)
250294-- import Data.Function ((&))
251295--
252−-- main = runStream $
253−-- S.repeatM getLine
254−-- & S.filter (not . null)
255−-- & S.drop 1
256−-- & S.take 2
296+-- main = 'runStream' $
297+-- S.'repeatM' getLine
298+-- & S.'filter' (not . null)
299+-- & S.'drop' 1
300+-- & S.'take' 2
257301-- & fmap (map toUpper)
258−-- & S.zipWith (\\n s -> show n ++ " " ++ s) (S.fromFoldable [1..])
259−-- & S.mapM putStrLn
302+-- & S.'zipWith' (\\n s -> show n ++ " " ++ s) (S.'fromFoldable' [1..])
303+-- & S.'mapM' putStrLn
260304-- @
261305
262306-- $semigroup
@@ -279,10 +323,11 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
279323--
280324-- @
281325-- import "Streamly"
282−-- import "Streamly.Prelude"(once)
326+-- import "Streamly.Prelude" (nil)
327+-- import qualified "Streamly.Prelude" as S
283328-- import Control.Concurrent
284329--
285−-- delay n = once $ do
330+-- delay n = S.'once' $ do
286331-- threadDelay (n * 1000000)
287332-- tid \<- myThreadId
288333-- putStrLn (show tid ++ ": Delay " ++ show n)
@@ -477,7 +522,7 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
477522--
478523-- @
479524-- main = 'runStream' . 'coparallely' $ traced (sqrt 9) '<>' traced (sqrt 16) '<>' traced (sqrt 25)
480−-- where traced m = once (myThreadId >>= print) >> return m
525+-- where traced m = S.'once' (myThreadId >>= print) >> return m
481526-- @
482527-- @
483528-- ThreadId 40
@@ -538,7 +583,7 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
538583--
539584-- @
540585-- import "Streamly"
541−-- import Streamly.Prelude (once)
586+-- import qualified Streamly.Prelude as S
542587-- import Network.HTTP.Simple
543588--
544589-- main = 'runStream' . 'parallely' $ google \<> bing \<> duckduckgo
@@ -546,7 +591,7 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
546591-- google = get "https://www.google.com/search?q=haskell"
547592-- bing = get "https://www.bing.com/search?q=haskell"
548593-- duckduckgo = get "https://www.duckduckgo.com/?q=haskell"
549−-- get s = once (httpNoBody (parseRequest_ s) >> putStrLn (show s))
594+-- get s = S.'once' (httpNoBody (parseRequest_ s) >> putStrLn (show s))
550595-- @
551596--
552597-- In the following example we can see that the fairly parallel composition
@@ -611,15 +656,15 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
611656--
612657-- @
613658-- import "Streamly"
614−-- import "Streamly.Prelude"
659+-- import qualified "Streamly.Prelude" as S
615660-- import Control.Concurrent
616661--
617662-- main = do
618−-- runStream $ parallely $ foldMap delay [1..10]
619−-- runStream $ 'foldWith' parallel (map delay [1..10])
620−-- runStream $ 'foldMapWith' parallel delay [1..10]
621−-- runStream $ 'forEachWith' parallel [1..10] delay
622−-- where delay n = once $ threadDelay (n * 1000000) >> print n
663+-- 'runStream' $ 'parallely' $ foldMap delay [1..10]
664+-- 'runStream' $ 'foldWith' 'parallel' (map delay [1..10])
665+-- 'runStream' $ 'foldMapWith' 'parallel' delay [1..10]
666+-- 'runStream' $ 'forEachWith' 'parallel' [1..10] delay
667+-- where delay n = S.'once' $ threadDelay (n * 1000000) >> print n
623668-- @
624669
625670-- $nesting
@@ -665,10 +710,10 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
665710--
666711-- @
667712-- import "Streamly"
668−-- import "Streamly.Prelude"
713+-- import qualified "Streamly.Prelude" as S
669714--
670715-- main = 'runStream' $ do
671−-- x <- 'fromFoldable' [3,2,1]
716+-- x <- S.'fromFoldable' [3,2,1]
672717-- delay x
673718-- @
674719-- @
@@ -687,8 +732,9 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
687732--
688733-- @
689734-- import "Streamly"
735+-- import qualified "Streamly.Prelude" as S
690736--
691−-- main = 'runStream' $ forever $ once getLine >>= once . putStrLn
737+-- main = 'runStream' $ forever $ S.once getLine >>= S.once . putStrLn
692738-- @
693739--
694740-- When multiple streams are composed using this style they nest in a DFS
@@ -698,12 +744,12 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
698744--
699745-- @
700746-- import "Streamly"
701−-- import "Streamly.Prelude"
747+-- import qualified "Streamly.Prelude" as S
702748--
703749-- main = 'runStream' $ do
704−-- x <- 'fromFoldable' [1,2]
705−-- y <- 'fromFoldable' [3,4]
706−-- once $ putStrLn $ show (x, y)
750+-- x <- S.'fromFoldable' [1,2]
751+-- y <- S.'fromFoldable' [3,4]
752+-- S.'once' $ putStrLn $ show (x, y)
707753-- @
708754-- @
709755-- (1,3)
@@ -757,12 +803,12 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
757803--
758804-- @
759805-- import "Streamly"
760−-- import "Streamly.Prelude"
806+-- import qualified "Streamly.Prelude" as S
761807--
762808-- main = 'runStream' . 'coparallely' $ do
763−-- x <- 'fromFoldable' [1,2]
764−-- y <- 'fromFoldable' [3,4]
765−-- once $ putStrLn $ show (x, y)
809+-- x <- S.'fromFoldable' [1,2]
810+-- y <- S.'fromFoldable' [3,4]
811+-- S.'once' $ putStrLn $ show (x, y)
766812-- @
767813-- @
768814-- (1,3)
@@ -782,12 +828,12 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
782828--
783829-- @
784830-- import "Streamly"
785−-- import "Streamly.Prelude"
831+-- import qualified "Streamly.Prelude" as S
786832--
787833-- main = 'runStream' . 'costreamly' $ do
788−-- x <- 'fromFoldable' [1,2]
789−-- y <- 'fromFoldable' [3,4]
790−-- once $ putStrLn $ show (x, y)
834+-- x <- S.'fromFoldable' [1,2]
835+-- y <- S.'fromFoldable' [3,4]
836+-- S.once $ putStrLn $ show (x, y)
791837-- @
792838-- @
793839-- (1,3)
@@ -808,10 +854,10 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
808854--
809855-- @
810856-- import "Streamly"
811−-- import "Streamly.Prelude"
857+-- import qualified "Streamly.Prelude" as S
812858--
813859-- main = 'runStream' . 'parallely' $ do
814−-- x <- 'fromFoldable' [3,2,1]
860+-- x <- S.'fromFoldable' [3,2,1]
815861-- delay x
816862-- @
817863-- @
@@ -829,20 +875,20 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
829875--
830876-- @
831877-- import "Streamly"
832−-- import "Streamly.Prelude"
878+-- import qualified "Streamly.Prelude" as S
833879--
834880-- composed :: 'IsStream' t => t m a
835881-- composed = do
836882-- sz <- sizes
837883-- cl <- colors
838884-- sh <- shapes
839−-- once $ putStrLn $ show (sz, cl, sh)
885+-- S.'once' $ putStrLn $ show (sz, cl, sh)
840886--
841887-- where
842888--
843−-- sizes = 'fromFoldable' [1, 2, 3]
844−-- colors = 'fromFoldable' ["red", "green", "blue"]
845−-- shapes = 'fromFoldable' ["triangle", "square", "circle"]
889+-- sizes = S.'fromFoldable' [1, 2, 3]
890+-- colors = S.'fromFoldable' ["red", "green", "blue"]
891+-- shapes = S.'fromFoldable' ["triangle", "square", "circle"]
846892-- @
847893--
848894-- Now we can interpret this in whatever way we want:
@@ -865,8 +911,9 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
865911--
866912-- @
867913-- import "Streamly"
914+-- import qualified "Streamly.Prelude" as S
868915--
869−-- main = ('toList' $ fmap show $ 'fromFoldable' [1..10]) >>= print
916+-- main = (S.'toList' $ fmap show $ S.'fromFoldable' [1..10]) >>= print
870917-- @
871918--
872919-- Also see the 'mapM' and 'sequence' functions for mapping actions, in the
@@ -883,14 +930,14 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
883930--
884931-- @
885932-- import "Streamly"
886−-- import "Streamly.Prelude"
933+-- import qualified "Streamly.Prelude" as S
887934-- import Control.Concurrent
888935--
889936-- s1 = d 1 <> d 2
890937-- s2 = d 3 <> d 4
891938-- d n = delay n >> return n
892939--
893−-- main = ('toList' . 'streamly' $ (,) \<$> s1 \<*> s2) >>= print
940+-- main = (S.'toList' . 'streamly' $ (,) \<$> s1 \<*> s2) >>= print
894941-- @
895942-- @
896943-- ThreadId 36: Delay 1
@@ -906,7 +953,7 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
906953-- order but since it is serial it takes a total of 17 seconds:
907954--
908955-- @
909−-- main = ('toList' . 'costreamly' $ (,) \<$> s1 \<*> s2) >>= print
956+-- main = (S.'toList' . 'costreamly' $ (,) \<$> s1 \<*> s2) >>= print
910957-- @
911958-- @
912959-- ThreadId 36: Delay 1
@@ -922,7 +969,7 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
922969-- of 10 seconds (1 + 2 + 3 + 4):
923970--
924971-- @
925−-- main = ('toList' . 'coparallely' $ (,) \<$> s1 \<*> s2) >>= print
972+-- main = (S.'toList' . 'coparallely' $ (,) \<$> s1 \<*> s2) >>= print
926973-- @
927974-- @
928975-- ThreadId 34: Delay 1
@@ -938,7 +985,7 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
938985-- therefore takes a total of 10 seconds (1 + 2 + 3 + 4):
939986--
940987-- @
941−-- main = ('toList' . 'parallely' $ (,) \<$> s1 \<*> s2) >>= print
988+-- main = (S.'toList' . 'parallely' $ (,) \<$> s1 \<*> s2) >>= print
942989-- @
943990-- @
944991-- ThreadId 34: Delay 1
@@ -987,14 +1034,14 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
9871034--
9881035-- @
9891036-- import "Streamly"
990−-- import "Streamly.Prelude"
1037+-- import qualified "Streamly.Prelude" as S
9911038-- import Control.Concurrent
9921039--
9931040-- d n = delay n >> return n
9941041-- s1 = 'streamly' $ d 1 <> d 2
9951042-- s2 = 'streamly' $ d 3 <> d 4
9961043--
997−-- main = ('toList' . 'zipStreamly' $ (,) \<$> s1 \<*> s2) >>= print
1044+-- main = (S.'toList' . 'zipStreamly' $ (,) \<$> s1 \<*> s2) >>= print
9981045-- @
9991046--
10001047-- This takes total 10 seconds to zip, which is (1 + 2 + 3 + 4) since
@@ -1017,7 +1064,7 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
10171064--
10181065-- @
10191066-- import "Streamly"
1020−-- import "Streamly.Prelude"
1067+-- import qualified "Streamly.Prelude" as S
10211068-- import Control.Concurrent
10221069-- import System.IO (stdout, hSetBuffering, BufferMode(LineBuffering))
10231070--
@@ -1027,7 +1074,7 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
10271074--
10281075-- main = do
10291076-- hSetBuffering stdout LineBuffering
1030−-- ('toList' . 'zipParallely' $ (,) \<$> s1 \<*> s2) >>= print
1077+-- (S.'toList' . 'zipParallely' $ (,) \<$> s1 \<*> s2) >>= print
10311078-- @
10321079--
10331080-- This takes 7 seconds to zip, which is max (1,3) + max (2,4) because 1 and 3
@@ -1059,11 +1106,11 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
10591106--
10601107-- @
10611108-- import "Streamly"
1062−-- import "Streamly.Prelude" (toList)
1109+-- import qualified "Streamly.Prelude" as S
10631110-- import Data.List (sum)
10641111--
10651112-- main = do
1066−-- z \<- 'toList'
1113+-- z \<- S.'toList'
10671114-- $ 'streamly' -- Serial monadic processing (sqrt below)
10681115-- $ do
10691116-- x2 \<- 'forEachWith' 'coparallel' [1..100] $ -- Concurrent @"for"@ loop
@@ -1252,10 +1299,22 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
12521299
12531300-- $comparison
12541301--
1255−-- Streamly unifies non-determinism, streaming, concurrency and FRP
1256−-- functionality that is otherwise covered by several disparate packages, and
1257−-- it does that with a surprisingly concise API. Here is a list of popular and
1258−-- well-known packages in all these areas:
1302+-- List transformers and logic programming monads also provide a product style
1303+-- composition similar to streamly, however streamly generalizes it with the
1304+-- time dimension; allowing streams to be composed in an asynchronous and
1305+-- concurrent fashion in many different ways. It also provides multiple
1306+-- alternative ways of composing streams e.g. serial, interleaved or
1307+-- concurrent.
1308+--
1309+-- This seemingly simple addition of asynchronicity and concurrency to product
1310+-- style streaming composition unifies a number of disparate abstractions into
1311+-- one powerful, concise and elegant abstraction. A wide variety of
1312+-- programming problems can be solved elegantly with this abstraction. In
1313+-- particular, it unifies three major programming domains namely
1314+-- non-deterministic (logic) programming, concurrent programming and functional
1315+-- reactive programming. In other words, you can do everything with this one
1316+-- abstraction that you could do with the popular libraries listed under these
1317+-- categories in the list below.
12591318--
12601319-- @
12611320-- +-----------------+----------------+
@@ -1318,3 +1377,10 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
13181377-- similar to @dunai@. dunai was designed from a FRP perspective and streamly
13191378-- was originally designed from a concurrency perspective. However, both have
13201379-- similarity at the core.
1380+
1381+-- $furtherReading
1382+--
1383+-- * Read the documentation of "Streamly" module
1384+-- * Read the documentation of "Streamly.Prelude" module
1385+-- * See the examples in the "examples" directory of the package
1386+-- * See the tests in the "test" directory of the package