Streams represented as chains of function calls using Continuation Passing
Style (CPS), suitable for dynamically and recursively composing potentially
large number of streams. The K in StreamK stands for Kontinuation.
In addition to the combinators in this module, you can use operations from
Streamly.Data.Stream for StreamK as well by converting StreamK to Stream
(toStream), and vice-versa (fromStream). Please refer to
Streamly.Internal.Data.StreamK for more functions that have not yet been
released.
For documentation see the corresponding combinators in
Streamly.Data.Stream. Documentation has been omitted in this module unless
there is a difference worth mentioning or if the combinator does not exist
in Streamly.Data.Stream.
Fused vs CPS Streams
Unlike the statically fused operations in Streamly.Data.Stream, StreamK
operations are less efficient, involving a function call overhead for each
element, but they exhibit linear O(n) time complexity wrt to the number of
stream compositions. Therefore, they are suitable for dynamically composing
streams e.g. appending potentially infinite streams in recursive loops.
While fused streams can be used efficiently to process elements as small as
a single byte, CPS streams are typically used on bigger chunks of data to
avoid the larger overhead per element.
Overview
StreamK can be constructed like lists, except that they use nil instead of
'[]' and cons instead of :.
>>> import Streamly.Data.StreamK (StreamK)>>> import qualified Streamly.Data.Fold as Fold>>> import qualified Streamly.Data.Parser as Parser>>> import qualified Streamly.Data.Stream as Stream>>> import qualified Streamly.Data.StreamK as StreamK>>> import qualified Streamly.FileSystem.DirIO as Dir
Example3 expressions
>>> mk = StreamK.fromStream . Stream.fromList>>> un = Stream.toList . StreamK.toStream>>> effect n = print n >> return n
For APIs that have not been released yet.
Example3 expressions
>>> import qualified Streamly.Internal.FileSystem.Path as Path>>> import qualified Streamly.Internal.Data.StreamK as StreamK>>> import qualified Streamly.Internal.FileSystem.DirIO as Dir
Semigroup (StreamKma)Defined in streamly-core-0.3.1 · Streamly.Internal.Data.StreamK.Type
Monoid (StreamKma)Defined in streamly-core-0.3.1 · Streamly.Internal.Data.StreamK.Type
typeItem (StreamKIdentitya) = aDefined in streamly-core-0.3.1 · Streamly.Internal.Data.StreamK.Type
Construction
0 declarations
Primitives
Primitives to construct a stream from pure values or monadic actions.
All other stream construction and generation combinators described later
can be expressed in terms of these primitives. However, the special
versions provided in this module can be much more efficient in some
cases. Users can create custom combinators using these primitives.
Unlike the operations in Streamly.Data.Stream, these operations can
be used to dynamically compose large number of streams e.g. using the
concatMapWith and mergeMapWith operations. They have a linear O(n)
time complexity wrt to the number of streams being composed.
Unlike the fused Streamly.Data.Stream append, StreamK append can be used
at scale, recursively, with linear performance:
Example1 expression
>>> cycle xs = let ys = xs `StreamK.append` ys in ys
concatMapWithappend (same as concatMap) flattens a stream of streams in a
depth-first manner i.e. it yields each stream fully and then the next and so
on. Given a stream of three streams:
Because of right association, the first stream yields as many items as the
next two streams combined.
Be careful when refactoring code involving a chain of three or more
interleave operations as it is not associative i.e. right associated code
may not produce the same result as left associated. This is a direct
consequence of the disbalance of scheduling in the previous example. If left
associated the above example would produce:
Note: Use concatMap based interleaving instead of the binary operator to
interleave more than two streams to avoid associativity issues.
concatMapWithinterleave flattens a stream of streams using interleave
in a right associative manner. Given a stream of three streams:
1. [1,2,3]
2. [4,5,6]
3. [7,8,9]
The resulting sequence is [1,4,2,7,3,5,8,6,9].
For this reason, the right associated flattening with interleave can work
with infinite number of streams without opening too many streams at the same
time. Each stream is consumed twice as much as the next one; if we are
combining an infinite number of streams of size n then at most log n
streams will be opened at any given time, because the first stream will
finish by the time the stream after log n th stream is opened.
Merging of n streams can be performed by combining the streams pair
wise using mergeMapWith to give O(n * log n) time complexity. If used
with concatMapWith it will have O(n^2) performance.
Zipping of n streams can be performed by combining the streams pair
wise using mergeMapWith with O(n * log n) time complexity. If used
with concatMapWith it will have O(n^2) performance.
>>> crossWith f m1 m2 = fmap f m1 `StreamK.crossApply` m2
Note that the second stream is evaluated multiple times.
Stream of streams
12 declarations
Some useful idioms:
Example3 expressions
>>> concatFoldableWith f = Prelude.foldr f StreamK.nil>>> concatMapFoldableWith f g = Prelude.foldr (f . g) StreamK.nil>>> concatForFoldableWith f xs g = Prelude.foldr (f . g) StreamK.nil xs
Perform a concatMap using a specified concat strategy. The first
argument specifies a merge or concat function that is used to merge the
streams generated by the map function.
For example, interleaving n streams in a left biased manner:
Map a stream generating function on each element of a stream and
concatenate the results. This is the same as the bind function of the monad
instance. It is just a flipped concatMap but more convenient to use for
nested use case, feels like an imperative for loop.
While concatFor flattens a stream of streams in a depth first manner,
bfsConcatFor flattens it in a breadth-first manner. It yields one item
from the first stream, then one item from the next stream and so on. In
nested loops it has the effect of prioritizing the new outer loop iteration
over the inner loops, thus inverting the looping.
Given a stream of three streams:
1. [1,2,3]
2. [4,5,6]
3. [7,8,9]
The resulting stream is (1,4,7),(2,5,8),(3,6,9). The parenthesis are added
to emphasize the iterations.
Compare with concatForWithinterleave which explores the depth
exponentially more compared to the breadth, such that each stream yields
twice as many items compared to the next stream.
See also the equivalent fused version Data.Stream.unfoldEachInterleave.
fairConcatFor is like concatFor but traverses the depth and breadth of
nesting equally. Therefore, the outer and the inner loops in a nested loop
get equal priority. It can be used to nest infinite streams without starving
outer streams due to inner ones.
Given a stream of three streams:
1. [1,2,3]
2. [4,5,6]
3. [7,8,9]
Here, outer loop is the stream of streams and the inner loops are the
individual streams. The traversal sweeps the diagonals in the above grid to
give equal chance to outer and inner loops. The resulting stream is
(1),(2,4),(3,5,7),(6,8),(9), diagonals are parenthesized for emphasis.
Multiple streams nest like for loops. The result is a cross product of the
streams. However, the ordering of the results of the cross product is such
that each stream gets consumed equally. In other words, inner iterations of
a nested loop get the same priority as the outer iterations. Inner
iterations do not finish completely before the outer iterations start.
If we look at the cross product of [1,2,3], [4,5,6], the streams being
combined using fairConcatFor are the following sequential loop iterations:
(1,4) (1,5) (1,6) -- first iteration of the outer loop
(2,4) (2,5) (2,6) -- second iteration of the outer loop
(3,4) (3,5) (3,6) -- third iteration of the outer loop
The result is a triangular or diagonal traversal of these iterations:
If one of the two interleaved streams does not produce an output at all and
continues forever then the other stream will never get scheduled. This is
because a stream is unscheduled only after it produces an output. This can
lead to non-terminating programs, an example is provided below.
Example1 expression
>>> :{oddsIf x = mk (if x then [1,3..] else [2,4..])filterEven x = if even x then StreamK.fromPure x else StreamK.nil:}
The evens function does not terminate because, when r is True, the nested
concatFor is a non-productive infinite loop, therefore, the outer loop
never gets a chance to generate the False value.
But the following refactoring of the above code works as expected:
This works because in mixed both the streams being interleaved are
productive.
Care should be taken how you write your program, keep in mind the scheduling
implications. To avoid such scheduling problems in serial interleaving, you
can use concurrent interleaving instead i.e. parFairConcatFor. Due to
concurrent threads the other branch will make progress even if one is an
infinite loop producing nothing.
Logic Programming
Streamly provides all operations for logic programming. It provides
functionality equivalent to LogicT type from the logict package.
The MonadLogic operations can be implemented using the available stream
operations. For example, uncons is msplit, interleave corresponds to
the interleave operation of MonadLogic, fairConcatFor is the
fair bind (>>-) operation.
Related Operations
concatForWithinterleave is another way to interleave two serial
streams. In this case, the inner loop iterations get exponentially more
priority over the outer iterations of the nested loop. This is biased
towards the inner loops - this is exactly how the logic-t and list-t
implementation of fair bind works.
Combine streams in pairs using a binary combinator, the resulting streams
are then combined again in pairs recursively until we get to a single
combined stream. The composition would thus form a binary tree.
For example, 'mergeMapWith interleave' gives the following result:
If the number of streams being combined is not a power of 2, the binary tree
composed by mergeMapWith is not balanced, therefore, the output may not look
fairly interleaved, it will be biased towards the unpaired streams:
However, this combinator uses a parser to first split the input stream into
down and up sorted segments and then merges them to optimize sorting when
pre-sorted sequences exist in the input stream.
O(n) space
Exceptions
1 declaration
Please note that Stream type does not observe any exceptions from
the consumer of the stream whereas StreamK does.
Like Streamly.Data.Stream.Streamly.Data.Stream.handle but with one
significant difference, this function observes exceptions from the consumer
of the stream as well.
You can also convert StreamK to Stream and use exception handling from
Stream module:
Example1 expression
>>> handle f s = StreamK.fromStream $ Stream.handle (\e -> StreamK.toStream (f e)) (StreamK.toStream s)
Resource Management
1 declaration
Please note that Stream type does not observe any exceptions from
the consumer of the stream whereas StreamK does.
Like Streamly.Data.Stream.Streamly.Data.Stream.bracketIO but with one
significant difference, this function observes exceptions from the consumer
of the stream as well. Therefore, it cleans up the resource promptly when
the consumer encounters an exception.
You can also convert StreamK to Stream and use resource handling from
Stream module: