on master help sign in
Add resumable unfolds for efficient nested parsing

Instead of using a closed loop we can stop an unfold and then resume it later. This allows us to break the loop and restart it at some other point. For example, we can parse a block from an input stream from a file handle or a socket and then return the socket/handle plus nay buffered data (due to backtracking) so that we can resume reading from it later on after doing some processing.

commit22c83f40fd02225911c8069c117214483bd344d9 authorHarendra Kumar <harendra@composewell.com> dateSun 7 Feb 2021, 04:04 (6 years ago) parente686510
9 files changed+604−9
benchmark/Streamly/Benchmark/Data/Parser.hs+32−2 benchmark/Streamly/Benchmark/Data/Parser/ParserD.hs+32−2 src/Streamly/Internal/Data/Array/Foreign.hs+7 src/Streamly/Internal/Data/Array/Foreign/Mut/Types.hs+17−5 src/Streamly/Internal/Data/Unfold/Resume.hs+229 src/Streamly/Internal/Data/Unfold/Resume/Type.hs+168 src/Streamly/Internal/Data/Unfold/Source.hs+84 streamly.cabal+3 test/Streamly/Test/Data/Parser/ParserD.hs+32
benchmark/Streamly/Benchmark/Data/Parser.hs+32−2
@@ -35,9 +35,12 @@ import qualified Data.Foldable as F
3535import qualified Control.Applicative as AP
3636import qualified Streamly.FileSystem.Handle as Handle
3737import qualified Streamly.Prelude as S
38+import qualified Streamly.Internal.Data.Array.Foreign as Array
3839import qualified Streamly.Internal.Data.Fold as FL
3940import qualified Streamly.Internal.Data.Parser as PR
4041import qualified Streamly.Internal.Data.Stream.IsStream as IP
42+import qualified Streamly.Internal.Data.Unfold.Resume as UnfoldR
43+import qualified Streamly.Internal.Data.Unfold.Source as Source
4144
4245import Gauge hiding (env)
4346import Streamly.Prelude (SerialT)
@@ -235,6 +238,22 @@ parseManyChunksOfSum n inh =
235238 (PR.fromFold $ FL.takeLE n FL.sum)
236239 (S.unfold Handle.read inh)
237240
241+-------------------------------------------------------------------------------
242+-- Parsing with unfolds
243+-------------------------------------------------------------------------------
244+
245+{-# INLINE parseManyUnfoldArrays #-}
246+parseManyUnfoldArrays :: Int -> [Array.Array Int] -> IO ()
247+parseManyUnfoldArrays count arrays = do
248+ let src = Source.source (arrays, Nothing)
249+ let parser = PR.fromFold (FL.takeLE count FL.drain)
250+ let readSrc =
251+ Source.read
252+ $ UnfoldR.concat UnfoldR.fromList Array.readResumable
253+ let streamParser =
254+ UnfoldR.simplify (UnfoldR.parseMany parser readSrc)
255+ S.drain $ S.unfold streamParser src
256+
238257-------------------------------------------------------------------------------
239258-- Parsers in which -fspec-constr-recursive=16 is problematic
240259-------------------------------------------------------------------------------
@@ -338,6 +357,14 @@ o_1_space_filesystem env =
338357 $ \inh _ -> inline parseManyChunksOfSum 1 inh
339358 ]
340359
360+o_1_space_serial_unfold :: Int -> [Array.Array Int] -> [Benchmark]
361+o_1_space_serial_unfold bound arrays =
362+ [ bench "parseMany/Unfold/1000 arrays/1 parse"
363+ $ nfIO $ parseManyUnfoldArrays bound arrays
364+ , bench "parseMany/Unfold/1000 arrays/100000 parses"
365+ $ nfIO $ parseManyUnfoldArrays 1 arrays
366+ ]
367+
341368o_n_heap_serial :: Int -> [Benchmark]
342369o_n_heap_serial value =
343370 [
@@ -370,14 +397,17 @@ main :: IO ()
370397main = do
371398 (value, cfg, benches) <- parseCLIOpts defaultStreamSize
372399 env <- mkHandleBenchEnv
373− value `seq` runMode (mode cfg) cfg benches (allBenchmarks env value)
400+ arrays <- IP.toList $ IP.arraysOf 100 $ sourceUnfoldrM value 0
401+ value `seq` runMode (mode cfg) cfg benches (allBenchmarks env value arrays)
374402
375403 where
376404
377− allBenchmarks env value =
405+ allBenchmarks env value arrays =
378406 [ bgroup (o_1_space_prefix moduleName) (o_1_space_serial value)
379407 , bgroup
380408 (o_1_space_prefix moduleName ++ "/filesystem")
381409 (o_1_space_filesystem env)
410+ , bgroup (o_1_space_prefix moduleName)
411+ (o_1_space_serial_unfold value arrays)
382412 , bgroup (o_n_heap_prefix moduleName) (o_n_heap_serial value)
383413 ]
benchmark/Streamly/Benchmark/Data/Parser/ParserD.hs+32−2
@@ -26,9 +26,12 @@ import qualified Data.Traversable as TR
2626import qualified Data.Foldable as F
2727import qualified Control.Applicative as AP
2828import qualified Streamly.Prelude as S
29+import qualified Streamly.Internal.Data.Array.Foreign as Array
2930import qualified Streamly.Internal.Data.Fold as FL
3031import qualified Streamly.Internal.Data.Parser.ParserD as PR
3132import qualified Streamly.Internal.Data.Stream.IsStream as IP
33+import qualified Streamly.Internal.Data.Unfold.Resume as UnfoldR
34+import qualified Streamly.Internal.Data.Unfold.Source as Source
3235
3336import Gauge
3437import Streamly.Prelude (SerialT, MonadAsync, IsStream)
@@ -209,6 +212,22 @@ parseManyGroupsRolling :: MonadThrow m => Bool -> SerialT m Int -> m ()
209212parseManyGroupsRolling b =
210213 IP.drain . IP.parseManyD (PR.groupByRolling (\_ _ -> b) FL.drain)
211214
215+-------------------------------------------------------------------------------
216+-- Parsing with unfolds
217+-------------------------------------------------------------------------------
218+
219+{-# INLINE parseManyUnfoldArrays #-}
220+parseManyUnfoldArrays :: Int -> [Array.Array Int] -> IO ()
221+parseManyUnfoldArrays count arrays = do
222+ let src = Source.source (arrays, Nothing)
223+ let parser = PR.fromFold (FL.takeLE count FL.drain)
224+ let readSrc =
225+ Source.read
226+ $ UnfoldR.concat UnfoldR.fromList Array.readResumable
227+ let streamParser =
228+ UnfoldR.simplify (UnfoldR.parseManyD parser readSrc)
229+ S.drain $ S.unfold streamParser src
230+
212231-------------------------------------------------------------------------------
213232-- Parsers in which -fspec-constr-recursive=16 is problematic
214233-------------------------------------------------------------------------------
@@ -305,6 +324,14 @@ o_1_space_serial_nested value =
305324 $ (parseManyGroupsRolling True)
306325 ]
307326
327+o_1_space_serial_unfold :: Int -> [Array.Array Int] -> [Benchmark]
328+o_1_space_serial_unfold bound arrays =
329+ [ bench "parseMany/Unfold/1000 arrays/1 parse"
330+ $ nfIO $ parseManyUnfoldArrays bound arrays
331+ , bench "parseMany/Unfold/1000 arrays/100000 parses"
332+ $ nfIO $ parseManyUnfoldArrays 1 arrays
333+ ]
334+
308335o_n_heap_serial :: Int -> [Benchmark]
309336o_n_heap_serial value =
310337 [
@@ -334,14 +361,17 @@ o_n_space_serial value =
334361main :: IO ()
335362main = do
336363 (value, cfg, benches) <- parseCLIOpts defaultStreamSize
337− value `seq` runMode (mode cfg) cfg benches (allBenchmarks value)
364+ arrays <- IP.toList $ IP.arraysOf 100 $ sourceUnfoldrM value 0
365+ value `seq` runMode (mode cfg) cfg benches (allBenchmarks value arrays)
338366
339367 where
340368
341− allBenchmarks value =
369+ allBenchmarks value arrays =
342370 [ bgroup (o_1_space_prefix moduleName) (o_1_space_serial value)
343371 , bgroup (o_1_space_prefix moduleName) (o_1_space_serial_spanning value)
344372 , bgroup (o_1_space_prefix moduleName) (o_1_space_serial_nested value)
373+ , bgroup (o_1_space_prefix moduleName)
374+ (o_1_space_serial_unfold value arrays)
345375 , bgroup (o_n_heap_prefix moduleName) (o_n_heap_serial value)
346376 , bgroup (o_n_space_prefix moduleName) (o_n_space_serial value)
347377 ]
src/Streamly/Internal/Data/Array/Foreign.hs+7
@@ -68,6 +68,7 @@ module Streamly.Internal.Data.Array.Foreign
6868 , A.toStream
6969 , A.toStreamRev
7070 , read
71+ , readResumable
7172 , unsafeRead
7273 , A.readRev
7374 -- , readChunksOf
@@ -170,6 +171,7 @@ import qualified Streamly.Internal.Data.Stream.Prelude as P
170171import qualified Streamly.Internal.Data.Stream.Serial as Serial
171172import qualified Streamly.Internal.Data.Stream.StreamD as D
172173import qualified Streamly.Internal.Data.Unfold as Unfold
174+import qualified Streamly.Internal.Data.Unfold.Resume.Type as UnfoldR
173175import qualified Streamly.Internal.Ring.Foreign as RB
174176
175177-------------------------------------------------------------------------------
@@ -212,6 +214,11 @@ fromStream = P.foldOnce A.write
212214read :: forall m a. (Monad m, Storable a) => Unfold m (Array a) a
213215read = Unfold.lmap A.unsafeThaw MA.read
214216
217+{-# INLINE_NORMAL readResumable #-}
218+readResumable :: forall m a. (Monad m, Storable a) =>
219+ UnfoldR.Unfold m (Array a) a
220+readResumable = UnfoldR.lmap A.unsafeThaw A.unsafeFreeze MA.readResumable
221+
215222-- | Unfold an array into a stream, does not check the end of the array, the
216223-- user is responsible for terminating the stream within the array bounds. For
217224-- high performance application where the end condition can be determined by
src/Streamly/Internal/Data/Array/Foreign/Mut/Types.hs+17−5
@@ -78,6 +78,7 @@ module Streamly.Internal.Data.Array.Foreign.Mut.Types
7878 , ReadUState
7979 , read
8080 , readRev
81+ , readResumable
8182 , flattenArrays
8283 , flattenArraysRev
8384
@@ -146,6 +147,7 @@ import qualified GHC.Exts as Exts
146147import qualified Streamly.Internal.Data.Fold.Types as FL
147148import qualified Streamly.Internal.Data.Stream.StreamD.Type as D
148149import qualified Streamly.Internal.Data.Stream.StreamK.Type as K
150+import qualified Streamly.Internal.Data.Unfold.Resume as UnfoldR
149151import qualified Streamly.Internal.Foreign.Malloc as Malloc
150152
151153import Prelude hiding (length, foldr, read, unlines, splitAt)
@@ -498,12 +500,12 @@ data ReadUState a = ReadUState
498500 {-# UNPACK #-} !(ForeignPtr a) -- foreign ptr with end of array pointer
499501 {-# UNPACK #-} !(Ptr a) -- current pointer
500502
501−-- | Unfold an array into a stream.
503+-- | Resumable unfold of an array.
502504--
503−-- @since 0.7.0
504−{-# INLINE_NORMAL read #-}
505−read :: forall m a. (Monad m, Storable a) => Unfold m (Array a) a
506−read = Unfold step inject
505+{-# INLINE_NORMAL readResumable #-}
506+readResumable :: forall m a. (Monad m, Storable a) =>
507+ UnfoldR.Unfold m (Array a) a
508+readResumable = UnfoldR.Unfold step inject extract
507509 where
508510
509511 inject (Array (ForeignPtr start contents) (Ptr end) _) =
@@ -524,6 +526,16 @@ read = Unfold step inject
524526 return $ D.Yield x
525527 (ReadUState fp (p `plusPtr` sizeOf (undefined :: a)))
526528
529+ extract (ReadUState (ForeignPtr end contents) (Ptr p)) =
530+ return $ Just $ Array (ForeignPtr p contents) (Ptr end) (Ptr end)
531+
532+-- | Unfold an array into a stream.
533+--
534+-- @since 0.7.0
535+{-# INLINE_NORMAL read #-}
536+read :: forall m a. (Monad m, Storable a) => Unfold m (Array a) a
537+read = UnfoldR.simplify readResumable
538+
527539-- | Unfold an array into a stream in reverse order.
528540--
529541-- /Internal/
src/Streamly/Internal/Data/Unfold/Resume.hsnew file+229
@@ -0,0 +1,229 @@
1+-- |
2+-- Module : Streamly.Internal.Data.Unfold.Resume
3+-- Copyright : (c) 2021 Composewell Technologies
4+-- License : BSD-3-Clause
5+-- Maintainer : streamly@composewell.com
6+-- Stability : experimental
7+-- Portability : GHC
8+--
9+-- Unfolds in "Streamly.Internal.Data.Unfold" module run to completion, they
10+-- cannot be stopped and then resumed. In contrast, the unfolds in this module
11+-- allow extraction of the intermediate state which can be used to resume the
12+-- unfold later on.
13+--
14+-- For simple cases the seed itself can be used to represent the intermediate
15+-- state as well. For example, if we are iterating through a list or an array
16+-- we can return the remaining list or the remaining array slice as the
17+-- state and resume from it later.
18+--
19+-- In general, the intermediate state could be different than the seed. Also,
20+-- when unfolds are nested, for example using 'concat', then each nesting layer
21+-- would have its own state.
22+--
23+-- In imperative terms, an unfold is like a for loop (or an iterator), the
24+-- state of the unfold is the state of the loop. The state of the 'concat' of
25+-- unfolds is like the state of each loop in the nesting of loops.
26+--
27+
28+module Streamly.Internal.Data.Unfold.Resume
29+ ( Unfold (..)
30+
31+ -- * Converting
32+ , simplify
33+
34+ -- * Unfolds
35+ , nil
36+ , nilM
37+ , unfoldrM
38+ , fromStreamD
39+ , fromList
40+
41+ -- * Combinators
42+ , concat
43+ , parse
44+ , parseMany
45+ , parseManyD
46+ )
47+where
48+
49+#include "inline.hs"
50+
51+import Control.Exception (assert)
52+import Control.Monad.Catch (MonadThrow, throwM)
53+import GHC.Exts (SpecConstrAnnotation(..))
54+import GHC.Types (SPEC(..))
55+import Streamly.Internal.Data.Parser.ParserD (ParseError(..), Step(..))
56+import Streamly.Internal.Data.Stream.StreamD.Step (Step(..))
57+import Streamly.Internal.Data.Stream.StreamD.Type (Stream(..))
58+import Streamly.Internal.Data.SVar (defState)
59+import Streamly.Internal.Data.Unfold.Source (Source)
60+
61+import qualified Streamly.Internal.Data.Parser.ParserD as ParserD
62+import qualified Streamly.Internal.Data.Parser.ParserK.Types as ParserK
63+import qualified Streamly.Internal.Data.Unfold.Types as Unfold
64+import qualified Streamly.Internal.Data.Unfold.Source as Source
65+
66+import Streamly.Internal.Data.Unfold.Resume.Type
67+import Prelude hiding (concat)
68+
69+-- XXX We should write unfolds as resumable unfolds where possible and define
70+-- the regular unfolds using "simplify".
71+--
72+-------------------------------------------------------------------------------
73+-- Converting to regular unfolds
74+-------------------------------------------------------------------------------
75+
76+-- | Simplify a resumable unfold to a regular unfold.
77+--
78+-- /Internal/
79+{-# INLINE simplify #-}
80+simplify :: Unfold m a b -> Unfold.Unfold m a b
81+simplify (Unfold step inject _) = Unfold.Unfold step inject
82+
83+-------------------------------------------------------------------------------
84+-- Unfolds
85+-------------------------------------------------------------------------------
86+
87+-- | Convert a StreamD stream into an Unfold.
88+--
89+-- /Internal/
90+{-# INLINE_NORMAL fromStreamD #-}
91+fromStreamD :: Monad m => Unfold m (Stream m a) a
92+fromStreamD = Unfold step return (return . Just)
93+ where
94+
95+ {-# INLINE_LATE step #-}
96+ step (UnStream step1 state1) = do
97+ r <- step1 defState state1
98+ return $ case r of
99+ Yield x s -> Yield x (Stream step1 s)
100+ Skip s -> Skip (Stream step1 s)
101+ Stop -> Stop
102+
103+-------------------------------------------------------------------------------
104+-- Parsing
105+-------------------------------------------------------------------------------
106+
107+-- GHC parser does not accept {-# ANN type [] NoSpecConstr #-}, so we need
108+-- to make a newtype.
109+{-# ANN type List NoSpecConstr #-}
110+newtype List a = List {getList :: [a]}
111+
112+{-# INLINE_NORMAL parseD #-}
113+parseD
114+ :: MonadThrow m
115+ => ParserD.Parser m a b
116+ -> Unfold m (Source s a) a
117+ -> Source s a
118+ -> m (b, Maybe (Source s a))
119+parseD
120+ (ParserD.Parser pstep initial extract)
121+ (Unfold ustep uinject uextract)
122+ seed = do
123+
124+ state <- uinject seed
125+ initial >>= go SPEC state (List [])
126+
127+ where
128+
129+ -- XXX currently we are using a dumb list based approach for backtracking
130+ -- buffer. This can be replaced by a sliding/ring buffer using Data.Array.
131+ -- That will allow us more efficient random back and forth movement.
132+ {-# INLINE go #-}
133+ go !_ st buf !pst = do
134+ r <- ustep st
135+ case r of
136+ Yield x s -> do
137+ pRes <- pstep pst x
138+ case pRes of
139+ Partial 0 pst1 -> go SPEC s (List []) pst1
140+ Partial n pst1 -> do
141+ assert (n <= length (x:getList buf)) (return ())
142+ let src0 = Prelude.take n (x:getList buf)
143+ src = Prelude.reverse src0
144+ gobuf SPEC s (List []) (List src) pst1
145+ Continue 0 pst1 -> go SPEC s (List (x:getList buf)) pst1
146+ Continue n pst1 -> do
147+ assert (n <= length (x:getList buf)) (return ())
148+ let (src0, buf1) = splitAt n (x:getList buf)
149+ src = Prelude.reverse src0
150+ gobuf SPEC s (List buf1) (List src) pst1
151+ Done n b -> do
152+ assert (n <= length (x:getList buf)) (return ())
153+ let src0 = Prelude.take n (x:getList buf)
154+ src = Prelude.reverse src0
155+ s1 <- uextract s
156+ return (b, fmap (Source.unread src) s1)
157+ Error err -> throwM $ ParseError err
158+ Skip s -> go SPEC s buf pst
159+ Stop -> do
160+ b <- extract pst
161+ -- XXX we should return the remaining buffer
162+ return (b, Nothing)
163+
164+ gobuf !_ s buf (List []) !pst = go SPEC s buf pst
165+ gobuf !_ s buf (List (x:xs)) !pst = do
166+ pRes <- pstep pst x
167+ case pRes of
168+ Partial 0 pst1 ->
169+ gobuf SPEC s (List []) (List xs) pst1
170+ Partial n pst1 -> do
171+ assert (n <= length (x:getList buf)) (return ())
172+ let src0 = Prelude.take n (x:getList buf)
173+ src = Prelude.reverse src0 ++ xs
174+ gobuf SPEC s (List []) (List src) pst1
175+ Continue 0 pst1 ->
176+ gobuf SPEC s (List (x:getList buf)) (List xs) pst1
177+ Continue n pst1 -> do
178+ assert (n <= length (x:getList buf)) (return ())
179+ let (src0, buf1) = splitAt n (x:getList buf)
180+ src = Prelude.reverse src0 ++ xs
181+ gobuf SPEC s (List buf1) (List src) pst1
182+ Done n b -> do
183+ assert (n <= length (x:getList buf)) (return ())
184+ let src0 = Prelude.take n (x:getList buf)
185+ src = Prelude.reverse src0
186+ s1 <- uextract s
187+ return (b, fmap (Source.unread src) s1)
188+ Error err -> throwM $ ParseError err
189+
190+-- | Parse a resumable unfold returning the parsed value and the state of the
191+-- unfold which can be resumed later to unfold the remaining values.
192+--
193+-- /Internal/
194+{-# INLINE [3] parse #-}
195+parse
196+ :: MonadThrow m
197+ => ParserK.Parser m a b
198+ -> Unfold m (Source s a) a
199+ -> Source s a
200+ -> m (b, Maybe (Source s a))
201+parse = parseD . ParserK.fromParserK
202+
203+-------------------------------------------------------------------------------
204+-- Nested parsing
205+-------------------------------------------------------------------------------
206+
207+{-# INLINE parseManyD #-}
208+parseManyD :: MonadThrow m =>
209+ ParserD.Parser m a b -> Unfold m (Source x a) a -> Unfold m (Source x a) b
210+parseManyD parser reader = Unfold step inject return
211+
212+ where
213+
214+ inject = return . Just
215+
216+ {-# INLINE_LATE step #-}
217+ step (Just src) = do
218+ (b, s1) <- parseD parser reader src
219+ return $ Yield b s1
220+ step Nothing = return Stop
221+
222+-- | Apply a parser repeatedly on an unfold to generate an unfold of parsed
223+-- values.
224+--
225+-- /Internal/
226+{-# INLINE parseMany #-}
227+parseMany :: MonadThrow m =>
228+ ParserK.Parser m a b -> Unfold m (Source x a) a -> Unfold m (Source x a) b
229+parseMany parser = parseManyD (ParserK.fromParserK parser)
src/Streamly/Internal/Data/Unfold/Resume/Type.hsnew file+168
@@ -0,0 +1,168 @@
1+-- |
2+-- Module : Streamly.Internal.Data.Unfold.Resume.Type
3+-- Copyright : (c) 2021 Composewell Technologies
4+-- License : BSD-3-Clause
5+-- Maintainer : streamly@composewell.com
6+-- Stability : experimental
7+-- Portability : GHC
8+--
9+-- Resumable unfolds. See "Streamly.Internal.Data.Unfold.Resume" for
10+-- introduction.
11+--
12+
13+module Streamly.Internal.Data.Unfold.Resume.Type
14+ (
15+ -- * Type
16+ Unfold (..)
17+
18+ -- * Unfolds
19+ , nil
20+ , nilM
21+ , unfoldrM
22+ , fromList
23+
24+ -- * Mapping
25+ , lmap
26+
27+ -- * Nesting
28+ , concat
29+ )
30+where
31+
32+#include "inline.hs"
33+
34+import Fusion.Plugin.Types (Fuse(..))
35+import Streamly.Internal.Data.Stream.StreamD.Step (Step(..))
36+import Prelude hiding (concat)
37+
38+------------------------------------------------------------------------------
39+-- Type
40+------------------------------------------------------------------------------
41+
42+-- | An @Unfold m a b@ is a generator of a stream of values of type @b@ from a
43+-- seed of type 'a' in 'Monad' @m@.
44+--
45+-- /Internal/
46+
47+data Unfold m a b =
48+ -- | @Unfold step inject extract@
49+ forall s. Unfold (s -> m (Step s b)) (a -> m s) (s -> m (Maybe a))
50+
51+------------------------------------------------------------------------------
52+-- Unfolds
53+------------------------------------------------------------------------------
54+
55+-- XXX we do not need a Maybe, we can just use undefined or error. There is no
56+-- state to extract in this case, and it should not be used.
57+--
58+-- However, in certain cases we may not be able to extract, e.g. if we are
59+-- unfolding a Map, we may not be able to resume reading a Map. In such cases
60+-- it may be useful to return a Nothing. Or maybe we do not allow unfolding
61+-- such structures?
62+{-# INLINE nilM #-}
63+nilM :: Monad m => (a -> m c) -> Unfold m a b
64+nilM f = Unfold step return (\_ -> return Nothing)
65+
66+ where
67+
68+ {-# INLINE_LATE step #-}
69+ step x = f x >> return Stop
70+
71+{-# INLINE nil #-}
72+nil :: Monad m => Unfold m a b
73+nil = nilM (\_ -> return ())
74+
75+{-# INLINE unfoldrM #-}
76+unfoldrM :: Monad m => (a -> m (Maybe (b, a))) -> Unfold m a b
77+unfoldrM next = Unfold step return (return . Just)
78+ where
79+ {-# INLINE_LATE step #-}
80+ step st = do
81+ r <- next st
82+ return $ case r of
83+ Just (x, s) -> Yield x s
84+ Nothing -> Stop
85+
86+-- | Convert a list of pure values to a 'Stream'
87+--
88+-- /Internal/
89+{-# INLINE_LATE fromList #-}
90+fromList :: Monad m => Unfold m [a] a
91+fromList = Unfold step inject (return . Just)
92+ where
93+ inject = return
94+ {-# INLINE_LATE step #-}
95+ step (x:xs) = return $ Yield x xs
96+ step [] = return Stop
97+
98+------------------------------------------------------------------------------
99+-- Mapping
100+------------------------------------------------------------------------------
101+
102+-- | We can only lmap between two types that can be interconverted without
103+-- loss.
104+--
105+-- /Internal/
106+{-# INLINE_NORMAL lmap #-}
107+lmap :: Functor m => (a -> c) -> (c -> a) -> Unfold m c b -> Unfold m a b
108+lmap f g (Unfold ustep uinject uextract) =
109+ Unfold ustep (uinject . f) (fmap (fmap g) . uextract)
110+
111+------------------------------------------------------------------------------
112+-- Nesting
113+------------------------------------------------------------------------------
114+
115+{-# ANN type ConcatState Fuse #-}
116+data ConcatState s1 s2 = ConcatOuter s1 | ConcatInner s1 s2
117+
118+-- | Apply the second unfold to each output element of the first unfold and
119+-- flatten the output in a single stream.
120+--
121+-- /Internal/
122+--
123+{-# INLINE_NORMAL concat #-}
124+concat :: Monad m => Unfold m a b -> Unfold m b c -> Unfold m (a, Maybe b) c
125+concat (Unfold step1 inject1 extract1) (Unfold step2 inject2 extract2) =
126+ Unfold step inject extract
127+
128+ where
129+
130+ inject (x, Nothing) = do
131+ s <- inject1 x
132+ return $ ConcatOuter s
133+ inject (x, Just y) = do
134+ s1 <- inject1 x
135+ s2 <- inject2 y
136+ return $ ConcatInner s1 s2
137+
138+ {-# INLINE_LATE step #-}
139+ step (ConcatOuter st) = do
140+ r <- step1 st
141+ case r of
142+ Yield x s -> do
143+ innerSt <- inject2 x
144+ return $ Skip (ConcatInner s innerSt)
145+ Skip s -> return $ Skip (ConcatOuter s)
146+ Stop -> return Stop
147+
148+ step (ConcatInner ost ist) = do
149+ r <- step2 ist
150+ return $ case r of
151+ Yield x s -> Yield x (ConcatInner ost s)
152+ Skip s -> Skip (ConcatInner ost s)
153+ Stop -> Skip (ConcatOuter ost)
154+
155+ extract (ConcatOuter s1) = do
156+ r <- extract1 s1
157+ return $ case r of
158+ Nothing -> Nothing
159+ Just a -> Just (a, Nothing)
160+ extract (ConcatInner s1 s2) = do
161+ r1 <- extract1 s1
162+ case r1 of
163+ Nothing -> return Nothing
164+ Just a -> do
165+ r2 <- extract2 s2
166+ return $ case r2 of
167+ Nothing -> Just (a, Nothing)
168+ Just b -> Just (a, Just b)
src/Streamly/Internal/Data/Unfold/Source.hsnew file+84
@@ -0,0 +1,84 @@
1+-- |
2+-- Module : Streamly.Internal.Data.Unfold.Source
3+-- Copyright : (c) 2021 Composewell Technologies
4+-- License : BSD-3-Clause
5+-- Maintainer : streamly@composewell.com
6+-- Stability : experimental
7+-- Portability : GHC
8+--
9+-- A stream source which can be unfolded with some possibly buffered data.
10+-- Allows to 'unread' data i.e. push unused data back to the source. This is
11+-- useful in parsing applications with backtracking.
12+--
13+
14+module Streamly.Internal.Data.Unfold.Source
15+ ( Source
16+
17+ -- * Creation
18+ , source
19+
20+ -- * Reading
21+ , read
22+ , unread
23+ )
24+where
25+
26+#include "inline.hs"
27+
28+import Streamly.Internal.Data.Stream.StreamD.Step (Step(..))
29+import Streamly.Internal.Data.Unfold.Resume.Type (Unfold(..))
30+import Prelude hiding (read)
31+
32+-- | An unfold seed with some buffered data. It allows us to 'unread' or return
33+-- some data after reading it. Useful in backtracked parsing.
34+--
35+data Source a b = Source [b] a
36+
37+-- | Make a source from a seed value. The buffer would start as empty. You can
38+-- use 'unread' to add to the buffer.
39+--
40+-- /Internal/
41+source :: a -> Source a b
42+source = Source []
43+
44+-- | An unfold to read from a 'Source'. If the buffer has data then it is read
45+-- first otherwise the seed is unfolded using the supplied unfold.
46+--
47+-- /Internal/
48+{-# INLINE_NORMAL read #-}
49+read :: Monad m => Unfold m a b -> Unfold m (Source a b) b
50+read (Unfold step1 inject1 extract1) = Unfold step inject extract
51+
52+ where
53+
54+ inject (Source [] a) = do
55+ s <- inject1 a
56+ return $ Left s
57+ inject (Source xs a) = return $ Right (xs, a)
58+
59+ {-# INLINE_LATE step #-}
60+ step (Left s) = do
61+ r <- step1 s
62+ return $ case r of
63+ Yield x s1 -> Yield x (Left s1)
64+ Skip s1 -> Skip (Left s1)
65+ Stop -> Stop
66+ step (Right ([], _)) = error "Bug: unreachable"
67+ step (Right (x:[], a)) = do
68+ s <- inject1 a
69+ return $ Yield x (Left s)
70+ step (Right (x:xs, a)) = return $ Yield x (Right (xs, a))
71+
72+ extract (Left s) = do
73+ r <- extract1 s
74+ return $ case r of
75+ Nothing -> Nothing
76+ Just x -> Just $ Source [] x
77+ extract (Right (xs, a)) = return $ Just $ Source xs a
78+
79+-- | Return some unused data back to the source. The data is prepended (or
80+-- consed) to the source.
81+--
82+-- /Internal/
83+unread :: [b] -> Source a b -> Source a b
84+unread xs (Source ys seed) = Source (xs ++ ys) seed
streamly.cabal+3
@@ -433,6 +433,9 @@ library
433433 , Streamly.Internal.Data.Stream.StreamD.Type
434434 , Streamly.Internal.Data.Stream.StreamDK.Type
435435 , Streamly.Internal.Data.Unfold.Types
436+ , Streamly.Internal.Data.Unfold.Resume.Type
437+ , Streamly.Internal.Data.Unfold.Resume
438+ , Streamly.Internal.Data.Unfold.Source
436439 , Streamly.Internal.Data.Sink.Types
437440 , Streamly.Internal.Data.Parser.ParserK.Types
438441 , Streamly.Internal.Data.Parser.ParserD.Types
test/Streamly/Test/Data/Parser/ParserD.hs+32
@@ -16,6 +16,9 @@ import qualified Streamly.Internal.Data.Array.Foreign as A
1616import qualified Streamly.Internal.Data.Fold as FL
1717import qualified Streamly.Internal.Data.Parser.ParserD as P
1818import qualified Streamly.Internal.Data.Stream.IsStream as S
19+import qualified Streamly.Internal.Data.Unfold as Unfold
20+import qualified Streamly.Internal.Data.Unfold.Source as Source
21+import qualified Streamly.Internal.Data.Unfold.Resume as UnfoldR
1922import qualified Test.Hspec as H
2023
2124import Prelude hiding (sequence)
@@ -604,6 +607,34 @@ parseMany =
604607 )
605608 listEquals (==) outs ins
606609
610+-- basic sanity test for parsing from arrays
611+parseUnfold :: Property
612+parseUnfold = do
613+ let len = 200
614+ -- ls = input list (stream)
615+ -- clen = chunk size
616+ -- tlen = parser take size
617+ forAll
618+ ((,,)
619+ <$> vectorOf len (chooseAny :: Gen Int)
620+ <*> chooseInt (1, len)
621+ <*> chooseInt (1, len)) $ \(ls, clen, tlen) ->
622+ monadicIO $ do
623+ arrays <- S.toList $ S.arraysOf clen (S.fromList ls)
624+ let src = Source.source (arrays, Nothing)
625+ let parser = P.fromFold (FL.takeLE tlen FL.toList)
626+ let readSrc =
627+ Source.read
628+ $ UnfoldR.concat UnfoldR.fromList A.readResumable
629+ let streamParser =
630+ UnfoldR.simplify (UnfoldR.parseManyD parser readSrc)
631+ xs <- run
632+ $ S.toList
633+ $ S.concatUnfold Unfold.fromList
634+ $ S.unfold streamParser src
635+
636+ listEquals (==) xs ls
637+
607638-------------------------------------------------------------------------------
608639-- Test for a particular case hit during fs events testing
609640-------------------------------------------------------------------------------
@@ -683,6 +714,7 @@ main =
683714 describe "Stream parsing" $ do
684715 prop "parseMany" parseMany
685716 prop "parseMany2Events" parseMany2Events
717+ prop "parseUnfold" parseUnfold
686718
687719 describe "test for accumulator" $ do
688720 prop "P.fromFold FL.sum = FL.sum" fromFold