Skip to content

Commit

Permalink
Add overall docs for unfold module
Browse files Browse the repository at this point in the history
Also remove some exports which may not be required for using unfolds
with streams.
  • Loading branch information
harendra-kumar committed Mar 26, 2021
1 parent ea6f2fb commit 7a1463d
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 65 deletions.
1 change: 1 addition & 0 deletions Changelog.md
Expand Up @@ -36,6 +36,7 @@
* New encoding/decoding routines, `encodeUtf8'`, `encodeLatin1'`, `decodeUtf8'`,
are added, these routines fail when they encounter any invalid characters.
* Several new functions added to `Streamly.Data.Fold`
* Several Unfold routines and combinators added to `Streamly.Data.Unfold`.

### Bug Fixes

Expand Down
134 changes: 81 additions & 53 deletions src/Streamly/Data/Unfold.hs
Expand Up @@ -8,57 +8,93 @@
-- Stability : experimental
-- Portability : GHC
--
-- 'Unfold' type represents an effectful action that generates a stream of
-- values from a single starting value often called a seed value. Values can be
-- generated and /pulled/ from the 'Unfold' one at a time. It can also be
-- called a producer or a source of stream. It is a data representation of the
-- standard 'Streamly.Prelude.unfoldr' function. An 'Unfold' can be converted
-- into a stream type using 'Streamly.Prelude.unfold' by supplying the seed.
--
-- = Performance Notes
--
-- 'Unfold' representation is more efficient than using streams when combining
-- streams. 'Unfold' type allows multiple unfold actions to be composed into a
-- single unfold function in an efficient manner by enabling the compiler to
-- perform stream fusion optimization.
-- @Unfold m a b@ can be considered roughly equivalent to an action @a -> t m
-- b@ (where @t@ is a stream type). Instead of using an 'Unfold' one could just
-- use a function of the shape @a -> t m b@. However, working with stream types
-- like t'Streamly.SerialT' does not allow the compiler to perform stream fusion
-- optimization when merging, appending or concatenating multiple streams.
-- Even though stream based combinator have excellent performance, they are
-- much less efficient when compared to combinators using 'Unfold'. For
-- example, the 'Streamly.Prelude.concatMap' combinator which uses @a -> t m b@
-- (where @t@ is a stream type) to generate streams is much less efficient
-- compared to 'Streamly.Prelude.unfoldMany'.
--
-- On the other hand, transformation operations on stream types are as
-- efficient as transformations on 'Unfold'.
--
-- We should note that in some cases working with stream types may be more
-- convenient compared to working with the 'Unfold' type. However, if extra
-- performance boost is important then 'Unfold' based composition should be
-- preferred compared to stream based composition when merging or concatenating
-- streams.
--
-- = Programmer Notes
--
-- > import qualified Streamly.Data.Unfold as UF
--
-- More, not yet exposed, unfold combinators can be found in
-- "Streamly.Internal.Data.Unfold".

-- The stream types (e.g. t'Streamly.SerialT') can be considered as a special
-- case of 'Unfold' with no starting seed.
-- An 'Unfold' is a source or a producer of a stream of values. It takes a
-- seed value as an input and unfolds it into a sequence of values.
--
-- >>> import qualified Streamly.Data.Fold as Fold
-- >>> import qualified Streamly.Data.Unfold as Unfold
-- >>> import qualified Streamly.Prelude as Stream
--
-- For example, the 'fromList' Unfold generates a stream of values from a
-- supplied list. Unfolds can be converted to 'Streamly.Prelude.SerialT'
-- stream using the Stream.unfold operation.
--
-- >>> stream = Stream.unfold Unfold.fromList [1..100]
-- >>> Stream.sum stream
-- 5050
--
-- All the serial stream generation operations in "Streamly.Prelude"
-- can be expressed using unfolds:
--
-- > Stream.fromList = Stream.unfold Unfold.fromList [1..100]
--
-- Conceptually, an 'Unfold' is just like "Data.List.unfoldr". Let us write a
-- step function to unfold a list using "Data.List.unfoldr":
--
-- >>> :{
-- f [] = Nothing
-- f (x:xs) = Just (x, xs)
-- :}
--
-- >>> Data.List.unfoldr f [1,2,3]
-- [1,2,3]
--
-- Unfold.unfoldr is just the same, it uses the same step function:
--
-- >>> Stream.toList $ Stream.unfold (Unfold.unfoldr f) [1,2,3]
-- [1,2,3]
--
-- The input of an unfold can be transformed using 'lmap':
--
-- >>> u = Unfold.lmap (fmap (+1)) Unfold.fromList
-- >>> Stream.toList $ Stream.unfold u [1..5]
-- [2,3,4,5,6]
--
-- 'Unfold' streams can be transformed using transformation combinators. For
-- example, to retain only the first two elements of an unfold:
--
-- >>> u = Unfold.take 2 Unfold.fromList
-- >>> Stream.toList $ Stream.unfold u [1..100]
-- [1,2]
--
-- Multiple unfolds can be combined in several interesting ways. For example,
-- to generate nested looping as in imperative languages (also known as cross
-- product of the two streams):
--
-- >>> u1 = Unfold.lmap fst Unfold.fromList
-- >>> u2 = Unfold.lmap snd Unfold.fromList
-- >>> u = Unfold.crossWith (,) u1 u2
-- >>> Stream.toList $ Stream.unfold u ([1,2,3], [4,5,6])
-- [(1,4),(1,5),(1,6),(2,4),(2,5),(2,6),(3,4),(3,5),(3,6)]
--
-- Nested loops using unfolds provide C like performance due to complete stream
-- fusion.
--
-- Please see "Streamly.Internal.Data.Unfold" for additional @Pre-release@
-- functions.
--
-- = Unfolds vs. Streams
--
-- Unfolds' raison d'etre is their efficiency in nested stream operations due
-- to complete stream fusion. 'Streamly.Prelude.concatMap' or the 'Monad'
-- instance of streams use stream generation operations of the shape @a -> t m
-- b@ and then flatten the resulting stream. This implementation is more
-- powerful but does not allow for complete stream fusion. Unfolds provide
-- less powerful but more efficient 'Streamly.Prelude.unfoldMany', 'many' and
-- 'crossWith' operations as an alternative to a subset of use cases of
-- 'concatMap' and 'Applicative' stream operations.
--
-- "Streamly.Prelude" exports polymorphic stream generation operations that
-- provide the same functionality as unfolds in this module. Since unfolds can
-- be easily converted to streams, several modules in streamly provide only
-- unfolds for serial stream generation. We cannot use unfolds exclusively for
-- stream generation as they do not support concurrency.

module Streamly.Data.Unfold
(
-- * Unfold Type
Unfold

-- * Folding
, fold

-- * Unfolds
-- One to one correspondence with
-- "Streamly.Internal.Data.Stream.IsStream.Generate"
Expand Down Expand Up @@ -106,14 +142,6 @@ module Streamly.Data.Unfold

-- ** Nesting
, many

-- ** Resource Management
, finally
, bracket

-- ** Exceptions
, onException
, handle
)
where

Expand Down
52 changes: 47 additions & 5 deletions src/Streamly/Internal/Data/Unfold.hs
Expand Up @@ -8,6 +8,11 @@
-- Stability : experimental
-- Portability : GHC
--
-- To run the examples in this module:
--
-- >>> import qualified Streamly.Data.Fold as Fold
-- >>> import qualified Streamly.Internal.Data.Unfold as Unfold
--
-- = Unfolds and Streams
--
-- An 'Unfold' type is the same as the direct style 'Stream' type except that
Expand Down Expand Up @@ -72,6 +77,33 @@
-- representation. We can use that for composable folds in StreamK
-- representation.
--

-- = Performance Notes
--
-- 'Unfold' representation is more efficient than using streams when combining
-- streams. 'Unfold' type allows multiple unfold actions to be composed into a
-- single unfold function in an efficient manner by enabling the compiler to
-- perform stream fusion optimization.
-- @Unfold m a b@ can be considered roughly equivalent to an action @a -> t m
-- b@ (where @t@ is a stream type). Instead of using an 'Unfold' one could just
-- use a function of the shape @a -> t m b@. However, working with stream types
-- like t'Streamly.SerialT' does not allow the compiler to perform stream fusion
-- optimization when merging, appending or concatenating multiple streams.
-- Even though stream based combinator have excellent performance, they are
-- much less efficient when compared to combinators using 'Unfold'. For
-- example, the 'Streamly.Prelude.concatMap' combinator which uses @a -> t m b@
-- (where @t@ is a stream type) to generate streams is much less efficient
-- compared to 'Streamly.Prelude.unfoldMany'.
--
-- On the other hand, transformation operations on stream types are as
-- efficient as transformations on 'Unfold'.
--
-- We should note that in some cases working with stream types may be more
-- convenient compared to working with the 'Unfold' type. However, if extra
-- performance boost is important then 'Unfold' based composition should be
-- preferred compared to stream based composition when merging or concatenating
-- streams.

module Streamly.Internal.Data.Unfold
(
-- * Unfold Type
Expand Down Expand Up @@ -177,17 +209,19 @@ module Streamly.Internal.Data.Unfold
, concatMapM
, bind

-- ** Exceptions
-- ** Resource Management
, gbracket_
, gbracket
, before
, after_
, after
, onException
, finally_
, after_
, finally
, bracket_
, finally_
, bracket
, bracket_

-- ** Exceptions
, onException
, handle
)
where
Expand Down Expand Up @@ -326,6 +360,9 @@ swap = lmap Tuple.swap
-- @Fold m b c@, returns a monadic action @a -> m c@ representing the
-- application of the fold on the unfolded stream.
--
-- >>> Unfold.fold Fold.sum Unfold.fromList [1..100]
-- 5050
--
-- /Pre-release/
--
{-# INLINE_NORMAL fold #-}
Expand Down Expand Up @@ -544,6 +581,11 @@ fromIndicesM gen = Unfold step return
-- Filtering
-------------------------------------------------------------------------------

-- |
-- >>> u = Unfold.take 2 Unfold.fromList
-- >>> Unfold.fold Fold.toList u [1..100]
-- [1,2]
--
{-# INLINE_NORMAL take #-}
take :: Monad m => Int -> Unfold m a b -> Unfold m a b
take n (Unfold step1 inject1) = Unfold step inject
Expand Down
32 changes: 25 additions & 7 deletions src/Streamly/Internal/Data/Unfold/Type.hs
Expand Up @@ -5,6 +5,12 @@
-- Maintainer : streamly@composewell.com
-- Stability : experimental
-- Portability : GHC
--
-- To run the examples in this module:
--
-- >>> import qualified Streamly.Prelude as Stream
-- >>> import qualified Streamly.Data.Fold as Fold
-- >>> import qualified Streamly.Internal.Data.Unfold as Unfold

module Streamly.Internal.Data.Unfold.Type
( Unfold (..)
Expand Down Expand Up @@ -57,12 +63,6 @@ import Streamly.Internal.Data.Stream.StreamD.Step (Step(..))

import Prelude hiding (const, map, concatMap, zipWith)

-- $setup
-- >>> :m
-- >>> import qualified Streamly.Prelude as Stream
-- >>> import qualified Streamly.Data.Fold as Fold
-- >>> import qualified Streamly.Data.Unfold as Unfold

------------------------------------------------------------------------------
-- Monadic Unfolds
------------------------------------------------------------------------------
Expand Down Expand Up @@ -121,6 +121,14 @@ unfoldrM next = Unfold step pure

-- | Like 'unfoldrM' but uses a pure step function.
--
-- >>> :{
-- f [] = Nothing
-- f (x:xs) = Just (x, xs)
-- :}
--
-- >>> Unfold.fold Fold.toList (Unfold.unfoldr f) [1,2,3]
-- [1,2,3]
--
-- /Pre-release/
--
{-# INLINE unfoldr #-}
Expand All @@ -133,6 +141,10 @@ unfoldr step = unfoldrM (pure . step)

-- | Map a function on the input argument of the 'Unfold'.
--
-- >>> u = Unfold.lmap (fmap (+1)) Unfold.fromList
-- >>> Unfold.fold Fold.toList u [1..5]
-- [2,3,4,5,6]
--
-- @
-- lmap f = Unfold.many (Unfold.function f)
-- @
Expand Down Expand Up @@ -246,6 +258,12 @@ crossWithM f (Unfold step1 inject1) (Unfold step2 inject2) = Unfold step inject
--
-- > crossWith f = crossWithM (\b c -> return $ f b c)
--
-- >>> u1 = Unfold.lmap fst Unfold.fromList
-- >>> u2 = Unfold.lmap snd Unfold.fromList
-- >>> u = Unfold.crossWith (,) u1 u2
-- >>> Unfold.fold Fold.toList u ([1,2,3], [4,5,6])
-- [(1,4),(1,5),(1,6),(2,4),(2,5),(2,6),(3,4),(3,5),(3,6)]
--
-- /Pre-release/
{-# INLINE crossWith #-}
crossWith :: Monad m =>
Expand Down Expand Up @@ -507,7 +525,7 @@ zipWithM f (Unfold step1 inject1) (Unfold step2 inject2) = Unfold step inject
-- >>> square = fmap (\x -> x * x) Unfold.fromList
-- >>> cube = fmap (\x -> x * x * x) Unfold.fromList
-- >>> u = Unfold.zipWith (,) square cube
-- >>> Unfold.fold u Fold.toList [1..5]
-- >>> Unfold.fold Fold.toList u [1..5]
-- [(1,1),(4,8),(9,27),(16,64),(25,125)]
--
-- > zipWith f = zipWithM (\a b -> return $ f a b)
Expand Down

0 comments on commit 7a1463d

Please sign in to comment.