on master help sign in
Add concurrent generation and transformation capabilities

- monadic stream generation functions are now concurrent - monadic stream transformation (mapM, sequence) functions are now concurrent - fixed a race which caused blockedindefinitely on MVar in rare cases

commit9cb2ac0bb76390425be7ff19e817ac18abd11383 authorHarendra Kumar <harendra.kumar@gmail.com> dateThu 24 May 2018, 16:48 (8 years ago) parentc2279b9
10 files changed+525−146
.travis.yml+5−3 Changelog.md+22−2 benchmark/LinearOps.hs+7−4 src/Streamly.hs+1−1 src/Streamly/Core.hs+201−37 src/Streamly/Prelude.hs+31−33 src/Streamly/Streams.hs+82−40 src/Streamly/Tutorial.hs+17 streamly.cabal+1 test/Prop.hs+158−26
.travis.yml+5−3
@@ -58,7 +58,9 @@ matrix:
5858 # (Linux) stack builds
5959 # --------------------------------------------------------------------------
6060
61− - env: BUILD=stack CABALVER=1.22 GHCVER=7.10.3 RESOLVER=lts-6 STACK_YAML=stack-7.10.yaml STACK_BUILD_OPTIONS="--flag streamly:examples-sdl"
61+ # For 7.10.3 we disable -Werror because of some bogus SPECIALIZE pragma
62+ # warnings.
63+ - env: BUILD=stack CABALVER=1.22 GHCVER=7.10.3 GHC_OPTIONS="" RESOLVER=lts-6 STACK_YAML=stack-7.10.yaml STACK_BUILD_OPTIONS="--flag streamly:examples-sdl"
6264 addons: {apt: {packages: [cabal-install-1.22,ghc-7.10.3,libsdl1.2-dev], sources: [hvr-ghc]}}
6365
6466 - env: BUILD=stack CABALVER=1.24 GHCVER=8.0.2 RESOLVER=lts-9 STACK_YAML=stack-8.0.yaml STACK_BUILD_OPTIONS="--flag streamly:examples-sdl"
@@ -121,8 +123,8 @@ matrix:
121123
122124 # Note COVERALLS (hpc-coveralls) works only with cabal build.
123125 # For this to succeed you have to add your project to coveralls.io first
124− #- env: BUILD=cabal CABALVER=2.0 GHCVER=8.2.2 COVERALLS_OPTIONS="--coverage-mode=StrictlyFullLines --exclude-dir=test test properties loops nested-loops parallel-loops"
125− # addons: {apt: {packages: [cabal-install-2.0,ghc-8.2.2], sources: [hvr-ghc]}}
126+ - env: BUILD=cabal CABALVER=2.0 GHCVER=8.2.2 COVERALLS_OPTIONS="--coverage-mode=StrictlyFullLines --exclude-dir=test test properties loops nested-loops parallel-loops"
127+ addons: {apt: {packages: [cabal-install-2.0,ghc-8.2.2], sources: [hvr-ghc]}}
126128
127129 # --------------------------------------------------------------------------
128130 # Builds that are allowed to fail
Changelog.md+22−2
@@ -1,7 +1,27 @@
11## Unreleased
22
3−* Add concurrent lookahead stream type `Ahead`
4−* Add `fromFoldableM` to create a stream from a container of monadic actions
3+### Breaking changes
4+
5+* Some prelude functions, to which concurrency capability has been added, will
6+ now require a `MonadAsync` constraint.
7+
8+### Bug Fixes
9+
10+* Fixed a race due to which, in a rare case, we might block indefinitely on
11+ an MVar due to a lost wakeup.
12+* Fixed an issue in adaptive concurrency. The issue caused us to stop creating
13+ more worker threads in some cases due to a race. This would not cause any
14+ functional issue but would reduce concurrency in some cases.
15+
16+### Enhancements
17+* Added a concurrent lookahead stream type `Ahead`
18+* Added `fromFoldableM` API that creates a stream from a container of monadic
19+ actions
20+* Monadic stream generation functions `consM`, `|:`, `unfoldrM`, `replicateM`,
21+ `repeatM`, `iterateM` and `fromFoldableM` can now generate streams
22+ concurrently when used with concurrent stream types.
23+* Monad transformation functions `mapM` and `sequence` can now map actions
24+ concurrently when used at appropriate stream types.
525
626## 0.2.0
727
benchmark/LinearOps.hs+7−4
@@ -5,6 +5,8 @@
55-- License : MIT
66-- Maintainer : harendra.kumar@gmail.com
77
8+{-# LANGUAGE FlexibleContexts #-}
9+
810module LinearOps where
911
1012import Prelude
@@ -44,13 +46,14 @@ maxValue = value + 1000
4446{-# INLINE composeAllInFilters #-}
4547{-# INLINE composeAllOutFilters #-}
4648{-# INLINE composeMapAllInFilter #-}
47−toNull, scan, map, filterEven, mapM, filterAllOut,
49+toNull, scan, map, filterEven, filterAllOut,
4850 filterAllIn, takeOne, takeAll, takeWhileTrue, dropAll, dropWhileTrue, zip,
49− concat, composeMapM, composeAllInFilters, composeAllOutFilters,
51+ concat, composeAllInFilters, composeAllOutFilters,
5052 composeMapAllInFilter
5153 :: Monad m
5254 => Stream m Int -> m ()
5355
56+mapM, composeMapM :: S.MonadAsync m => Stream m Int -> m ()
5457toList :: Monad m => Stream m Int -> m [Int]
5558foldl :: Monad m => Stream m Int -> m Int
5659last :: Monad m => Stream m Int -> m (Maybe Int)
@@ -74,14 +77,14 @@ sourceFromFoldable :: Int -> Stream m Int
7477sourceFromFoldable n = S.fromFoldable [n..n+value]
7578
7679{-# INLINE sourceFromFoldableM #-}
77−sourceFromFoldableM :: Monad m => Int -> Stream m Int
80+sourceFromFoldableM :: S.MonadAsync m => Int -> Stream m Int
7881sourceFromFoldableM n = S.fromFoldableM (Prelude.fmap return [n..n+value])
7982
8083sourceFoldMapWith :: Monad m => Int -> Stream m Int
8184sourceFoldMapWith n = S.foldMapWith S.serial return [n..n+value]
8285
8386{-# INLINE sourceUnfoldrM #-}
84−sourceUnfoldrM :: Monad m => Int -> Stream m Int
87+sourceUnfoldrM :: S.MonadAsync m => Int -> Stream m Int
8588sourceUnfoldrM n = S.unfoldrM step n
8689 where
8790 step cnt =
src/Streamly.hs+1−1
@@ -91,7 +91,7 @@ module Streamly
9191
9292 -- * Stream Type Adapters
9393 -- $adapters
94− , IsStream
94+ , IsStream ()
9595
9696 , serially
9797 , wSerially
src/Streamly/Core.hs+201−37
@@ -1,3 +1,4 @@
1+{-# LANGUAGE CPP #-}
12{-# LANGUAGE ConstraintKinds #-}
23{-# LANGUAGE FlexibleContexts #-}
34{-# LANGUAGE FlexibleInstances #-}
@@ -25,13 +26,19 @@ module Streamly.Core
2526 -- * Streams
2627 , Stream (..)
2728
28− -- * Construction
29+ -- * Construction (pure)
30+ , nil
31+ , cons
2932 , singleton
3033 , once
31− , cons
32− , consM
3334 , repeat
34− , nil
35+
36+ -- * Construction (monadic)
37+ , consM
38+ , consMAhead
39+ , consMAsync
40+ , consMWAsync
41+ , consMParallel
3542
3643 -- * Semigroup Style Composition
3744 , serial
@@ -41,13 +48,11 @@ module Streamly.Core
4148 , wAsync
4249 , parallel
4350
44− -- * Alternative
45− , alt
46−
4751 -- * zip
4852 , zipWith
4953 , zipAsyncWith
5054
55+ -- XXX move to Streams.hs
5156 -- * Transformers
5257 , withLocal
5358 , withCatchError
@@ -55,18 +60,24 @@ module Streamly.Core
5560 -- * Concurrent Stream Vars (SVars)
5661 , SVar
5762 , SVarStyle (..)
58− , newEmptySVar
5963 , newStreamVar1
60− , joinStreamVarAsync
6164 , fromStreamVar
6265 , toStreamVar
6366 )
6467where
6568
69+-- MVar diagnostics has some overhead - around 5% on asyncly null benchmark,
70+-- but keep it on for now to debug problems quickly if and when they happen.
71+-- define DIAGNOSTICS
72+
6673import Control.Concurrent (ThreadId, myThreadId, threadDelay)
6774import Control.Concurrent.MVar (MVar, newEmptyMVar, tryTakeMVar,
6875 tryPutMVar, takeMVar, putMVar)
6976import Control.Exception (SomeException (..))
77+#ifdef DIAGNOSTICS
78+import Control.Exception (catch,
79+ BlockedIndefinitelyOnMVar(..))
80+#endif
7081import qualified Control.Exception.Lifted as EL
7182import Control.Monad (when)
7283import Control.Monad.Catch (MonadThrow, throwM)
@@ -120,7 +131,7 @@ data SVarStyle =
120131 | WAsyncVar -- breadth first concurrent
121132 | ParallelVar -- all parallel
122133 | AheadVar -- Concurrent look ahead
123− deriving Eq
134+ deriving (Eq, Show)
124135
125136-- | An SVar or a Stream Var is a conduit to the output from multiple streams
126137-- running concurrently and asynchronously. An SVar can be thought of as an
@@ -170,6 +181,7 @@ data SVar m a =
170181 , workQueue :: IORef ([Stream m a], Int)
171182 , enqueue :: Stream m a -> IO ()
172183 , queueEmpty :: m Bool
184+ , waitingForWork :: IORef Bool
173185 , runqueue :: m ()
174186
175187 -- Shared, thread tracking
@@ -177,6 +189,50 @@ data SVar m a =
177189 , activeWorkers :: IORef Int
178190 }
179191
192+#ifdef DIAGNOSTICS
193+{-# NOINLINE dumpSVar #-}
194+dumpSVar :: SVar m a -> IO String
195+dumpSVar sv = do
196+ (oqList, oqLen) <- readIORef $ outputQueue sv
197+ db <- tryTakeMVar $ doorBell sv
198+ srn <- tryTakeMVar $ siren sv
199+ (oheap, oheapSize) <- readIORef $ outputHeap sv
200+ hsrn <- tryTakeMVar $ heapSiren sv
201+ (wq, wqSize) <- readIORef $ workQueue sv
202+ waiting <- readIORef $ waitingForWork sv
203+ rthread <- readIORef $ runningThreads sv
204+ workers <- readIORef $ activeWorkers sv
205+ -- XXX queueEmpty should be made IO return type
206+
207+ return $ unlines
208+ [ "style = " ++ show (svarStyle sv)
209+ , "outputQueue length computed = " ++ show (length oqList)
210+ , "outputQueue length maintained = " ++ show oqLen
211+ , "output doorBell = " ++ show db
212+ , "worker siren = " ++ show srn
213+ , "heap length computed = " ++ show (H.size oheap)
214+ , "heap length maintained = " ++ show oheapSize
215+ , "heap siren = " ++ show hsrn
216+ , "work queue length computed = " ++ show (length wq)
217+ , "work queue length maintained = " ++ show wqSize
218+ , "waitingForWork = " ++ show waiting
219+ , "running threads = " ++ show rthread
220+ , "running thread count = " ++ show workers
221+ ]
222+
223+{-# NOINLINE mvarExcHandler #-}
224+mvarExcHandler :: SVar m a -> String -> BlockedIndefinitelyOnMVar -> IO ()
225+mvarExcHandler sv label BlockedIndefinitelyOnMVar = do
226+ svInfo <- dumpSVar sv
227+ error $ label ++ " " ++ "BlockedIndefinitelyOnMVar\n" ++ svInfo
228+
229+withDBGMVar :: SVar m a -> String -> IO () -> IO ()
230+withDBGMVar sv label action = action `catch` mvarExcHandler sv label
231+#else
232+withDBGMVar :: SVar m a -> String -> IO () -> IO ()
233+withDBGMVar _ _ action = action
234+#endif
235+
180236-- Slightly faster version of CAS. Gained some improvement by avoiding the use
181237-- of "evaluate" because we know we do not have exceptions in fn.
182238{-# INLINE atomicModifyIORefCAS #-}
@@ -350,7 +406,7 @@ send sv msg = do
350406 -- we will be woken up by the consumer blowing the siren.
351407 -- Note: current length after adding our output is len + 1
352408 else when (len + 1 >= 1500) $ do
353− takeMVar (siren sv)
409+ withDBGMVar sv "send: wait for siren" $ takeMVar (siren sv)
354410 (_, n) <- readIORef (outputQueue sv)
355411 when (n <= 1500) $ void $ tryPutMVar (siren sv) ()
356412
@@ -367,8 +423,22 @@ sendStop sv = myThreadId >>= \tid -> send sv (ChildStop tid Nothing)
367423-- expressions. Large left associated compositions can grow this to a
368424-- large size
369425{-# INLINE enqueueLIFO #-}
370−enqueueLIFO :: IORef [Stream m a] -> Stream m a -> IO ()
371−enqueueLIFO q m = atomicModifyIORefCAS_ q $ \ ms -> m : ms
426+enqueueLIFO :: SVar m a -> IORef [Stream m a] -> Stream m a -> IO ()
427+enqueueLIFO sv q m = do
428+ w <- readIORef $ waitingForWork sv
429+ if w
430+ then withDoorBell
431+ else atomicModifyIORefCAS_ q $ \ms -> m : ms
432+
433+ where
434+
435+ withDoorBell = do
436+ v <- atomicModifyIORefCAS q $ \ ms ->
437+ case ms of
438+ [] -> (m : ms, True)
439+ _ -> (m : ms, False)
440+ when v $ void $ tryPutMVar (doorBell sv) ()
441+ atomicModifyIORefCAS_ (waitingForWork sv) (const False)
372442
373443runqueueLIFO :: MonadIO m => SVar m a -> IORef [Stream m a] -> m ()
374444runqueueLIFO sv q = run
@@ -394,8 +464,19 @@ runqueueLIFO sv q = run
394464-------------------------------------------------------------------------------
395465
396466{-# INLINE enqueueFIFO #-}
397−enqueueFIFO :: LinkedQueue (Stream m a) -> Stream m a -> IO ()
398−enqueueFIFO = pushL
467+enqueueFIFO :: SVar m a -> LinkedQueue (Stream m a) -> Stream m a -> IO ()
468+enqueueFIFO sv q m = do
469+ w <- readIORef $ waitingForWork sv
470+ if w
471+ then withDoorBell
472+ else pushL q m
473+
474+ where
475+
476+ withDoorBell = do
477+ emp <- nullQ q
478+ pushL q m
479+ when emp $ void $ tryPutMVar (doorBell sv) ()
399480
400481runqueueFIFO :: MonadIO m => SVar m a -> LinkedQueue (Stream m a) -> m ()
401482runqueueFIFO sv q = run
@@ -411,7 +492,7 @@ runqueueFIFO sv q = run
411492 dequeue = liftIO $ tryPopR q
412493 sendit a = liftIO $ send sv (ChildYield a)
413494 single a = sendit a >> run
414− yield a r = sendit a >> liftIO (enqueueFIFO q r) >> run
495+ yield a r = sendit a >> liftIO (enqueueFIFO sv q r) >> run
415496
416497-------------------------------------------------------------------------------
417498-- Parallel
@@ -499,7 +580,8 @@ sendToHeap sv msg seqNo = do
499580 if added
500581 then do
501582 when (len + 1 >= maxHeapSize) $ do
502− takeMVar (heapSiren sv)
583+ withDBGMVar sv "sendToHeap: wait for heapSiren"
584+ $ takeMVar (heapSiren sv)
503585 (h, _) <- readIORef (outputHeap sv)
504586 when (H.size h <= maxHeapSize) $ void $ tryPutMVar (siren sv) ()
505587 return False
@@ -508,7 +590,7 @@ sendToHeap sv msg seqNo = do
508590 AheadYieldSingle a -> send sv (ChildYield a)
509591 AheadYieldMany a mv -> do
510592 send sv (ChildYield a)
511− putMVar mv ()
593+ withDBGMVar sv "sendToHeap: wakeup stream" $ putMVar mv ()
512594 return True
513595
514596popFromHeap
@@ -547,7 +629,7 @@ drainHeap sv nextSeqNo = do
547629 drainHeap sv nextSeqNo
548630 AheadYieldMany a mv -> do
549631 send sv (ChildYield a)
550− putMVar mv ()
632+ withDBGMVar sv "drainHeap: wakeup stream" $ putMVar mv ()
551633
552634-- | The thread that has the token has the responsibility to move any pending
553635-- entries in the heap to the outputQueue. When the token is assigned to a
@@ -585,11 +667,15 @@ handleOutput sv haveToken prevSeqNo seqNo a ev = do
585667-- associated expression, we queue the right expression and execute the left.
586668-- Thererefore the queue never has more than on item in it.
587669{-# INLINE enqueueAhead #-}
588−enqueueAhead :: IORef ([Stream m a], Int) -> Stream m a -> IO ()
589−enqueueAhead q m = do
670+enqueueAhead :: SVar m a -> IORef ([Stream m a], Int) -> Stream m a -> IO ()
671+enqueueAhead sv q m = do
672+ w <- readIORef $ waitingForWork sv
590673 atomicModifyIORefCAS_ q $ \ case
591674 ([], n) -> ([m], n + 1) -- increment sequence
592675 _ -> error "not empty"
676+ when w $ do
677+ void $ tryPutMVar (doorBell sv) ()
678+ atomicModifyIORefCAS_ (waitingForWork sv) (const False)
593679
594680runqueueAhead :: MonadIO m => SVar m a -> IORef ([Stream m a], Int) -> m ()
595681runqueueAhead sv q = run False (-1)
@@ -637,7 +723,9 @@ runqueueAhead sv q = run False (-1)
637723 gotToken <-
638724 handleOutput sv tStatus prevSeqNo seqNo a
639725 (AheadYieldMany a mv)
640− when (not gotToken) $ void $ takeMVar mv
726+ when (not gotToken) $ void $
727+ withDBGMVar sv "runqueueAhead: stream waiting"
728+ $ takeMVar mv
641729 (runStream r) (Just sv) stopNext singleNext yieldNext
642730
643731 (runStream m) (Just sv) stop single yield
@@ -725,32 +813,76 @@ sendWorkerWait sv = do
725813 -- If there is no output pending to process and there is no worker to be
726814 -- sent then we block, so that we do not keep looping fruitlessly.
727815
816+ -- Note that we are guaranteed to have at least one outstanding worker when
817+ -- we enter this function. So if we sleep we are guaranteed to be woken up.
818+ -- Note that the worker count is only decremented during event processing
819+ -- in fromStreamVar and therefore it is safe to read and use it without a
820+ -- lock.
821+
728822 -- XXX we should wait in such a way so that we can be immediately
729− -- interrupted by a doorBell.
823+ -- interrupted by a doorBell. We can sleep on the doorbell and use a
824+ -- monitor thread to send us a doorbell.
730825 liftIO $ threadDelay 200
731826 (_, n) <- liftIO $ readIORef (outputQueue sv)
732827 when (n <= 0) $ do
733− -- XXX the queue may be empty temporarily if the worker has dequeued
734− -- the work item but has not enqueued the remaining part yet. For the
735− -- same reason, a worker may come back if it tries to dequeue when the
736− -- queue is empty, even though the whole work has not finished yet.
828+ -- The queue may be empty temporarily if the worker has dequeued the
829+ -- work item but has not enqueued the remaining part yet. For the same
830+ -- reason, a worker may come back if it tries to dequeue and finds the
831+ -- queue empty, even though the whole work has not finished yet.
737832 done <- queueEmpty sv
738833 if not done
739834 then do
740835 cnt <- liftIO $ readIORef $ activeWorkers sv
836+ -- Note that we may deadlock if the previous workers (tasks in the
837+ -- stream) wait/depend on the future workers (tasks in the stream)
838+ -- executing. In that case we should either configure the maxWorker
839+ -- count to higher or use parallel style instead of ahead or async
840+ -- style.
741841 if (cnt < 1500)
742842 then do
743843 pushWorker sv
744844 sendWorkerWait sv
745− else liftIO $ takeMVar (doorBell sv)
746− else liftIO $ takeMVar (doorBell sv)
845+ else liftIO $ withDBGMVar sv "sendWorkerWait: wait for workers"
846+ $ takeMVar (doorBell sv)
847+ else do
848+ -- We found that the queue was empty, but it may be empty
849+ -- temporarily, when we checked it. If that's the case we might
850+ -- sleep indefinitely unless the active workers produce some
851+ -- output. We may deadlock specially if the otuput from the other
852+ -- workers depends on the future workers that we may never send.
853+ -- So in case the queue was temporarily empty set a flag to inform
854+ -- the enqueue to send us a doorbell.
855+
856+ liftIO $ atomicModifyIORefCAS_ (waitingForWork sv) $ const True
857+
858+ -- check again, this time we have set the waitingForWork flag so we
859+ -- are guaranteed to get a doorbell in case the status changed
860+ -- before we could sleep.
861+ --
862+ -- Note that this is just a best effort mechanism to avoid a
863+ -- deadlock. Deadlocks may still happen if for some weird reason
864+ -- the consuming computation shares an MVar or some other resource
865+ -- with the producing computation and gets blocked on that resource
866+ -- and therefore cannot do any pushworker to add more threads to
867+ -- the producer. In such cases the programmer should use a parallel
868+ -- style so that all the producers are scheduled immediately and
869+ -- unconditionally. We can also use a separate monitor thread to
870+ -- push workers instead of pushing them from the consumer, but then
871+ -- we are no longer using pull based concurrency rate adaptation.
872+ --
873+ -- XXX update this in the tutorial.
874+ emp <- queueEmpty sv
875+ when emp $
876+ liftIO $ withDBGMVar sv "sendWorkerWait: nothing to do"
877+ $ takeMVar (doorBell sv)
747878
748879-- | Pull a stream from an SVar.
749880{-# NOINLINE fromStreamVar #-}
750881fromStreamVar :: MonadAsync m => SVar m a -> Stream m a
751882fromStreamVar sv = Stream $ \_ stp sng yld -> do
752883 if svarStyle sv == ParallelVar
753− then liftIO $ takeMVar (doorBell sv)
884+ then liftIO $ withDBGMVar sv "fromStreamVar: doorbell"
885+ $ takeMVar (doorBell sv)
754886 else do
755887 res <- liftIO $ tryTakeMVar (doorBell sv)
756888 when (isNothing res) $ sendWorkerWait sv
@@ -798,6 +930,7 @@ getFifoSVar ctype = do
798930 outQMv <- newEmptyMVar
799931 hooter <- newEmptyMVar
800932 active <- newIORef 0
933+ wfw <- newIORef False
801934 running <- newIORef S.empty
802935 q <- newQ
803936 let sv =
@@ -809,8 +942,9 @@ getFifoSVar ctype = do
809942 , runningThreads = running
810943 , workQueue = undefined
811944 , runqueue = runqueueFIFO sv q
812− , enqueue = pushL q
945+ , enqueue = enqueueFIFO sv q
813946 , queueEmpty = liftIO $ nullQ q
947+ , waitingForWork = wfw
814948 , svarStyle = ctype
815949 , activeWorkers = active
816950 }
@@ -822,6 +956,7 @@ getLifoSVar ctype = do
822956 outQMv <- newEmptyMVar
823957 hooter <- newEmptyMVar
824958 active <- newIORef 0
959+ wfw <- newIORef False
825960 running <- newIORef S.empty
826961 q <- newIORef []
827962 let checkEmpty = null <$> liftIO (readIORef q)
@@ -834,8 +969,9 @@ getLifoSVar ctype = do
834969 , runningThreads = running
835970 , workQueue = undefined
836971 , runqueue = runqueueLIFO sv q
837− , enqueue = enqueueLIFO q
972+ , enqueue = enqueueLIFO sv q
838973 , queueEmpty = checkEmpty
974+ , waitingForWork = wfw
839975 , svarStyle = ctype
840976 , activeWorkers = active
841977 }
@@ -847,6 +983,7 @@ getParSVar style = do
847983 outQMv <- newEmptyMVar
848984 hooter <- newEmptyMVar
849985 active <- newIORef 0
986+ wfw <- newIORef False
850987 running <- newIORef S.empty
851988 let sv =
852989 SVar { outputQueue = outQ
@@ -859,6 +996,7 @@ getParSVar style = do
859996 , runqueue = undefined
860997 , enqueue = undefined
861998 , queueEmpty = undefined
999+ , waitingForWork = wfw
8621000 , svarStyle = style
8631001 , activeWorkers = active
8641002 }
@@ -872,6 +1010,7 @@ getAheadSVar style = do
8721010 hooter <- newEmptyMVar
8731011 hsiren <- newEmptyMVar
8741012 active <- newIORef 0
1013+ wfw <- newIORef False
8751014 running <- newIORef S.empty
8761015 q <- newIORef ([], -1)
8771016 let checkEmpty = liftIO $ do
@@ -888,6 +1027,7 @@ getAheadSVar style = do
8881027 , runqueue = runqueueAhead sv q
8891028 , enqueue = undefined
8901029 , queueEmpty = checkEmpty
1030+ , waitingForWork = wfw
8911031 , svarStyle = style
8921032 , activeWorkers = active
8931033 }
@@ -921,7 +1061,7 @@ newStreamVarAhead m = do
9211061 -- Note: We must have all the work on the queue before sending the
9221062 -- pushworker, otherwise the pushworker may exit before we even get a
9231063 -- chance to push.
924− liftIO $ enqueueAhead (workQueue sv) m
1064+ liftIO $ enqueueAhead sv (workQueue sv) m
9251065 pushWorker sv
9261066 return sv
9271067
@@ -1033,7 +1173,7 @@ ahead :: MonadAsync m => Stream m a -> Stream m a -> Stream m a
10331173ahead m1 m2 = Stream $ \svr stp sng yld -> do
10341174 case svr of
10351175 Just sv | svarStyle sv == AheadVar -> do
1036− liftIO $ enqueueAhead (workQueue sv) m2
1176+ liftIO $ enqueueAhead sv (workQueue sv) m2
10371177 -- Always run the left side on a new SVar to avoid complexity in
10381178 -- sequencing results. This means the left side cannot further
10391179 -- split into more ahead computations on the same SVar.
@@ -1043,9 +1183,15 @@ ahead m1 m2 = Stream $ \svr stp sng yld -> do
10431183 (runStream (fromStreamVar sv)) Nothing stp sng yld
10441184 where
10451185 concurrently ma mb = Stream $ \svr stp sng yld -> do
1046− liftIO $ enqueueAhead (workQueue (fromJust svr)) mb
1186+ liftIO $ enqueueAhead (fromJust svr) (workQueue (fromJust svr)) mb
10471187 (runStream ma) Nothing stp sng yld
10481188
1189+-- | XXX we can implement it more efficienty by directly implementing instead
1190+-- of combining streams using ahead.
1191+{-# INLINE consMAhead #-}
1192+consMAhead :: MonadAsync m => m a -> Stream m a -> Stream m a
1193+consMAhead m r = once m `ahead` r
1194+
10491195------------------------------------------------------------------------------
10501196-- Semigroup and Monoid style compositions for parallel actions
10511197------------------------------------------------------------------------------
@@ -1054,14 +1200,32 @@ ahead m1 m2 = Stream $ \svr stp sng yld -> do
10541200async :: MonadAsync m => Stream m a -> Stream m a -> Stream m a
10551201async = joinStreamVarAsync AsyncVar
10561202
1203+-- | XXX we can implement it more efficienty by directly implementing instead
1204+-- of combining streams using async.
1205+{-# INLINE consMAsync #-}
1206+consMAsync :: MonadAsync m => m a -> Stream m a -> Stream m a
1207+consMAsync m r = once m `async` r
1208+
10571209{-# INLINE wAsync #-}
10581210wAsync :: MonadAsync m => Stream m a -> Stream m a -> Stream m a
10591211wAsync = joinStreamVarAsync WAsyncVar
10601212
1213+-- | XXX we can implement it more efficienty by directly implementing instead
1214+-- of combining streams using wAsync.
1215+{-# INLINE consMWAsync #-}
1216+consMWAsync :: MonadAsync m => m a -> Stream m a -> Stream m a
1217+consMWAsync m r = once m `wAsync` r
1218+
10611219{-# INLINE parallel #-}
10621220parallel :: MonadAsync m => Stream m a -> Stream m a -> Stream m a
10631221parallel = joinStreamVarPar ParallelVar
10641222
1223+-- | XXX we can implement it more efficienty by directly implementing instead
1224+-- of combining streams using parallel.
1225+{-# INLINE consMParallel #-}
1226+consMParallel :: MonadAsync m => m a -> Stream m a -> Stream m a
1227+consMParallel m r = once m `parallel` r
1228+
10651229-------------------------------------------------------------------------------
10661230-- Functor instace is the same for all types
10671231-------------------------------------------------------------------------------
@@ -1076,8 +1240,8 @@ instance Monad m => Functor (Stream m) where
10761240-- Alternative & MonadPlus
10771241------------------------------------------------------------------------------
10781242
1079−alt :: Stream m a -> Stream m a -> Stream m a
1080−alt m1 m2 = Stream $ \_ stp sng yld ->
1243+_alt :: Stream m a -> Stream m a -> Stream m a
1244+_alt m1 m2 = Stream $ \_ stp sng yld ->
10811245 let stop = runStream m2 Nothing stp sng yld
10821246 in runStream m1 Nothing stop sng yld
10831247
src/Streamly/Prelude.hs+31−33
@@ -26,6 +26,9 @@
2626-- provided for convenience and for consistency with other pure APIs in the
2727-- @base@ package.
2828--
29+-- Functions having a 'MonadAsync' constraint work concurrently when used
30+-- with appropriate stream type combinator.
31+--
2932-- Deconstruction and folds accept a 'SerialT' type instead of a polymorphic
3033-- type to ensure that streams always have a concrete monomorphic type by
3134-- default, reducing type errors. In case you want to use any other type of
@@ -159,14 +162,15 @@ unfoldr step = fromStream . go
159162--
160163-- @since 0.1.0
161164{-# INLINE unfoldrM #-}
162−unfoldrM :: (IsStream t, Monad m) => (b -> m (Maybe (a, b))) -> b -> t m a
163−unfoldrM step = fromStream . go
165+unfoldrM :: (IsStream t, MonadAsync m) => (b -> m (Maybe (a, b))) -> b -> t m a
166+unfoldrM step = go
164167 where
165− go s = Stream $ \_ stp _ yld -> do
168+ go s = fromStream $ Stream $ \svr stp sng yld -> do
166169 mayb <- step s
167170 case mayb of
168171 Nothing -> stp
169− Just (a, b) -> yld a (go b)
172+ Just (a, b) ->
173+ S.runStream (toStream (return a |: go b)) svr stp sng yld
170174
171175-- | Construct a stream from a 'Foldable' containing pure values.
172176--
@@ -179,7 +183,7 @@ fromFoldable = Prelude.foldr cons nil
179183--
180184-- @since 0.3.0
181185{-# INLINE fromFoldableM #-}
182−fromFoldableM :: (IsStream t, Monad m, Foldable f) => f (m a) -> t m a
186+fromFoldableM :: (IsStream t, MonadAsync m, Foldable f) => f (m a) -> t m a
183187fromFoldableM = Prelude.foldr consM nil
184188
185189-- | Same as 'fromFoldable'.
@@ -206,22 +210,17 @@ once = fromStream . S.once
206210-- | Generate a stream by performing a monadic action @n@ times.
207211--
208212-- @since 0.1.1
209−replicateM :: (IsStream t, Monad m) => Int -> m a -> t m a
210−replicateM n m = fromStream $ go n
213+replicateM :: (IsStream t, MonadAsync m) => Int -> m a -> t m a
214+replicateM n m = go n
211215 where
212− go cnt = Stream $ \_ stp _ yld ->
213− if cnt <= 0
214− then stp
215− else m >>= \a -> yld a (go (cnt - 1))
216+ go cnt = if cnt <= 0 then nil else m |: go (cnt - 1)
216217
217218-- | Generate a stream by repeatedly executing a monadic action forever.
218219--
219220-- @since 0.2.0
220−repeatM :: (IsStream t, Monad m) => m a -> t m a
221−repeatM = fromStream . go
222− where
223− go m = Stream $ \_ _ _ yld ->
224− m >>= \a -> yld a (go m)
221+repeatM :: (IsStream t, MonadAsync m) => m a -> t m a
222+repeatM = go
223+ where go m = m |: go m
225224
226225-- | Iterate a pure function from a seed value, streaming the results forever.
227226--
@@ -235,12 +234,12 @@ iterate step = fromStream . go
235234-- forever.
236235--
237236-- @since 0.1.2
238−iterateM :: (IsStream t, Monad m) => (a -> m a) -> a -> t m a
239−iterateM step = fromStream . go
237+iterateM :: (IsStream t, MonadAsync m) => (a -> m a) -> a -> t m a
238+iterateM step = go
240239 where
241− go s = Stream $ \_ _ _ yld -> do
242− a <- step s
243− yld s (go a)
240+ go s = fromStream $ Stream $ \svr stp sng yld -> do
241+ next <- step s
242+ S.runStream (toStream (return s |: go next)) svr stp sng yld
244243
245244-- | Read lines from an IO Handle into a stream of Strings.
246245--
@@ -396,8 +395,8 @@ foldlM' :: Monad m => (b -> a -> m b) -> b -> SerialT m a -> m b
396395foldlM' step begin m = foldxM step (return begin) return m
397396
398397-- | Decompose a stream into its head and tail. If the stream is empty, returns
399−-- 'Nothing'. If the stream is non-empty, returns 'Just (a, ma)', where 'a' is
400−-- the head of the stream and 'ma' its tail.
398+-- 'Nothing'. If the stream is non-empty, returns @Just (a, ma)@, where @a@ is
399+-- the head of the stream and @ma@ its tail.
401400--
402401-- @since 0.1.0
403402uncons :: (IsStream t, Monad m) => SerialT m a -> m (Maybe (a, t m a))
@@ -655,21 +654,20 @@ maximum m = go Nothing (toStream m)
655654-- Transformation
656655------------------------------------------------------------------------------
657656
658−-- XXX Parallel variants of these? mapMWith et al. sequenceWith.
659−
660657-- | Replace each element of the stream with the result of a monadic action
661658-- applied on the element.
662659--
663660-- @since 0.1.0
664661{-# INLINE mapM #-}
665−mapM :: (IsStream t, Monad m) => (a -> m b) -> t m a -> t m b
666−mapM f m = fromStream $ go (toStream m)
662+mapM :: (IsStream t, MonadAsync m) => (a -> m b) -> t m a -> t m b
663+mapM f m = go (toStream m)
667664 where
668− go m1 = Stream $ \_ stp sng yld ->
665+ go m1 = fromStream $ Stream $ \svr stp sng yld ->
669666 let single a = f a >>= sng
670− yield a r = f a >>= \b -> yld b (go r)
667+ yield a r = S.runStream (toStream (f a |: (go r))) svr stp sng yld
671668 in (S.runStream m1) Nothing stp single yield
672669
670+-- XXX this can utilize parallel mapping if we implement it as runStream . mapM
673671-- | Apply a monadic action to each element of the stream and discard the
674672-- output of the action.
675673--
@@ -687,12 +685,12 @@ mapM_ f m = go (toStream m)
687685-- actions.
688686--
689687-- @since 0.1.0
690−sequence :: (IsStream t, Monad m) => t m (m a) -> t m a
691−sequence m = fromStream $ go (toStream m)
688+sequence :: (IsStream t, MonadAsync m) => t m (m a) -> t m a
689+sequence m = go (toStream m)
692690 where
693− go m1 = Stream $ \_ stp sng yld ->
691+ go m1 = fromStream $ Stream $ \svr stp sng yld ->
694692 let single ma = ma >>= sng
695− yield ma r = ma >>= \b -> yld b (go r)
693+ yield ma r = S.runStream (toStream $ ma |: go r) svr stp sng yld
696694 in (S.runStream m1) Nothing stp single yield
697695
698696------------------------------------------------------------------------------
src/Streamly/Streams.hs+82−40
@@ -3,6 +3,7 @@
33{-# LANGUAGE FlexibleContexts #-}
44{-# LANGUAGE FlexibleInstances #-}
55{-# LANGUAGE GeneralizedNewtypeDeriving#-}
6+{-# LANGUAGE InstanceSigs #-}
67{-# LANGUAGE MultiParamTypeClasses #-}
78{-# LANGUAGE RankNTypes #-}
89{-# LANGUAGE StandaloneDeriving #-}
@@ -24,15 +25,10 @@ module Streamly.Streams
2425 , Streaming -- deprecated
2526 , S.MonadAsync
2627
27− -- * SVars
28− , SVarStyle (..)
29− , SVar
30− , S.newEmptySVar
31−
3228 -- * Construction
3329 , nil
30+ , cons
3431 , (.:)
35− , (|:)
3632 , streamBuild
3733 , fromCallback
3834 , fromSVar
@@ -125,6 +121,9 @@ import qualified Streamly.Core as S
125121-- Types that can behave as a Stream
126122------------------------------------------------------------------------------
127123
124+infixr 5 `consM`
125+infixr 5 |:
126+
128127-- | Class of types that can represent a stream of elements of some type 'a' in
129128-- some monad 'm'.
130129--
@@ -132,8 +131,29 @@ import qualified Streamly.Core as S
132131class IsStream t where
133132 toStream :: t m a -> S.Stream m a
134133 fromStream :: S.Stream m a -> t m a
135− cons :: a -> t m a -> t m a
136− consM :: Monad m => m a -> t m a -> t m a
134+ -- | Constructs a stream by adding a monadic action at the head of an existing
135+ -- stream. For example:
136+ --
137+ -- @
138+ -- > toList $ getLine \`consM` getLine \`consM` nil
139+ -- hello
140+ -- world
141+ -- ["hello","world"]
142+ -- @
143+ --
144+ -- @since 0.2.0
145+ consM :: MonadAsync m => m a -> t m a -> t m a
146+ -- | Operator equivalent of 'consM'.
147+ --
148+ -- @
149+ -- > toList $ getLine |: getLine |: nil
150+ -- hello
151+ -- world
152+ -- ["hello","world"]
153+ -- @
154+ --
155+ -- @since 0.2.0
156+ (|:) :: MonadAsync m => m a -> t m a -> t m a
137157
138158-- | Same as 'IsStream'.
139159--
@@ -156,8 +176,6 @@ type Streaming = IsStream
156176nil :: IsStream t => t m a
157177nil = fromStream S.nil
158178
159−infixr 5 `consM`
160−
161179-- | Constructs a stream by adding a monadic action at the head of an existing
162180-- stream. For example:
163181--
@@ -169,25 +187,9 @@ infixr 5 `consM`
169187-- @
170188--
171189-- @since 0.2.0
172−-- {-# INLINE consMSerial #-}
173190consMSerial :: (IsStream t, Monad m) => m a -> t m a -> t m a
174191consMSerial m r = fromStream $ S.consM m (toStream r)
175192
176−infixr 5 |:
177−
178−-- | Operator equivalent of 'consM'.
179−--
180−-- @
181−-- > toList $ getLine |: getLine |: nil
182−-- hello
183−-- world
184−-- ["hello","world"]
185−-- @
186−--
187−-- @since 0.2.0
188−(|:) :: (IsStream t, Monad m) => m a -> t m a -> t m a
189−(|:) = consM
190−
191193infixr 5 `cons`
192194
193195-- | Construct a stream by adding a pure value at the head of an existing
@@ -199,8 +201,8 @@ infixr 5 `cons`
199201-- @
200202--
201203-- @since 0.1.0
202−consSerial :: IsStream t => a -> t m a -> t m a
203−consSerial a r = fromStream $ S.cons a (toStream r)
204+cons :: IsStream t => a -> t m a -> t m a
205+cons a r = fromStream $ S.cons a (toStream r)
204206
205207infixr 5 .:
206208
@@ -412,11 +414,17 @@ type StreamT = SerialT
412414instance IsStream SerialT where
413415 toStream = getSerialT
414416 fromStream = SerialT
415− cons = consSerial
417+
416418 {-# INLINE consM #-}
417419 {-# SPECIALIZE consM :: IO a -> SerialT IO a -> SerialT IO a #-}
420+ consM :: Monad m => m a -> SerialT m a -> SerialT m a
418421 consM = consMSerial
419422
423+ {-# INLINE (|:) #-}
424+ {-# SPECIALIZE (|:) :: IO a -> SerialT IO a -> SerialT IO a #-}
425+ (|:) :: Monad m => m a -> SerialT m a -> SerialT m a
426+ (|:) = consMSerial
427+
420428------------------------------------------------------------------------------
421429-- Semigroup
422430------------------------------------------------------------------------------
@@ -501,11 +509,17 @@ type InterleavedT = WSerialT
501509instance IsStream WSerialT where
502510 toStream = getWSerialT
503511 fromStream = WSerialT
504− cons = consSerial
512+
505513 {-# INLINE consM #-}
506514 {-# SPECIALIZE consM :: IO a -> WSerialT IO a -> WSerialT IO a #-}
515+ consM :: Monad m => m a -> WSerialT m a -> WSerialT m a
507516 consM = consMSerial
508517
518+ {-# INLINE (|:) #-}
519+ {-# SPECIALIZE (|:) :: IO a -> WSerialT IO a -> WSerialT IO a #-}
520+ (|:) :: Monad m => m a -> WSerialT m a -> WSerialT m a
521+ (|:) = consMSerial
522+
509523------------------------------------------------------------------------------
510524-- Semigroup
511525------------------------------------------------------------------------------
@@ -612,10 +626,14 @@ newtype AheadT m a = AheadT {getAheadT :: S.Stream m a}
612626instance IsStream AheadT where
613627 toStream = getAheadT
614628 fromStream = AheadT
615− cons = consSerial
629+
616630 {-# INLINE consM #-}
617631 {-# SPECIALIZE consM :: IO a -> AheadT IO a -> AheadT IO a #-}
618− consM = consMSerial
632+ consM m r = fromStream $ S.consMAhead m (toStream r)
633+
634+ {-# INLINE (|:) #-}
635+ {-# SPECIALIZE (|:) :: IO a -> AheadT IO a -> AheadT IO a #-}
636+ (|:) = consM
619637
620638------------------------------------------------------------------------------
621639-- Semigroup
@@ -729,10 +747,14 @@ newtype AsyncT m a = AsyncT {getAsyncT :: S.Stream m a}
729747instance IsStream AsyncT where
730748 toStream = getAsyncT
731749 fromStream = AsyncT
732− cons = consSerial
750+
733751 {-# INLINE consM #-}
734752 {-# SPECIALIZE consM :: IO a -> AsyncT IO a -> AsyncT IO a #-}
735− consM = consMSerial
753+ consM m r = fromStream $ S.consMAsync m (toStream r)
754+
755+ {-# INLINE (|:) #-}
756+ {-# SPECIALIZE (|:) :: IO a -> AsyncT IO a -> AsyncT IO a #-}
757+ (|:) = consM
736758
737759------------------------------------------------------------------------------
738760-- Semigroup
@@ -853,10 +875,14 @@ newtype WAsyncT m a = WAsyncT {getWAsyncT :: S.Stream m a}
853875instance IsStream WAsyncT where
854876 toStream = getWAsyncT
855877 fromStream = WAsyncT
856− cons = consSerial
878+
857879 {-# INLINE consM #-}
858880 {-# SPECIALIZE consM :: IO a -> WAsyncT IO a -> WAsyncT IO a #-}
859− consM = consMSerial
881+ consM m r = fromStream $ S.consMWAsync m (toStream r)
882+
883+ {-# INLINE (|:) #-}
884+ {-# SPECIALIZE (|:) :: IO a -> WAsyncT IO a -> WAsyncT IO a #-}
885+ (|:) = consM
860886
861887------------------------------------------------------------------------------
862888-- Semigroup
@@ -992,10 +1018,14 @@ newtype ParallelT m a = ParallelT {getParallelT :: S.Stream m a}
9921018instance IsStream ParallelT where
9931019 toStream = getParallelT
9941020 fromStream = ParallelT
995− cons = consSerial
1021+
9961022 {-# INLINE consM #-}
9971023 {-# SPECIALIZE consM :: IO a -> ParallelT IO a -> ParallelT IO a #-}
998− consM = consMSerial
1024+ consM m r = fromStream $ S.consMParallel m (toStream r)
1025+
1026+ {-# INLINE (|:) #-}
1027+ {-# SPECIALIZE (|:) :: IO a -> ParallelT IO a -> ParallelT IO a #-}
1028+ (|:) = consM
9991029
10001030------------------------------------------------------------------------------
10011031-- Semigroup
@@ -1068,11 +1098,17 @@ type ZipStream = ZipSerialM
10681098instance IsStream ZipSerialM where
10691099 toStream = getZipSerialM
10701100 fromStream = ZipSerialM
1071− cons = consSerial
1101+
10721102 {-# INLINE consM #-}
10731103 {-# SPECIALIZE consM :: IO a -> ZipSerialM IO a -> ZipSerialM IO a #-}
1104+ consM :: Monad m => m a -> ZipSerialM m a -> ZipSerialM m a
10741105 consM = consMSerial
10751106
1107+ {-# INLINE (|:) #-}
1108+ {-# SPECIALIZE (|:) :: IO a -> ZipSerialM IO a -> ZipSerialM IO a #-}
1109+ (|:) :: Monad m => m a -> ZipSerialM m a -> ZipSerialM m a
1110+ (|:) = consMSerial
1111+
10761112instance Monad m => Applicative (ZipSerialM m) where
10771113 pure = ZipSerialM . S.repeat
10781114 m1 <*> m2 = fromStream $ S.zipWith id (toStream m1) (toStream m2)
@@ -1104,11 +1140,17 @@ newtype ZipAsyncM m a = ZipAsyncM {getZipAsyncM :: S.Stream m a}
11041140instance IsStream ZipAsyncM where
11051141 toStream = getZipAsyncM
11061142 fromStream = ZipAsyncM
1107− cons = consSerial
1143+
11081144 {-# INLINE consM #-}
11091145 {-# SPECIALIZE consM :: IO a -> ZipAsyncM IO a -> ZipAsyncM IO a #-}
1146+ consM :: Monad m => m a -> ZipAsyncM m a -> ZipAsyncM m a
11101147 consM = consMSerial
11111148
1149+ {-# INLINE (|:) #-}
1150+ {-# SPECIALIZE (|:) :: IO a -> ZipAsyncM IO a -> ZipAsyncM IO a #-}
1151+ (|:) :: Monad m => m a -> ZipAsyncM m a -> ZipAsyncM m a
1152+ (|:) = consMSerial
1153+
11121154instance MonadAsync m => Applicative (ZipAsyncM m) where
11131155 pure = ZipAsyncM . S.repeat
11141156 m1 <*> m2 = fromStream $ S.zipAsyncWith id (toStream m1) (toStream m2)
src/Streamly/Tutorial.hs+17
@@ -307,6 +307,15 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
307307-- @
308308--
309309-- For more ways to construct a stream see the module "Streamly.Prelude".
310+--
311+-- Note that monadic generation functions e.g. 'consM', 'unfoldrM',
312+-- 'replicateM', 'repeatM', 'iterateM' and 'fromFoldableM' etc work
313+-- concurrently when used with appropriate stream type combinator. The pure
314+-- versions do not work concurrently, however you can use the monadic versions
315+-- even for pure computations to get the concurrent generation capability where
316+-- required.
317+--
318+-- TBD add more details and examples about concurrent generation.
310319
311320-- $eliminating
312321--
@@ -353,6 +362,14 @@ import Control.Monad.Trans.Class (MonadTrans (lift))
353362-- & S.'zipWith' (\\n s -> show n ++ " " ++ s) (S.'fromFoldable' [1..])
354363-- & S.'mapM' putStrLn
355364-- @
365+--
366+-- Note that monadic transformation functions 'mapM' and 'sequence' work
367+-- concurrently when used with appropriate stream type combinator. The pure
368+-- versions do not work concurrently, however you can use the monadic versions
369+-- even for pure computations to get the concurrent transformation capability
370+-- where required.
371+--
372+-- TBD add more details and examples about concurrent transformation.
356373
357374-- $semigroup
358375--
streamly.cabal+1
@@ -171,6 +171,7 @@ test-suite properties
171171 hs-source-dirs: test
172172 ghc-options: -O0 -Wall -threaded -with-rtsopts=-N4
173173 if flag(dev)
174+ cpp-options: -DDEVBUILD
174175 ghc-options: -Wmissed-specialisations
175176 -Wall-missed-specialisations
176177 if impl(ghc >= 8.0)
test/Prop.hs+158−26
@@ -2,14 +2,17 @@
22
33module Main (main) where
44
5+import Control.Exception (BlockedIndefinitelyOnMVar(..), catch)
56import Control.Monad (when)
67import Control.Applicative (ZipList(..))
7−import Control.Monad (replicateM)
8+import Control.Concurrent (MVar, takeMVar, putMVar, newEmptyMVar)
9+import Control.Monad (replicateM, replicateM_)
10+import Control.Monad.IO.Class (liftIO)
811import Data.List (sort, foldl', scanl')
912import GHC.Word (Word8)
1013
1114import Test.Hspec.QuickCheck (prop)
12−import Test.QuickCheck (counterexample, Property)
15+import Test.QuickCheck (counterexample, Property, withMaxSuccess)
1316import Test.QuickCheck.Monadic (run, monadicIO, monitor, assert, PropertyM)
1417
1518import Test.Hspec
@@ -18,6 +21,13 @@ import Streamly
1821import Streamly.Prelude ((.:), nil)
1922import qualified Streamly.Prelude as A
2023
24+maxTestCount :: Int
25+#ifdef DEVBUILD
26+maxTestCount = 100
27+#else
28+maxTestCount = 10
29+#endif
30+
2131singleton :: IsStream t => a -> t m a
2232singleton a = a .: nil
2333
@@ -59,6 +69,91 @@ transformFromList constr eq listOp op a =
5969 let list = listOp a
6070 equals eq stream list
6171
72+dbgMVar :: String -> IO a -> IO a
73+dbgMVar label action =
74+ action `catch` \BlockedIndefinitelyOnMVar ->
75+ error $ label ++ " " ++ "BlockedIndefinitelyOnMVar"
76+
77+-- | first n actions takeMVar and the last action performs putMVar n times
78+mvarSequenceOp :: MVar () -> Word8 -> Word8 -> IO Word8
79+mvarSequenceOp mv n x = do
80+ let msg = show x ++ "/" ++ show n
81+ if x < n
82+ then dbgMVar ("take mvarSequenceOp " ++ msg) (takeMVar mv) >> return x
83+ else dbgMVar ("put mvarSequenceOp" ++ msg)
84+ (replicateM_ (fromIntegral n) (putMVar mv ())) >> return x
85+
86+concurrentMapM
87+ :: ([Word8] -> t IO Word8)
88+ -> ([Word8] -> [Word8] -> Bool)
89+ -> (Word8 -> MVar () -> t IO Word8 -> SerialT IO Word8)
90+ -> Word8
91+ -> Property
92+concurrentMapM constr eq op n =
93+ monadicIO $ do
94+ let list = [0..n]
95+ stream <- run $ do
96+ mv <- newEmptyMVar :: IO (MVar ())
97+ (A.toList . (op n mv)) (constr list)
98+ equals eq stream list
99+
100+concurrentFromFoldable
101+ :: IsStream t
102+ => ([Word8] -> [Word8] -> Bool)
103+ -> (t IO Word8 -> SerialT IO Word8)
104+ -> Word8
105+ -> Property
106+concurrentFromFoldable eq op n =
107+ monadicIO $ do
108+ let list = [0..n]
109+ stream <- run $ do
110+ mv <- newEmptyMVar :: IO (MVar ())
111+ (A.toList . op) (A.fromFoldableM (map (mvarSequenceOp mv n) list))
112+ equals eq stream list
113+
114+sourceUnfoldrM :: IsStream t => MVar () -> Word8 -> t IO Word8
115+sourceUnfoldrM mv n = A.unfoldrM step 0
116+ where
117+ -- argument must be integer to avoid overflow of word8 at 255
118+ step :: Int -> IO (Maybe (Word8, Int))
119+ step cnt = do
120+ let msg = show cnt ++ "/" ++ show n
121+ if cnt > fromIntegral n
122+ then return Nothing
123+ else do
124+ liftIO $ dbgMVar ("put sourceUnfoldrM " ++ msg) (putMVar mv ())
125+ return (Just (fromIntegral cnt, cnt + 1))
126+
127+concurrentUnfoldrM
128+ :: IsStream t
129+ => ([Word8] -> [Word8] -> Bool)
130+ -> (t IO Word8 -> SerialT IO Word8)
131+ -> Word8
132+ -> Property
133+concurrentUnfoldrM eq op n =
134+ monadicIO $ do
135+ let list = [0..n]
136+ stream <- run $ do
137+ -- putStrLn $ "concurrentUnfoldrM: " ++ show n
138+ mv <- newEmptyMVar :: IO (MVar ())
139+ -- since unfoldr happens in parallel with the stream processing we
140+ -- can do two takeMVar in one iteration. If it is not parallel then
141+ -- this will not work and the test will fail.
142+ A.toList $ do
143+ x <- op (sourceUnfoldrM mv n)
144+ let msg = show x ++ "/" ++ show n
145+ if even x
146+ then do
147+ liftIO $ dbgMVar ("first take concurrentUnfoldrM " ++ msg)
148+ (takeMVar mv)
149+ if n > x
150+ then liftIO $ dbgMVar ("second take concurrentUnfoldrM " ++ msg)
151+ (takeMVar mv)
152+ else return ()
153+ else return ()
154+ return x
155+ equals eq stream list
156+
62157foldFromList
63158 :: ([Int] -> t IO Int)
64159 -> (t IO Int -> SerialT IO Int)
@@ -147,7 +242,29 @@ transformOps constr desc t eq = do
147242 prop (desc ++ " dropWhile > 0") $
148243 transform (dropWhile (> 0)) $ t . (A.dropWhile (> 0))
149244 prop (desc ++ " scan") $ transform (scanl' (+) 0) $ t . (A.scanl' (+) 0)
150− prop (desc ++ "reverse") $ transform reverse $ t . A.reverse
245+ prop (desc ++ " reverse") $ transform reverse $ t . A.reverse
246+
247+concurrentOps
248+ :: IsStream t
249+ => ([Word8] -> t IO Word8)
250+ -> String
251+ -> (t IO Word8 -> SerialT IO Word8)
252+ -> ([Word8] -> [Word8] -> Bool)
253+ -> Spec
254+concurrentOps constr desc t eq = do
255+ prop (desc ++ " fromFoldableM") $ withMaxSuccess maxTestCount $
256+ concurrentFromFoldable eq t
257+ prop (desc ++ " unfoldrM") $ withMaxSuccess maxTestCount $
258+ concurrentUnfoldrM eq t
259+ -- we pass it the length of the stream n and an mvar mv.
260+ -- The stream is [0..n]. The threads communicate in such a way that the
261+ -- actions coming first in the stream are dependent on the last action. So
262+ -- if the stream is not processed concurrently it will block forever.
263+ -- Note that if the size of the stream is bigger than the thread limit
264+ -- then it will block even if it is concurrent.
265+ prop (desc ++ " mapM") $ withMaxSuccess maxTestCount $
266+ concurrentMapM constr eq $ \n mv stream ->
267+ t $ A.mapM (mvarSequenceOp mv n) stream
151268
152269wrapMaybe :: Eq a1 => ([a1] -> a2) -> [a1] -> Maybe a2
153270wrapMaybe f =
@@ -223,10 +340,11 @@ applicativeOps
223340 -> ([(Int, Int)] -> [(Int, Int)] -> Bool)
224341 -> ([Int], [Int])
225342 -> Property
226−applicativeOps constr t eq (a, b) = monadicIO $ do
227− stream <- run ((A.toList . t) ((,) <$> (constr a) <*> (constr b)))
228− let list = (,) <$> a <*> b
229− equals eq stream list
343+applicativeOps constr t eq (a, b) = withMaxSuccess maxTestCount $
344+ monadicIO $ do
345+ stream <- run ((A.toList . t) ((,) <$> (constr a) <*> (constr b)))
346+ let list = (,) <$> a <*> b
347+ equals eq stream list
230348
231349zipApplicative
232350 :: (IsStream t, Applicative (t IO))
@@ -235,14 +353,15 @@ zipApplicative
235353 -> ([(Int, Int)] -> [(Int, Int)] -> Bool)
236354 -> ([Int], [Int])
237355 -> Property
238−zipApplicative constr t eq (a, b) = monadicIO $ do
239− stream1 <- run ((A.toList . t) ((,) <$> (constr a) <*> (constr b)))
240− stream2 <- run ((A.toList . t) (pure (,) <*> (constr a) <*> (constr b)))
241− stream3 <- run ((A.toList . t) (A.zipWith (,) (constr a) (constr b)))
242− let list = getZipList $ (,) <$> ZipList a <*> ZipList b
243− equals eq stream1 list
244− equals eq stream2 list
245− equals eq stream3 list
356+zipApplicative constr t eq (a, b) = withMaxSuccess maxTestCount $
357+ monadicIO $ do
358+ stream1 <- run ((A.toList . t) ((,) <$> (constr a) <*> (constr b)))
359+ stream2 <- run ((A.toList . t) (pure (,) <*> (constr a) <*> (constr b)))
360+ stream3 <- run ((A.toList . t) (A.zipWith (,) (constr a) (constr b)))
361+ let list = getZipList $ (,) <$> ZipList a <*> ZipList b
362+ equals eq stream1 list
363+ equals eq stream2 list
364+ equals eq stream3 list
246365
247366zipMonadic
248367 :: (IsStream t, Monad (t IO))
@@ -251,7 +370,7 @@ zipMonadic
251370 -> ([(Int, Int)] -> [(Int, Int)] -> Bool)
252371 -> ([Int], [Int])
253372 -> Property
254−zipMonadic constr t eq (a, b) =
373+zipMonadic constr t eq (a, b) = withMaxSuccess maxTestCount $
255374 monadicIO $ do
256375 stream1 <-
257376 run
@@ -272,7 +391,7 @@ monadThen
272391 -> ([Int] -> [Int] -> Bool)
273392 -> ([Int], [Int])
274393 -> Property
275−monadThen constr t eq (a, b) = monadicIO $ do
394+monadThen constr t eq (a, b) = withMaxSuccess maxTestCount $ monadicIO $ do
276395 stream <- run ((A.toList . t) ((constr a) >> (constr b)))
277396 let list = a >> b
278397 equals eq stream list
@@ -284,7 +403,7 @@ monadBind
284403 -> ([Int] -> [Int] -> Bool)
285404 -> ([Int], [Int])
286405 -> Property
287−monadBind constr t eq (a, b) =
406+monadBind constr t eq (a, b) = withMaxSuccess maxTestCount $
288407 monadicIO $ do
289408 stream <-
290409 run
@@ -295,6 +414,12 @@ monadBind constr t eq (a, b) =
295414
296415main :: IO ()
297416main = hspec $ do
417+ let folded :: IsStream t => [a] -> t IO a
418+ folded = serially . (\xs ->
419+ case xs of
420+ [x] -> return x -- singleton stream case
421+ _ -> foldMapWith (<>) return xs
422+ )
298423 describe "Construction" $ do
299424 -- XXX test for all types of streams
300425 prop "serially replicateM" $ constructWithReplicateM serially
@@ -307,12 +432,6 @@ main = hspec $ do
307432 A.toList . serially . (A.take 100) $ A.iterateM addM (0 :: Int)
308433 `shouldReturn` (take 100 $ iterate (+ 1) 0)
309434
310− let folded :: IsStream t => [a] -> t IO a
311− folded = serially . (\xs ->
312− case xs of
313− [x] -> return x -- singleton stream case
314− _ -> foldMapWith (<>) return xs
315− )
316435 describe "Functor operations" $ do
317436 functorOps A.fromFoldable "serially" serially (==)
318437 functorOps folded "serially folded" serially (==)
@@ -358,8 +477,10 @@ main = hspec $ do
358477
359478 describe "Zip operations" $ do
360479 prop "zipSerially applicative" $ zipApplicative A.fromFoldable zipSerially (==)
361− -- XXX this hangs
362− -- prop "zipAsyncly applicative" $ zipApplicative zipAsyncly (==)
480+ prop "zipSerially applicative folded" $ zipApplicative folded zipSerially (==)
481+ prop "zipAsyncly applicative" $ zipApplicative A.fromFoldable zipAsyncly (==)
482+ prop "zipAsyncly applicative folded" $ zipApplicative folded zipAsyncly (==)
483+
363484 prop "zip monadic serially" $ zipMonadic A.fromFoldable serially (==)
364485 prop "zip monadic serially folded" $ zipMonadic folded serially (==)
365486 prop "zip monadic aheadly" $ zipMonadic A.fromFoldable aheadly (==)
@@ -432,6 +553,17 @@ main = hspec $ do
432553 transformOpsWord8 folded "wAsyncly folded" wAsyncly
433554 transformOpsWord8 folded "parallely folded" parallely
434555
556+ describe "Stream concurrent operations" $ do
557+ concurrentOps A.fromFoldable "aheadly" aheadly (==)
558+ concurrentOps A.fromFoldable "asyncly" asyncly sortEq
559+ concurrentOps A.fromFoldable "wAsyncly" wAsyncly sortEq
560+ concurrentOps A.fromFoldable "parallely" parallely sortEq
561+
562+ concurrentOps folded "aheadly folded" aheadly (==)
563+ concurrentOps folded "asyncly folded" asyncly sortEq
564+ concurrentOps folded "wAsyncly folded" wAsyncly sortEq
565+ concurrentOps folded "parallely folded" parallely sortEq
566+
435567 describe "Stream elimination operations" $ do
436568 eliminationOps A.fromFoldable "serially" serially
437569 eliminationOps A.fromFoldable "aheadly" aheadly