on master help sign in
Update StreamK documentation
commite694d88ee7397aeea0bbcc921ca821f617562680 authorHarendra Kumar <harendra@composewell.com> dateSun 12 Mar 2023, 18:15 (4 years ago) parentebe2472
5 files changed+132−91
core/src/DocTestDataStreamK.hs+2 core/src/Streamly/Data/Stream.hs+73−24 core/src/Streamly/Data/StreamK.hs+50−21 core/src/Streamly/Internal/Data/Stream/StreamK.hs+6−41 core/src/Streamly/Internal/Data/Stream/StreamK/Type.hs+1−5
core/src/DocTestDataStreamK.hs+2
@@ -4,6 +4,8 @@
44>>> import Data.Function (fix, (&))
55>>> import Data.Semigroup (cycle1)
66
7+>>> effect n = print n >> return n
8+
79>>> import Streamly.Data.StreamK (StreamK)
810>>> import qualified Streamly.Data.Fold as Fold
911>>> import qualified Streamly.Data.Parser as Parser
core/src/Streamly/Data/Stream.hs+73−24
@@ -14,6 +14,9 @@
1414-- Please refer to "Streamly.Internal.Data.Stream" for more functions that have
1515-- not yet been released.
1616--
17+-- For continuation passing style (CPS) stream type, please refer to
18+-- the "Streamly.Data.StreamK" module.
19+--
1720-- Checkout the <https://github.com/composewell/streamly-examples>
1821-- repository for many more real world examples of stream programming.
1922
@@ -501,30 +504,76 @@ import Prelude
501504-- "Data.List" like functions and many more powerful combinators to perform
502505-- common programming tasks.
503506--
504−-- == Performance Notes
505−--
506−-- The 'Stream' type represents a stream by composing state as data which
507−-- enables stream fusion. Stream fusion generates a tight loop without any
508−-- constructor allocations between the stages, providing C like performance for
509−-- the loop. Stream fusion works when multiple functions are combined in a
510−-- pipeline statically. Therefore, the operations in this module must be
511−-- inlined and must not be used recursively to allow for stream fusion.
512−--
513−-- Using the 'Stream' type binary stream construction operations like 'cons',
514−-- 'append' etc. degrade quadratically (O(n^2)) when combined many times. If
515−-- you need to combine these operations, say more than 50 times in a single
516−-- loop, then you should use the continuation style stream type 'StreamK'
517−-- instead. Also, if you need to use these operations in a recursive loop you
518−-- should use 'StreamK' instead.
519−--
520−-- The 'StreamK' type represents a stream by composing function calls,
521−-- therefore, a function call overhead is incurred at each composition. It is
522−-- quite fast in general but may be a few times slower than a fused stream.
523−-- However, it allows for scalable dynamic composition and control flow
524−-- manipulation. Using the 'StreamK' type binary operations like 'cons' and
525−-- 'append' provide linear (O(n)) performance.
526−--
527−-- 'Stream' and 'StreamK' types can be interconverted.
507+-- == Stream Fusion
508+--
509+-- The fused 'Stream' type employs stream fusion for C-like performance when
510+-- looping over data. It represents a stream source or transformation by
511+-- defining a state machine with explicit state, and a step function working on
512+-- the state. A typical stream operation consumes elements from the previous
513+-- state machine in the pipeline, transforms them and yields new values for the
514+-- next stage to consume. The stream operations are modular and represent a
515+-- single task, they have no knowledge of previous or next operation on the
516+-- elements.
517+--
518+-- A typical stream pipeline consists of a stream producer, several stream
519+-- transformation operations and a stream consumer. All these operations taken
520+-- together form a closed loop processing the stream elements. Elements are
521+-- transferred between stages using a boxed data constructor. However, all the
522+-- stages of the pipeline are fused together by GHC, eliminating the
523+-- intermediate constructors, and thus forming a tight C like loop without any
524+-- boxed data being used in the loop.
525+--
526+-- Stream fusion works effectively when:
527+--
528+-- * the stream pipeline is composed statically (known at compile time)
529+-- * all the operations forming the loop are inlined
530+-- * the loop is not recursively defined, recursion breaks inlining
531+--
532+-- If these conditions cannot be met, the CPS style stream type 'StreamK' may
533+-- turn out to be a better choice than the fused stream type 'Stream'.
534+--
535+-- == Stream vs StreamK
536+--
537+-- The fused stream model avoids constructor allocations or function call
538+-- overheads. However, the stream is represented as a state machine and to
539+-- generate elements it has to navigate the decision tree of the state machine.
540+-- Moreover, the state machine is cranked for each element in the stream. This
541+-- performs extremely well when the number of states are limited. The state
542+-- machine starts getting expensive as the number of states increase. For
543+-- example, generating a million element stream from a list requires a single
544+-- state and is very efficient. However, using fused 'cons' to generate a
545+-- million element stream would be a disaster.
546+--
547+-- A typical worst case scenario for fused stream model is a large number of
548+-- `cons` or `append` operations. A few static `cons` or `append` operations
549+-- are very fast and much faster than a CPS style stream. However, if we
550+-- construct a large stream using `cons` it introduces as many states in the
551+-- state machine as the number of elements. If we compose the `cons` as a
552+-- binary tree it will take @n * log n@ time to navigate the tree, and @n * n@
553+-- if it is a right associative composition.
554+--
555+-- For quadratic cases of fused stream, after a certain threshold the CPS
556+-- stream would perform much better and exhibit linear performance behavior.
557+-- Operations like 'cons' or 'append'; are typically recursively called to
558+-- construct a lazy infinite stream. For such use cases the CPS style 'StreamK'
559+-- type is provided. CPS streams do not have a state machine that needs to be
560+-- cranked for each element, past state has no effect on the future element
561+-- processing. However, it incurs a function call overhead for each operation
562+-- for each element, which could be very large overhead compared to fused state
563+-- machines even if it has many states and cranks it for each element. But in
564+-- some cases scales tip in favor of the CPS stream. In those cases even though
565+-- CPS has a large constant overhead, it has a linear performance rather than
566+-- quadratic.
567+--
568+-- As a general guideline, if you have to use 'cons' or 'append' or operations
569+-- of similar nature, at a large scale, then 'StreamK' should be used. When you
570+-- need to compose the stream dynamically or recursively, then 'StreamK' should
571+-- be used. Typically you would use a dynamically generated 'StreamK' with
572+-- chunks of data which can then be processed by statically fused stream
573+-- pipeline operations.
574+--
575+-- 'Stream' and 'StreamK' types can be interconverted. See
576+-- "Streamly.Data.StreamK" module for conversion operations.
528577--
529578-- == Useful Idioms
530579--
core/src/Streamly/Data/StreamK.hs+50−21
@@ -8,29 +8,15 @@
88-- Stability : released
99-- Portability : GHC
1010--
11−-- Streams using Continuation Passing Style (CPS). See notes in "Data.Stream"
12−-- module to know when to use this module.
11+-- Streams using Continuation Passing Style (CPS). See the @Stream vs StreamK@
12+-- section in the "Streamly.Data.Stream" module to know when to use this
13+-- module.
1314--
14−-- 'StreamK' operations have more overhead than 'Stream' operations, however,
15−-- 'Stream' operations fuse only when the pipeline is statically composed, is
16−-- not too large, and does not involve recursion. For such cases, a typical
17−-- example is creating a large or infinite stream by appending, 'StreamK'
18−-- should be used.
19−--
20−-- You can use operations from "Streamly.Data.Stream" for StreamK as well by
21−-- converting StreamK to Stream, and vice-versa. Most of the time converting to
22−-- 'Stream' is better than using native StreamK operations, however in certain
23−-- cases using native StreamK operations may work better. Using 'Stream'
24−-- operations are especially when multiple contiguous operations can fuse
25−-- together.
26−--
27−-- There are no native exception handling operations in the StreamK module,
28−-- please use 'toStream' to convert to 'Stream' type and use exception
29−-- handling operations from "Streamly.Data.Stream".
30−--
31−-- See "Streamly.Internal.Data.Stream.StreamK" for more native StreamK
32−-- operations.
15+-- Please refer to "Streamly.Internal.Data.Stream.StreamK" for more functions
16+-- that have not yet been released.
3317
18+-- Notes:
19+--
3420-- primitive/open loop operations that can be used recursively e.g. uncons,
3521-- foldBreak, parseBreak should not be converted from StreamD for use in
3622-- StreamK, instead native StreamK impl should be used.
@@ -44,6 +30,16 @@
4430--
4531module Streamly.Data.StreamK
4632 (
33+ -- * Setup
34+ -- | To execute the code examples provided in this module in ghci, please
35+ -- run the following commands first.
36+ --
37+ -- $setup
38+
39+ -- * Overview
40+ -- $overview
41+
42+ -- * Type
4743 StreamK
4844
4945 -- * Construction
@@ -122,3 +118,36 @@ import Streamly.Internal.Data.Stream.StreamK
122118import Prelude hiding (reverse, zipWith, mapM, dropWhile, take)
123119
124120#include "DocTestDataStreamK.hs"
121+
122+-- $overview
123+--
124+-- Continuation passing style (CPS) stream implementation. The 'K' in 'StreamK'
125+-- stands for Kontinuation.
126+--
127+-- StreamK can be constructed like lists, except that they use 'nil' instead of
128+-- '[]' and 'cons' instead of ':'.
129+--
130+-- `cons` adds a pure value at the head of the stream:
131+--
132+-- >>> import Streamly.Data.StreamK (StreamK, cons, consM, nil)
133+-- >>> stream = 1 `cons` 2 `cons` nil :: StreamK IO Int
134+--
135+-- You can use operations from "Streamly.Data.Stream" for StreamK as well by
136+-- converting StreamK to Stream ('toStream'), and vice-versa ('fromStream').
137+--
138+-- >>> Stream.fold Fold.toList $ StreamK.toStream stream -- IO [Int]
139+-- [1,2]
140+--
141+-- `consM` adds an effect at the head of the stream:
142+--
143+-- >>> stream = effect 1 `consM` effect 2 `consM` nil
144+-- >>> Stream.fold Fold.toList $ StreamK.toStream stream
145+-- 1
146+-- 2
147+-- [1,2]
148+--
149+-- == Exception Handling
150+--
151+-- There are no native exception handling operations in the StreamK module,
152+-- please convert to 'Stream' type and use exception handling operations from
153+-- "Streamly.Data.Stream".
core/src/Streamly/Internal/Data/Stream/StreamK.hs+6−41
@@ -8,47 +8,14 @@
88-- Stability : experimental
99-- Portability : GHC
1010--
11−-- To run examples in this module:
12−--
13−-- >>> import qualified Streamly.Data.Fold as Fold
14−-- >>> import qualified Streamly.Data.Stream as Stream
15−-- >>> import qualified Streamly.Data.StreamK as StreamK
16−--
17−-- We will add some more imports in the examples as needed.
18−--
19−-- For effectful streams we will use the following IO action:
20−--
21−-- >>> effect n = print n >> return n
22−--
23−-- = Overview
24−--
25−-- Continuation passing style (CPS) stream implementation. The 'K' in 'StreamK'
26−-- stands for Kontinuation.
27−--
28−-- StreamK can be constructed like lists, except that they use 'nil' instead of
29−-- '[]' and 'cons' instead of ':'.
30−--
31−-- `cons` adds a pure value at the head of the stream:
32−--
33−-- >>> import Streamly.Data.StreamK (StreamK, cons, consM, nil)
34−-- >>> stream = 1 `cons` 2 `cons` nil :: StreamK IO Int
35−--
36−-- Convert 'StreamK' to 'Stream' to use functions from the
37−-- "Streamly.Data.Stream" module:
38−--
39−-- >>> Stream.fold Fold.toList $ StreamK.toStream stream -- IO [Int]
40−-- [1,2]
41−--
42−-- `consM` adds an effect at the head of the stream:
43−--
44−-- >>> stream = effect 1 `consM` effect 2 `consM` nil
45−-- >>> Stream.fold Fold.toList $ StreamK.toStream stream
46−-- 1
47−-- 2
48−-- [1,2]
49−--
5011module Streamly.Internal.Data.Stream.StreamK
5112 (
13+ -- * Setup
14+ -- | To execute the code examples provided in this module in ghci, please
15+ -- run the following commands first.
16+ --
17+ -- $setup
18+
5219 -- * The stream type
5320 Stream
5421 , StreamK(..)
@@ -916,14 +883,12 @@ mapMaybe f = go
916883
917884-- | Zip two streams serially using a pure zipping function.
918885--
919−-- @since 0.1.0
920886{-# INLINE zipWith #-}
921887zipWith :: Monad m => (a -> b -> c) -> StreamK m a -> StreamK m b -> StreamK m c
922888zipWith f = zipWithM (\a b -> return (f a b))
923889
924890-- | Zip two streams serially using a monadic zipping function.
925891--
926−-- @since 0.1.0
927892{-# INLINE zipWithM #-}
928893zipWithM :: Monad m =>
929894 (a -> b -> m c) -> StreamK m a -> StreamK m b -> StreamK m c
core/src/Streamly/Internal/Data/Stream/StreamK/Type.hs+1−5
@@ -280,7 +280,6 @@ infixr 5 .:
280280-- [1,2,3]
281281-- @
282282--
283−-- @since 0.1.1
284283{-# INLINE (.:) #-}
285284(.:) :: a -> StreamK m a -> StreamK m a
286285(.:) = cons
@@ -1848,13 +1847,10 @@ mfix f = mkStream $ \st yld sng stp ->
18481847-------------------------------------------------------------------------------
18491848
18501849-- |
1851−-- @
1852−-- fromFoldable = 'Prelude.foldr' 'cons' 'nil'
1853−-- @
1850+-- >>> fromFoldable = Prelude.foldr StreamK.cons StreamK.nil
18541851--
18551852-- Construct a stream from a 'Foldable' containing pure values:
18561853--
1857−-- @since 0.2.0
18581854{-# INLINE fromFoldable #-}
18591855fromFoldable :: Foldable f => f a -> StreamK m a
18601856fromFoldable = Prelude.foldr cons nil