conduino 0.2.0.0 → 0.2.1.0
raw patch · 6 files changed
+844/−52 lines, 6 filesdep +exceptionsPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: exceptions
API changes (from Hackage documentation)
- Data.Conduino: pattern PipeList :: Monad m => ListT m (Maybe a) -> Pipe () a u m ()
+ Data.Conduino: (&|) :: Monad m => Pipe a b u m v -> Pipe b c v m r -> Pipe a c u m (v, r)
+ Data.Conduino: (|.) :: Monad m => Pipe a b u m v -> Pipe b c v m r -> Pipe a c u m v
+ Data.Conduino: feedPipe :: Monad m => [i] -> Pipe i o u m a -> m ([o], Either (i -> Pipe i o u m a) ([i], a))
+ Data.Conduino: feedPipeEither :: Monad m => [i] -> Pipe i o u m a -> m ([o], Either (Either u i -> Pipe i o u m a) ([i], a))
+ Data.Conduino: feedbackPipe :: Monad m => Pipe x x u m a -> Pipe x x u m a
+ Data.Conduino: fuseBoth :: Monad m => Pipe a b u m v -> Pipe b c v m r -> Pipe a c u m (v, r)
+ Data.Conduino: fuseBothMaybe :: Monad m => Pipe a b u m v -> Pipe b c v m r -> Pipe a c u m (Maybe v, r)
+ Data.Conduino: fuseUpstream :: Monad m => Pipe a b u m v -> Pipe b c v m r -> Pipe a c u m v
+ Data.Conduino: hoistPipe :: (Monad m, Monad n) => (forall x. m x -> n x) -> Pipe i o u m a -> Pipe i o u n a
+ Data.Conduino: squeezePipe :: Monad m => Pipe i o u m a -> m ([o], Either (i -> Pipe i o u m a) a)
+ Data.Conduino: squeezePipeEither :: Monad m => Pipe i o u m a -> m ([o], Either (Either u i -> Pipe i o u m a) a)
+ Data.Conduino: zipSource :: Monad m => Pipe () (a -> b) u m () -> Pipe () a v m () -> Pipe () b w m ()
+ Data.Conduino.Combinators: iterM :: Monad m => (i -> m ()) -> Pipe i i u m u
+ Data.Conduino.Internal: instance Control.Monad.Catch.MonadCatch m => Control.Monad.Catch.MonadCatch (Data.Conduino.Internal.Pipe i o u m)
+ Data.Conduino.Internal: instance Control.Monad.Catch.MonadThrow m => Control.Monad.Catch.MonadThrow (Data.Conduino.Internal.Pipe i o u m)
+ Data.Conduino.Internal: instance GHC.Base.Alternative m => GHC.Base.Alternative (Data.Conduino.Internal.Pipe i o u m)
+ Data.Conduino.Internal: instance GHC.Base.MonadPlus m => GHC.Base.MonadPlus (Data.Conduino.Internal.Pipe i o u m)
+ Data.Conduino.Internal: pAwaitF :: forall m_a9B6 i_a6LI o_a6LJ u_a6LK. MonadFree (PipeF i_a6LI o_a6LJ u_a6LK) m_a9B6 => m_a9B6 (Either u_a6LK i_a6LI)
+ Data.Conduino.Internal: pYieldF :: forall m_a9Dt i_a6LI o_a6LJ u_a6LK. MonadFree (PipeF i_a6LI o_a6LJ u_a6LK) m_a9Dt => o_a6LJ -> m_a9Dt ()
+ Data.Conduino.Internal: runStateP :: Monad m => s -> Pipe i o u (StateT s m) a -> Pipe i o u m (a, s)
+ Data.Conduino.Internal: withRecPipe :: (Monad m, Monad n) => (RecPipe i o u m a -> RecPipe j p v n b) -> Pipe i o u m a -> Pipe j p v n b
+ Data.Conduino.Lift: catchP :: Monad m => Pipe i o u m (Either SomeException a) -> Pipe i o u (CatchT m) a
+ Data.Conduino.Lift: evalRWSP :: (Monad m, Monoid w) => r -> s -> Pipe i o u (RWST r w s m) a -> Pipe i o u m (a, w)
+ Data.Conduino.Lift: evalRWSPS :: (Monad m, Monoid w) => r -> s -> Pipe i o u (RWST r w s m) a -> Pipe i o u m (a, w)
+ Data.Conduino.Lift: evalStateP :: Monad m => s -> Pipe i o u (StateT s m) a -> Pipe i o u m a
+ Data.Conduino.Lift: evalStatePS :: Monad m => s -> Pipe i o u (StateT s m) a -> Pipe i o u m a
+ Data.Conduino.Lift: exceptP :: Monad m => Pipe i o u m (Either e a) -> Pipe i o u (ExceptT e m) a
+ Data.Conduino.Lift: execRWSP :: (Monad m, Monoid w) => r -> s -> Pipe i o u (RWST r w s m) a -> Pipe i o u m (s, w)
+ Data.Conduino.Lift: execRWSPS :: (Monad m, Monoid w) => r -> s -> Pipe i o u (RWST r w s m) a -> Pipe i o u m (s, w)
+ Data.Conduino.Lift: execStateP :: Monad m => s -> Pipe i o u (StateT s m) a -> Pipe i o u m s
+ Data.Conduino.Lift: execStatePS :: Monad m => s -> Pipe i o u (StateT s m) a -> Pipe i o u m s
+ Data.Conduino.Lift: execWriterP :: (Monad m, Monoid w) => Pipe i o u (WriterT w m) a -> Pipe i o u m w
+ Data.Conduino.Lift: execWriterPS :: (Monad m, Monoid w) => Pipe i o u (WriterT w m) a -> Pipe i o u m w
+ Data.Conduino.Lift: readerP :: Monad m => (r -> Pipe i o u m a) -> Pipe i o u (ReaderT r m) a
+ Data.Conduino.Lift: runCatchP :: Monad m => Pipe i o u (CatchT m) a -> Pipe i o u m (Either SomeException a)
+ Data.Conduino.Lift: runExceptP :: Monad m => Pipe i o u (ExceptT e m) a -> Pipe i o u m (Either e a)
+ Data.Conduino.Lift: runExceptP_ :: Monad m => Pipe i o u (ExceptT e m) a -> Pipe i o u m ()
+ Data.Conduino.Lift: runRWSP :: (Monad m, Monoid w) => r -> s -> Pipe i o u (RWST r w s m) a -> Pipe i o u m (a, s, w)
+ Data.Conduino.Lift: runRWSPS :: (Monad m, Monoid w) => r -> s -> Pipe i o u (RWST r w s m) a -> Pipe i o u m (a, s, w)
+ Data.Conduino.Lift: runReaderP :: Monad m => r -> Pipe i o u (ReaderT r m) a -> Pipe i o u m a
+ Data.Conduino.Lift: runStateP :: Monad m => s -> Pipe i o u (StateT s m) a -> Pipe i o u m (a, s)
+ Data.Conduino.Lift: runStatePS :: Monad m => s -> Pipe i o u (StateT s m) a -> Pipe i o u m (a, s)
+ Data.Conduino.Lift: runWriterP :: (Monad m, Monoid w) => Pipe i o u (WriterT w m) a -> Pipe i o u m (a, w)
+ Data.Conduino.Lift: runWriterPS :: (Monad m, Monoid w) => Pipe i o u (WriterT w m) a -> Pipe i o u m (a, w)
+ Data.Conduino.Lift: rwsP :: (Monad m, Monoid w) => (r -> s -> Pipe i o u m (a, s, w)) -> Pipe i o u (RWST r w s m) a
+ Data.Conduino.Lift: rwsPS :: (Monad m, Monoid w) => (r -> s -> Pipe i o u m (a, s, w)) -> Pipe i o u (RWST r w s m) a
+ Data.Conduino.Lift: stateP :: Monad m => (s -> Pipe i o u m (a, s)) -> Pipe i o u (StateT s m) a
+ Data.Conduino.Lift: statePS :: Monad m => (s -> Pipe i o u m (a, s)) -> Pipe i o u (StateT s m) a
+ Data.Conduino.Lift: writerP :: (Monad m, Monoid w) => Pipe i o u m (a, w) -> Pipe i o u (WriterT w m) a
+ Data.Conduino.Lift: writerPS :: (Monad m, Monoid w) => Pipe i o u m (a, w) -> Pipe i o u (WriterT w m) a
- Data.Conduino: infixr 2 .|
+ Data.Conduino: infixr 2 |.
Files
- CHANGELOG.md +20/−0
- conduino.cabal +4/−2
- src/Data/Conduino.hs +191/−32
- src/Data/Conduino/Combinators.hs +8/−0
- src/Data/Conduino/Internal.hs +100/−18
- src/Data/Conduino/Lift.hs +521/−0
CHANGELOG.md view
@@ -1,6 +1,26 @@ Changelog ========= +Version 0.2.1.0+---------------++*October 30, 2019*++<https://github.com/mstksg/conduino/releases/tag/v0.2.1.0>++* `hoistPipe` exported from *Data.Conduit*+* A handful of pipe primitive combinators added to *Data.Conduit*, including:+ * `feedbackPipe`+ * `zipPipe`+ * `&|` / `fuseBoth`+ * `|.` / `fuseUpstream`+ * `fuseBothMaybe`+* Some pipe runners added to *Data.Conduit*, including:+ * `squeezePipe` / `squeezePipeEither`+ * `feedPipe` / `feedPipeEither`+* *Data.Conduit.Lift* module, for working with monad transformers+* `iterM` added to *Data.Conduit.Combinators*+ Version 0.2.0.0 ---------------
conduino.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: e4f50c4a474c128ace934290082d6a4eda455370d44bb3efb0192215a4fbcd1b+-- hash: 52a1f0db85b8a281a18d41d489266853a6510e8e6ea76c959b68bf3cbb5ac0ce name: conduino-version: 0.2.0.0+version: 0.2.1.0 synopsis: Lightweight composable continuation-based stream processors description: A lightweight continuation-based stream processing library. .@@ -38,6 +38,7 @@ Data.Conduino Data.Conduino.Combinators Data.Conduino.Internal+ Data.Conduino.Lift other-modules: Paths_conduino hs-source-dirs:@@ -47,6 +48,7 @@ base >=4.11 && <5 , bytestring , containers+ , exceptions , free , list-transformer , mtl
src/Data/Conduino.hs view
@@ -5,6 +5,7 @@ {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeInType #-} {-# LANGUAGE ViewPatterns #-} @@ -23,36 +24,36 @@ -- A "prelude" of useful pipes can be found in "Data.Conduino.Combinators". -- -- == Why a stream processing library?--- +-- -- A stream processing library is a way to stream processors in a /composable/ way: -- instead of defining your entire stream processing function as a single -- recursive loop with some global state, instead think about each "stage" of the process, -- and isolate each state to its own segment. Each component can contain its own -- isolated state:--- +-- -- >>> runPipePure $ sourceList [1..10] -- .| scan (+) 0 -- .| sinkList -- [1,3,6,10,15,21,28,36,45,55]--- +-- -- All of these components have internal "state":--- +-- -- * @sourceList@ keeps track of "which" item in the list to yield next -- * @scan@ keeps track of the current running sum -- * @sinkList@ keeps track of all items that have been seen so far, as a list--- +-- -- They all work together without knowing any other component's internal state, so -- you can write your total streaming function without concerning yourself, at -- each stage, with the entire part.--- +-- -- In addition, there are useful functions to "combine" stream processors:--- +-- -- * 'zipSink' combines sinks in an "and" sort of way: combine two sinks in -- parallel and finish when all finish. -- * 'altSink' combines sinks in an "or" sort of way: combine two sinks in -- parallel and finish when any of them finish -- * 'zipSource' combines sources in parallel and collate their outputs.--- +-- -- Stream processing libraries are also useful for streaming composition of -- monadic effects (like IO or State), as well. --@@ -60,11 +61,22 @@ Pipe , (.|) , runPipe, runPipePure+ -- * Primitives , awaitEither, await, awaitWith, awaitSurely, awaitForever, yield+ -- * Special chaining+ , (&|), (|.)+ , fuseBoth, fuseUpstream, fuseBothMaybe+ -- * Incremental running+ , squeezePipe, squeezePipeEither+ , feedPipe, feedPipeEither+ -- * Pipe transformers , mapInput, mapOutput, mapUpRes, trimapPipe+ , hoistPipe+ , feedbackPipe -- * Wrappers , ZipSource(..) , unconsZipSource+ , zipSource , ZipSink(..) , zipSink, altSink -- * Generators@@ -78,11 +90,15 @@ import Control.Monad.Trans.Class import Control.Monad.Trans.Free (FreeT(..), FreeF(..)) import Control.Monad.Trans.Free.Church+import Control.Monad.Trans.State+import Data.Bifunctor import Data.Conduino.Internal import Data.Functor import Data.Functor.Identity+import Data.Sequence (Seq(..)) import Data.Void import List.Transformer (ListT(..), Step(..))+import qualified Data.Sequence as Seq import qualified List.Transformer as LT -- | Await input from upstream. Will block until upstream 'yield's.@@ -195,6 +211,77 @@ runPipePure :: Pipe () Void Void Identity a -> a runPipePure = runIdentity . runPipe +-- | Repeatedly run 'squeezePipe' by giving it items from an input list.+-- Returns the outputs observed, and 'Left' if the input list was exhausted+-- with more input expected, or 'Right' if the pipe terminated, with the+-- leftover inputs and output result.+--+-- @since 0.2.1.0+feedPipe+ :: Monad m+ => [i]+ -> Pipe i o u m a+ -> m ([o], Either (i -> Pipe i o u m a) ([i], a))+feedPipe xs = (fmap . second . first) (. Right)+ . feedPipeEither xs++-- | Repeatedly run 'squeezePipeEither' by giving it items from an input+-- list. Returns the outputs observed, and 'Left' if the input list was+-- exhausted with more input expected (or a @u@ terminating upstream+-- value), or 'Right' if the pipe terminated, with the leftover inputs and+-- output result.+--+-- @since 0.2.1.0+feedPipeEither+ :: Monad m+ => [i]+ -> Pipe i o u m a+ -> m ([o], Either (Either u i -> Pipe i o u m a) ([i], a))+feedPipeEither xs p = do+ (zs, r) <- squeezePipeEither p+ case r of+ Left n -> case xs of+ [] -> pure (zs, Left n)+ y:ys -> first (zs ++) <$> feedPipeEither ys (n (Right y))+ Right z -> pure (zs, Right (xs, z))++-- | "Squeeze" a pipe by extracting all output that can be extracted+-- before any input is requested. Returns a 'Left' if the pipe eventually+-- does request input (as a continuation on the new input), or a 'Right' if+-- the pipe terminates with a value before ever asking for input.+--+-- @since 0.2.1.0+squeezePipe+ :: Monad m+ => Pipe i o u m a+ -> m ([o], Either (i -> Pipe i o u m a) a)+squeezePipe = (fmap . second . first) (. Right)+ . squeezePipeEither++-- | "Squeeze" a pipe by extracting all output that can be extracted before+-- any input is requested. Returns a 'Left' if the pipe eventually does+-- request input (as a continuation on the new input, or a terminating @u@+-- value), or a 'Right' if the pipe terminates with a value before ever+-- asking for input.+--+-- @since 0.2.1.0+squeezePipeEither+ :: Monad m+ => Pipe i o u m a+ -> m ([o], Either (Either u i -> Pipe i o u m a) a)+squeezePipeEither p = runFT (pipeFree p)+ (pure . ([],) . Right)+ (\pNext -> \case+ PAwaitF f g -> pure . ([],) . Left $ (unSqueeze =<<) . lift . pNext . either f g+ PYieldF o x -> first (o:) <$> pNext x+ )+ where+ unSqueeze (os, nxt) = do+ mapM_ yield os+ case nxt of+ Left f -> f =<< awaitEither+ Right a -> pure a+ -- | The main operator for chaining pipes together. @pipe1 .| pipe2@ will -- connect the output of @pipe1@ to the input of @pipe2@. --@@ -235,6 +322,90 @@ Free (PYieldF x' y') -> runFreeT $ compPipe_ y' (g x') Free (PYieldF x y) -> pure . Free $ PYieldF x (compPipe_ p y) +-- | Useful prefix version of '&|'.+--+-- @since 0.2.1.0+fuseBoth+ :: Monad m+ => Pipe a b u m v+ -> Pipe b c v m r+ -> Pipe a c u m (v, r)+fuseBoth p q = p+ .| (q >>= exhaust)+ where+ exhaust x = go+ where+ go = awaitEither >>= \case+ Left y -> pure (y, x)+ Right _ -> go++-- | Like 'fuseBoth' and '&|', except does not wait for the upstream pipe+-- to terminate. Return 'Nothing' in the first field if the upstream pipe hasn't terminated,+-- and 'Just' if it has, with the terminating value.+--+-- @since 0.2.1.0+fuseBothMaybe :: Monad m => Pipe a b u m v -> Pipe b c v m r -> Pipe a c u m (Maybe v, r)+fuseBothMaybe p q = p+ .| (q >>= check)+ where+ check x = (,x) . either Just (const Nothing) <$> awaitEither++-- | Useful prefix version of '|.'.+--+-- @since 0.2.1.0+fuseUpstream+ :: Monad m+ => Pipe a b u m v+ -> Pipe b c v m r+ -> Pipe a c u m v+fuseUpstream p q = fst <$> fuseBoth p q++-- | Like @.|@, but get the result of /both/ pipes on termination, instead+-- of just the second. This means that @p &| q@ will only terminate with a result when+-- /both/ @p@ and @q@ terminate. (Typically, @p .| q@ would terminate as soon as+-- @q@ terminates.)+--+-- @since 0.2.1.0+(&|) :: Monad m => Pipe a b u m v -> Pipe b c v m r -> Pipe a c u m (v, r)+(&|) = fuseBoth++-- | Like @.|@, but keep the result of the /first/ pipe, instead of the+-- second. This means that @p |. q@ will only terminate with a result when+-- /both/ @p@ and @q@ terminate. (Typically, @p .| q@ would terminate as soon as+-- @q@ terminates.)+--+-- @since 0.2.1.0+(|.) :: Monad m => Pipe a b u m v -> Pipe b c v m r -> Pipe a c u m v+(|.) = fuseUpstream++infixr 2 &|+infixr 2 |.++-- | Loop a pipe into itself.+--+-- * Will feed all output back to the input+-- * Will only ask for input upstream if output is stalled.+-- * Yields all outputted values downstream, effectively duplicating them.+--+-- @since 0.2.1.0+feedbackPipe+ :: Monad m+ => Pipe x x u m a+ -> Pipe x x u m a+feedbackPipe p = fmap fst . runStateP Seq.empty $+ popper+ .| hoistPipe lift p+ .| awaitForever (\x -> lift (modify (:|> x)) *> yield x)+ where+ popper = lift get >>= \case+ Empty -> awaitEither >>= \case+ Left r -> pure r+ Right x -> yield x >> popper+ x :<| xs -> do+ lift $ put xs+ yield x+ popper+ -- | A newtype wrapper over a source (@'Pipe' () o 'Void'@) that gives it an -- alternative 'Applicative' and 'Alternative' instance, matching "ListT -- done right".@@ -278,10 +449,15 @@ instance Monad m => Applicative (ZipSource m) where pure = ZipSource . yield- ZipSource (PipeList fs) <*> ZipSource (PipeList xs) = ZipSource . PipeList . fmap Just $- uncurry ($)- <$> LT.zip (concatListT fs) (concatListT xs)+ ZipSource p <*> ZipSource q = ZipSource $ zipSource p q +-- | Takes two sources and runs them in parallel, collating their outputs.+--+-- @since 0.2.1.0+zipSource :: Monad m => Pipe () (a -> b) u m () -> Pipe () a v m () -> Pipe () b w m ()+zipSource (PipeList fs) (PipeList xs) = PipeList . fmap Just $+ uncurry ($) <$> LT.zip (concatListT fs) (concatListT xs)+ concatListT :: Monad m => ListT m (Maybe a) -> ListT m a concatListT xs = ListT $ next xs >>= \case Nil -> pure Nil@@ -320,27 +496,10 @@ :: Monad m => ListT m (Maybe o) -> Pipe i o u m ()-fromListT = fromRecPipe . go- where- go xs = FreeT $ next xs >>= \case- Nil -> pure . Pure $ ()- Cons Nothing ys -> pure . Free $ PAwaitF (\_ -> pure ()) $ \_ -> go ys- Cons (Just y) ys -> pure . Free $ PYieldF y (go ys)------ | A source is essentially equiavlent to 'ListT'. This converts----- a 'ListT' to the source it encodes.---------- See 'ZipSource' for a wrapper over 'Pipe' that gives the right 'Functor'----- and 'Alternative' instances.---fromListT--- :: Monad m--- => ListT m o--- -> Pipe i o u m ()---fromListT = fromRecPipe . go--- where--- go xs = FreeT $ next xs >>= \case--- Nil -> pure . Pure $ ()--- Cons y ys -> pure . Free $ PYieldF y (go ys)+fromListT xs = lift (next xs) >>= \case+ Nil -> pure ()+ Cons Nothing ys -> fromListT ys+ Cons (Just y) ys -> yield y *> fromListT ys -- | Given a "generator" of @o@ in @m@, return a /source/ that that -- generator encodes. Is the inverse of 'withSource'.
src/Data/Conduino/Combinators.hs view
@@ -44,6 +44,7 @@ -- * Pipes , map , mapM+ , iterM , scan , mapAccum , take@@ -282,6 +283,13 @@ -- | Map a monadic function to process every input, and yield its output. mapM :: Monad m => (i -> m o) -> Pipe i o u m u mapM f = awaitForever ((yield =<<) . lift . f)++-- | Execute a monadic function to process every input, passing through the+-- original value back downstream.+--+-- @since 0.2.1.0+iterM :: Monad m => (i -> m ()) -> Pipe i i u m u+iterM f = mapM (\x -> x <$ f x) -- | Map a pure "stateful" function over each incoming item. Give -- a function to update the state and return an output and an initial
src/Data/Conduino/Internal.hs view
@@ -31,14 +31,21 @@ , hoistPipe , RecPipe , toRecPipe, fromRecPipe+ , withRecPipe+ , runStateP+ , pAwaitF, pYieldF ) where +import Control.Applicative+import Control.Monad.Catch import Control.Monad.Except import Control.Monad.Free.Class import Control.Monad.Free.TH import Control.Monad.RWS-import Control.Monad.Trans.Free (FreeT(..))+import Control.Monad.Trans.Free (FreeT(..), FreeF(..)) import Control.Monad.Trans.Free.Church+import Control.Monad.Trans.State+import Data.Functor #if !MIN_VERSION_base(4,13,0) import Control.Monad.Fail@@ -86,22 +93,23 @@ -- to produce items. It will pump out items on its own, for pipes -- downstream to receive and process. ----- * If @o@ is 'Void', the pipe is a /sink/ --- it will never 'yield'--- anything downstream. It will consume items from things upstream, and--- produce a result (@a@) if and when it terminates.+-- * If @o@ is 'Data.Void.Void', the pipe is a /sink/ --- it will never+-- 'yield' anything downstream. It will consume items from things+-- upstream, and produce a result (@a@) if and when it terminates. ----- * If @u@ is 'Void', then the pipe's upstream is limitless, and never--- terminates. This means that you can use 'Data.Condunio.awaitSurely'--- instead of 'Data.Conduino.await', to get await a value that is--- guaranteed to come. You'll get an @i@ instead of a @'Maybe' i@.+-- * If @u@ is 'Data.Void.Void', then the pipe's upstream is limitless,+-- and never terminates. This means that you can use+-- 'Data.Conduino.awaitSurely' instead of 'Data.Conduino.await', to get+-- await a value that is guaranteed to come. You'll get an @i@ instead+-- of a @'Maybe' i@. ----- * If @a@ is 'Void', then the pipe never terminates --- it will keep on--- consuming and/or producing values forever. If this is a sink, it--- means that the sink will never terminate, and so--- 'Data.Condunio.runPipe' will also never terminate. If it is+-- * If @a@ is 'Data.Void.Void', then the pipe never terminates --- it+-- will keep on consuming and/or producing values forever. If this is+-- a sink, it means that the sink will never terminate, and so+-- 'Data.Conduino.runPipe' will also never terminate. If it is -- a source, it means that if you chain something downstream with--- 'Data.Condunio..|', that downstream pipe can use 'awaitSurely' to--- guarantee something being passed down.+-- 'Data.Conduino..|', that downstream pipe can use+-- 'Data.Conduino.awaitSurely' to guarantee something being passed down. -- -- Applicative and Monadic sequencing of pipes chains by exhaustion. --@@ -122,8 +130,8 @@ -- @ -- -- Usually you would use it by chaining together pipes with--- 'Data.Condunio..|' and then running the result with--- 'Data.Condunio.runPipe'.+-- 'Data.Conduino..|' and then running the result with+-- 'Data.Conduino.runPipe'. -- -- @ -- 'Data.Conduino.runPipe' $ someSource@@ -132,11 +140,11 @@ -- .| someSink -- @ ----- See 'Data.Condunio..|' and 'Data.Condunio.runPipe' for more information+-- See 'Data.Conduino..|' and 'Data.Conduino.runPipe' for more information -- on usage. -- -- For a "prelude" of commonly used 'Pipe's, see--- "Data.Condunio.Combinators".+-- "Data.Conduino.Combinators". -- newtype Pipe i o u m a = Pipe { pipeFree :: FT (PipeF i o u) m a } deriving@@ -151,6 +159,10 @@ , MonadWriter w , MonadError e , MonadRWS r w s+ , Alternative+ , MonadPlus+ , MonadThrow+ , MonadCatch ) instance MonadFail m => MonadFail (Pipe i o u m) where@@ -185,6 +197,10 @@ PYieldF a x -> PYieldF (g a) x -- | Transform the underlying monad of a pipe.+--+-- Note that if you are trying to work with monad transformers, this is+-- probably not what you want. See "Data.Conduino.Lift" for tools for+-- working with underlying monad transformers. hoistPipe :: (Monad m, Monad n) => (forall x. m x -> n x)@@ -221,3 +237,69 @@ -- | Convert a 'RecPipe' back into a 'Pipe'. fromRecPipe :: Monad m => RecPipe i o u m a -> Pipe i o u m a fromRecPipe = Pipe . toFT++-- | Convenint wrapper over 'toRecPipe' and 'fromRecPipe'.+--+-- @since 0.2.1.0+withRecPipe+ :: (Monad m, Monad n)+ => (RecPipe i o u m a -> RecPipe j p v n b)+ -> Pipe i o u m a+ -> Pipe j p v n b+withRecPipe f = fromRecPipe . f . toRecPipe++-- | Turn a 'Pipe' that runs over 'StateT' into a "state-modifying 'Pipe'",+-- that returns the final state when it terminates.+--+-- The main usage of this is to "isolate" the state from other pipes in the+-- same chain. For example, of @p@, @q@, and @r@ are all pipes under+-- 'StateT', then:+--+-- @+-- p+-- .| q+-- .| r+-- @+--+-- will all share underlying state, and each can modify the state that they+-- all three share. We essentially have global state.+--+-- However, if you use 'runStateP', you can all have them use different+-- encapsulated states.+--+-- @+-- void (runStateP s0 p)+-- .| void (runStateP s1 q)+-- .| runStateP s2 r+-- @+--+-- In this case, each of those three chained pipes will use their own+-- internal states, without sharing.+--+-- This is also useful if you want to chain a pipe over 'StateT' with+-- pipes that don't use state at all: for example if @a@ and @b@ are+-- "non-stateful" pipes (/not/ over 'StateT'), you can do:+--+-- @+-- a+-- .| void (runStateP s1 q)+-- .| b+-- @+--+-- And @a@ and @b@ will be none the wiser to the fact that @q@ uses+-- 'StateT' internally.+--+-- Note to avoid the usage of 'void', 'Data.Conduino.Lift.evalStateP' might+-- be more useful.+runStateP+ :: Monad m+ => s+ -> Pipe i o u (StateT s m) a+ -> Pipe i o u m (a, s)+runStateP = withRecPipe . go+ where+ go s (FreeT p) = FreeT $ runStateT p s <&> \(q, s') ->+ case q of+ Pure x -> Pure (x, s')+ Free l -> Free $ go s' <$> l+
+ src/Data/Conduino/Lift.hs view
@@ -0,0 +1,521 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE ViewPatterns #-}++-- |+-- Module : Data.Conduino.Lift+-- Copyright : (c) Justin Le 2019+-- License : BSD3+--+-- Maintainer : justin@jle.im+-- Stability : experimental+-- Portability : non-portable+--+-- Working with underlying monad transformers and 'Pipe'.+--+-- There is no "general abstraction" for dealing with each monad+-- transformer, but we can translate the semantics that each monad+-- transformer provides into meaningful 'Pipe' operations.+--+-- For example, a @'Pipe' i o u ('State' s) a@ is a pipe working over+-- stateful effects --- it can pull information and modify an underlying+-- state to do its job. It takes in @i@ and outputs @o@, using an+-- underlying state @s@.+--+-- However, such a pipe is similar to @s -> 'Pipe'+-- i o u 'Data.Functor.Identity.Identity' (a, s)@. Giving some starting+-- state, it takes in @i@ and outputs @o@, and when it completes, it+-- returns an @a@ and an @s@, the final state after all its processing is+-- done.+--+-- The /general/ idea is that:+--+-- * A pipe over a monad transformer /shares that monadic context/ over+-- /every pipe/ in a composition.+--+-- For example, if @p@, @q@, and @r@ are all pipes over 'StateT', the @p+-- .| q .| r@ will all share a common global state.+--+-- If @p@, @q@, and @r@ are all pipes over 'ExceptT', then @p .| q .| r@+-- will all short-circult fail each other: if @q@ fails, then they all+-- fail, etc.+--+-- If @p@, @q@, and @r@ are all pipes over 'WriterT' then @p .| q .| r@+-- will all accumulate to a shared global log.+--+-- If @p@, @q@, and @r@ are all pipes over 'ReaderT' then @p .| q .| r@+-- will use the same identical environment.+--+-- * Using the @runX@ family of functions ('runStateP', 'runExceptP',+-- etc.) lets you /isolate/ out the common context within a composition+-- of pipes.+--+-- For example, if @p@ is a pipe over 'StateT', then @a .| 'void' ('runStateP'+-- s0 p) .| b@, @a@ and @b@ will not be able to use the state of @p@.+--+-- If @p@ is a pipe over 'ExceptT', then in @a .| void ('runExceptP' p) .|+-- b@, a failure in @p@ will not cause all the others to fail.+--+-- Both of these representations have different advantages and+-- disadvantages, that are separate and unique for each individual monad+-- transformer on a case-by-case basis. This module provides functions on+-- such a case-by-case basis as you need them.+--+-- @since 0.2.1.0+module Data.Conduino.Lift (+ -- * State+ -- ** Lazy+ stateP, runStateP, evalStateP, execStateP+ -- ** Strict+ , statePS, runStatePS, evalStatePS, execStatePS+ -- * Except+ , exceptP, runExceptP, runExceptP_+ -- * Reader+ , readerP, runReaderP+ -- * Writer+ -- ** Lazy+ , writerP, runWriterP, execWriterP+ -- ** Strict+ , writerPS, runWriterPS, execWriterPS+ -- * RWS+ -- ** Lazy+ , rwsP, runRWSP, evalRWSP, execRWSP+ -- ** Strict+ , rwsPS, runRWSPS, evalRWSPS, execRWSPS+ -- * Catch+ , catchP, runCatchP+ ) where++import Control.Monad.Catch.Pure+import Control.Monad.Trans.Class+import Control.Monad.Trans.Except+import Control.Monad.Trans.Free+import Control.Monad.Trans.RWS (RWST(..))+import Control.Monad.Trans.Reader+import Control.Monad.Trans.State+import Control.Monad.Trans.Writer+import Data.Conduino+import Data.Conduino.Internal+import Data.Functor+import qualified Control.Monad.Trans.RWS as RWS+import qualified Control.Monad.Trans.RWS.Strict as RWSS+import qualified Control.Monad.Trans.State.Strict as SS+import qualified Control.Monad.Trans.Writer.Strict as WS++-- | Turn a "state-modifying 'Pipe'" into a 'Pipe' that runs over 'StateT',+-- so you can chain it with other 'StateT' pipes.+--+-- Note that this will /overwrite/ whatever state exists with+-- the @s@ that it gets when it terminates. If any other pipe in this+-- chain modifies or uses state, all modifications will be overwritten when+-- the @(a, s)@-producing pipe terminates.+stateP+ :: Monad m+ => (s -> Pipe i o u m (a, s))+ -> Pipe i o u (StateT s m) a+stateP f = do+ s <- lift get+ (x, s') <- hoistPipe lift (f s)+ x <$ lift (put s')++-- | Like 'runStateP', but ignoring the final result. It returns the final+-- state after the pipe succesfuly terminates.+execStateP+ :: Monad m+ => s+ -> Pipe i o u (StateT s m) a+ -> Pipe i o u m s+execStateP s = fmap snd . runStateP s++-- | Takes a 'Pipe' over 'StateT' and "hides" the state from the outside+-- world. Give an initial state --- the pipe behaves the same way, but to+-- the external user it is abstracted away. See 'runStateP' for more+-- information.+--+-- This can be cleaner than 'runStateP' because if @a@ is @()@, you+-- don't have to sprinkle in 'void' everywhere. However, it's only really+-- useful if you don't need to get the final state upon termination.+evalStateP+ :: Monad m+ => s+ -> Pipe i o u (StateT s m) a+ -> Pipe i o u m a+evalStateP s = fmap fst . runStateP s++-- | 'stateP', but for "Control.Monad.Trans.State.Strict".+statePS+ :: Monad m+ => (s -> Pipe i o u m (a, s))+ -> Pipe i o u (SS.StateT s m) a+statePS f = do+ s <- lift SS.get+ (x, s') <- hoistPipe lift (f s)+ x <$ lift (SS.put s')++-- | 'runStateP', but for "Control.Monad.Trans.State.Strict".+runStatePS+ :: Monad m+ => s+ -> Pipe i o u (SS.StateT s m) a+ -> Pipe i o u m (a, s)+runStatePS = withRecPipe . go+ where+ go s (FreeT p) = FreeT $ SS.runStateT p s <&> \(q, s') ->+ case q of+ Pure x -> Pure (x, s')+ Free l -> Free $ go s' <$> l++-- | 'execStateP', but for "Control.Monad.Trans.State.Strict".+execStatePS+ :: Monad m+ => s+ -> Pipe i o u (SS.StateT s m) a+ -> Pipe i o u m s+execStatePS s = fmap snd . runStatePS s++-- | 'evalStateP', but for "Control.Monad.Trans.State.Strict".+evalStatePS+ :: Monad m+ => s+ -> Pipe i o u (SS.StateT s m) a+ -> Pipe i o u m a+evalStatePS s = fmap fst . runStatePS s++-- | Turn a "failable-result" 'Pipe' into a pipe over 'ExceptT'.+--+-- Note that a 'throwE' failure will only ever happen when the input pipe+-- "succesfully" terminates with 'Left'. It would never happen before the+-- pipe terminates, since you don't get the @'Either' e a@ until the pipe+-- succesfully terminates.+exceptP+ :: Monad m+ => Pipe i o u m (Either e a)+ -> Pipe i o u (ExceptT e m) a+exceptP p = hoistPipe lift p >>= \case+ Left e -> lift $ throwE e+ Right x -> pure x++-- | Turn a 'Pipe' that runs over 'ExceptT' into an "early-terminating+-- 'Pipe'" that "succesfully" returns 'Left' or 'Right'.+--+-- The main usage of this is to "isolate" the short-circuiting failure of+-- 'ExceptT' to only happen within one component of a chain. For example,+-- of @p@, @q@, and @r@ are all pipes under 'ExceptT', then:+--+-- @+-- p+-- .| q+-- .| r+-- @+--+-- will short-circuit fail if /any/ of @p@, @q@, or @r@ fail. We have+-- global failure only.+--+-- However, if you use 'runExceptP', we isolate the short-circuiting+-- failure to only a single type.+--+-- @+-- void (runExceptP p)+-- .| void (runExceptP q)+-- .| runExceptP r+-- @+--+-- In this case, if (for example) @q@ fails, it won't cause the whole thing+-- to fail: it will just be the same as if @q@ succesfully terminates+-- normally.+--+-- This is also useful if you want to chain a pipe over 'ExceptT' with+-- pipes that don't have 'ExceptT' at all: for example if @a@ and @b@ are+-- "non-erroring" pipes (/not/ over 'ExceptT'), you can do:+--+-- @+-- a+-- .| void (runExceptP q)+-- .| b+-- @+--+-- And @a@ and @b@ will be none the wiser to the fact that @q@ uses+-- 'ExceptT' internally.+--+-- Note to avoid the usage of 'void', 'runExceptP_' might be more useful.+runExceptP+ :: Monad m+ => Pipe i o u (ExceptT e m) a+ -> Pipe i o u m (Either e a)+runExceptP = withRecPipe go+ where+ go (FreeT p) = FreeT $ runExceptT p <&> \case+ Left e -> Pure $ Left e+ Right (Pure x) -> Pure $ Right x+ Right (Free l) -> Free $ go <$> l++-- | A handy version of 'runExceptP' that discards its output, so it can be+-- easier to chain using '.|'. It's useful if you are using 'runExceptP'+-- to "isolate" failures from the rest of a chain.+runExceptP_+ :: Monad m+ => Pipe i o u (ExceptT e m) a+ -> Pipe i o u m ()+runExceptP_ = void . runExceptP++-- | Like 'exceptP', but for 'CatchT'. See 'exceptP' for usage details and+-- caveats. In general, can be useful for chaining with other 'CatchT'+-- pipes.+--+-- Note that a 'throwM' failure will only ever happen when the input pipe+-- "succesfully" terminates with 'Left'. It would never happen before the+-- pipe terminates, since you don't get the @'Either' 'SomeException' a@+-- until the pipe succesfully terminates.+catchP+ :: Monad m+ => Pipe i o u m (Either SomeException a)+ -> Pipe i o u (CatchT m) a+catchP p = hoistPipe lift p >>= \case+ Left e -> lift $ throwM e+ Right x -> pure x++-- | Like 'runExceptP', but for 'CatchT'. See 'runExceptP' for usage+-- details. In general, can be useful for "isolating" a 'CatchT' pipe from+-- the rest of its chain.+runCatchP+ :: Monad m+ => Pipe i o u (CatchT m) a+ -> Pipe i o u m (Either SomeException a)+runCatchP = withRecPipe go+ where+ go (FreeT p) = FreeT $ runCatchT p <&> \case+ Left e -> Pure $ Left e+ Right (Pure x) -> Pure $ Right x+ Right (Free l) -> Free $ go <$> l++-- | Turn a "parameterized 'Pipe'" into a 'Pipe' that runs over 'ReaderT',+-- so you can chain it with other 'ReaderT' pipes.+--+-- Essentially, instead of directly providing the @r@ in an @r -> 'Pipe'+-- i o u m a@, the @r@ instead comes from the globally shared environment.+readerP+ :: Monad m+ => (r -> Pipe i o u m a)+ -> Pipe i o u (ReaderT r m) a+readerP f = hoistPipe lift . f =<< lift ask++-- | Turn a pipe over 'ReaderT' into a directly parameterized pipe.+-- Instead of getting the parameter from the globally shared 'ReaderT'+-- environment, give it directly instead.+--+-- It can be useful to "ignore" a globally shared environment and just give+-- the @r@ directly and immediately.+runReaderP+ :: Monad m+ => r+ -> Pipe i o u (ReaderT r m) a+ -> Pipe i o u m a+runReaderP r = hoistPipe (`runReaderT` r)++-- | Turn a pipe returning an @(a, w)@ tuple upon termination into a pipe+-- returning @a@, logging the @w@ in an underlying 'WriterT' context.+--+-- This can be useful for composing your pipe with other 'WriterT' pipes,+-- aggregating all to a common global log.+--+-- However, be aware that this only ever 'tell's when the pipe succesfuly+-- terminates. It doesn't do "streaming logging" -- it only makes one+-- log payload at the point of succesful termination. To do streaming+-- logging (logging things as you get them), you should probably just+-- directly use 'WriterT' instead, with 'Data.Conduino.Combinators.repeatM'+-- or 'Data.Conduino.Combinators.iterM' or something similar.+writerP+ :: (Monad m, Monoid w)+ => Pipe i o u m (a, w)+ -> Pipe i o u (WriterT w m) a+writerP p = do+ (x, w) <- hoistPipe lift p+ x <$ lift (tell w)++-- | Turn a 'Pipe' that runs over 'WriterT' into a 'Pipe' that returns the+-- final log when it terminates.+--+-- The main usage of this is to "isolate" the log from other pipes in the+-- same chain. For example, of @p@, @q@, and @r@ are all pipes under+-- 'WriterT', then:+--+-- @+-- p+-- .| q+-- .| r+-- @+--+-- will all share underlying log, and all logging from any of them will+-- accumulate together in an interleaved way. It is essentially a global+-- log.+--+-- However, if you use 'runWriterP', you can all have them use different+-- encapsulated logs.+--+-- @+-- void (runWriterP p)+-- .| void (runWriterP q)+-- .| runWriterP r+-- @+--+-- In this case, each of those three chained pipes will use their own+-- internal logs, without sharing.+--+-- This is also useful if you want to chain a pipe over 'WriterT' with+-- pipes that don't use state at all: for example if @a@ and @b@ are+-- "non-logging" pipes (/not/ over 'WriterT'), you can do:+--+-- @+-- a+-- .| void (runWriterP q)+-- .| b+-- @+--+-- And @a@ and @b@ will be none the wiser to the fact that @q@ uses+-- 'WriterT' internally.+runWriterP+ :: (Monad m, Monoid w)+ => Pipe i o u (WriterT w m) a+ -> Pipe i o u m (a, w)+runWriterP = withRecPipe (go mempty)+ where+ go w (FreeT p) = FreeT $ runWriterT p <&> \(r, (w <>)->w') ->+ case r of+ Pure x -> Pure (x, w')+ Free l -> Free $ go w' <$> l++-- | 'runWriterP', but only returning the final log after succesful+-- termination.+execWriterP+ :: (Monad m, Monoid w)+ => Pipe i o u (WriterT w m) a+ -> Pipe i o u m w+execWriterP = fmap snd . runWriterP++-- | 'writerP', but for "Control.Monad.Trans.Writer.Strict".+writerPS+ :: (Monad m, Monoid w)+ => Pipe i o u m (a, w)+ -> Pipe i o u (WS.WriterT w m) a+writerPS p = do+ (x, w) <- hoistPipe lift p+ x <$ lift (WS.tell w)++-- | 'runWriterP', but for "Control.Monad.Trans.Writer.Strict".+runWriterPS+ :: (Monad m, Monoid w)+ => Pipe i o u (WS.WriterT w m) a+ -> Pipe i o u m (a, w)+runWriterPS = withRecPipe (go mempty)+ where+ go w (FreeT p) = FreeT $ WS.runWriterT p <&> \(r, (w <>)->w') ->+ case r of+ Pure x -> Pure (x, w')+ Free l -> Free $ go w' <$> l++-- | 'execWriterP', but for "Control.Monad.Trans.Writer.Strict".+execWriterPS+ :: (Monad m, Monoid w)+ => Pipe i o u (WriterT w m) a+ -> Pipe i o u m w+execWriterPS = fmap snd . runWriterP++-- | Turn a parameterized, state-transforming, log-producing 'Pipe' into+-- a 'Pipe' over 'RWST', which can be useful for chaining it with other+-- 'RWST' pipes.+--+-- See 'stateP' and 'writerP' for more details on caveats, including:+--+-- * Logging only happens when the @(a,s,w)@-returning pipe terminates.+-- There is no "streaming logging" --- the resulting @w@ is logged all+-- at once.+-- * When the @(a,s,w)@-returning pipe terminates, whatever state in the+-- 'RWST' is overwritten with the @s@ returned. If other pipes in the+-- chain modify the @s@, their modifications will be overwritten.+rwsP+ :: (Monad m, Monoid w)+ => (r -> s -> Pipe i o u m (a, s, w))+ -> Pipe i o u (RWST r w s m) a+rwsP f = do+ r <- lift RWS.ask+ s <- lift RWS.get+ (x, s', w) <- hoistPipe lift (f r s)+ lift (RWS.tell w)+ x <$ lift (RWS.put s')++-- | Turn a 'Pipe' that runs over 'RWST' into a state-modifying,+-- environment-using, log-accumulating 'Pipe'. See 'runStateP',+-- 'runWriterP', and 'runReaderP' for the uses and semantics.+runRWSP+ :: (Monad m, Monoid w)+ => r+ -> s+ -> Pipe i o u (RWST r w s m) a+ -> Pipe i o u m (a, s, w)+runRWSP r = withRecPipe . go mempty+ where+ go w s (FreeT p) = FreeT $ runRWST p r s <&> \(q, s', (w <>)->w') ->+ case q of+ Pure x -> Pure (x, s', w')+ Free l -> Free $ go w' s' <$> l++-- | 'runRWSP', but ignoring the final state.+evalRWSP+ :: (Monad m, Monoid w)+ => r+ -> s+ -> Pipe i o u (RWST r w s m) a+ -> Pipe i o u m (a, w)+evalRWSP r s = fmap (\(x,_,w) -> (x,w)) . runRWSP r s++-- | 'runRWSP', but ignoring the result value.+execRWSP+ :: (Monad m, Monoid w)+ => r+ -> s+ -> Pipe i o u (RWST r w s m) a+ -> Pipe i o u m (s, w)+execRWSP r s = fmap (\(_,s',w) -> (s',w)) . runRWSP r s++-- | 'rwsP', but for "Control.Monad.Trans.RWS.Strict".+rwsPS+ :: (Monad m, Monoid w)+ => (r -> s -> Pipe i o u m (a, s, w))+ -> Pipe i o u (RWSS.RWST r w s m) a+rwsPS f = do+ r <- lift RWSS.ask+ s <- lift RWSS.get+ (x, s', w) <- hoistPipe lift (f r s)+ lift (RWSS.tell w)+ x <$ lift (RWSS.put s')++-- | 'runRWSPS', but for "Control.Monad.Trans.RWS.Strict".+runRWSPS+ :: (Monad m, Monoid w)+ => r+ -> s+ -> Pipe i o u (RWSS.RWST r w s m) a+ -> Pipe i o u m (a, s, w)+runRWSPS r = withRecPipe . go mempty+ where+ go w s (FreeT p) = FreeT $ RWSS.runRWST p r s <&> \(q, s', (w <>)->w') ->+ case q of+ Pure x -> Pure (x, s', w')+ Free l -> Free $ go w' s' <$> l++-- | 'evalRWSPS', but for "Control.Monad.Trans.RWS.Strict".+evalRWSPS+ :: (Monad m, Monoid w)+ => r+ -> s+ -> Pipe i o u (RWSS.RWST r w s m) a+ -> Pipe i o u m (a, w)+evalRWSPS r s = fmap (\(x,_,w) -> (x,w)) . runRWSPS r s++-- | 'execRWSPS', but for "Control.Monad.Trans.RWS.Strict".+execRWSPS+ :: (Monad m, Monoid w)+ => r+ -> s+ -> Pipe i o u (RWSS.RWST r w s m) a+ -> Pipe i o u m (s, w)+execRWSPS r s = fmap (\(_,s',w) -> (s',w)) . runRWSPS r s