packages feed

streamly-0.11.0: src/Streamly/Internal/Data/Stream/Channel.hs

-- |
-- Module      : Streamly.Internal.Data.Stream.Channel
-- Copyright   : (c) 2017 Composewell Technologies
-- License     : BSD-3-Clause
-- Maintainer  : streamly@composewell.com
-- Stability   : experimental
-- Portability : GHC

module Streamly.Internal.Data.Stream.Channel
    (
      module Streamly.Internal.Data.Stream.Channel.Type

    -- ** Allocation
    , module Streamly.Internal.Data.Stream.Channel.Append
    , module Streamly.Internal.Data.Stream.Channel.Interleave
    , newChannel

    -- ** Event Processing Loop
    , module Streamly.Internal.Data.Stream.Channel.Dispatcher
    , module Streamly.Internal.Data.Stream.Channel.Consumer
    , module Streamly.Internal.Data.Stream.Channel.Operations
    , chanConcatMapK

    -- ** Evaluation
    , withChannelK
    , withChannel
    -- quiesceChannel -- wait for running tasks but do not schedule any more.
    )
where

import Streamly.Internal.Control.Concurrent (MonadAsync)
import Control.Monad.IO.Class (MonadIO(liftIO))
import Streamly.Internal.Data.Stream (Stream)
import Streamly.Internal.Control.Concurrent (askRunInIO)
import Streamly.Internal.Data.SVar.Type (adaptState)

import qualified Streamly.Internal.Data.StreamK as K

import Streamly.Internal.Data.Channel.Types
import Streamly.Internal.Data.Stream.Channel.Type
import Streamly.Internal.Data.Stream.Channel.Operations
import Streamly.Internal.Data.Stream.Channel.Append
import Streamly.Internal.Data.Stream.Channel.Interleave
import Streamly.Internal.Data.Stream.Channel.Dispatcher
import Streamly.Internal.Data.Stream.Channel.Consumer

-------------------------------------------------------------------------------
--  Channel allocation
-------------------------------------------------------------------------------

-- | Create a new concurrent stream evaluation channel. The monad
-- state used to run the stream actions is captured from the call site of
-- newChannel.
{-# INLINE newChannel #-}
newChannel :: MonadAsync m =>
    (Config -> Config) -> m (Channel m a)
newChannel modifier =
    let cfg = modifier defaultConfig
     in if getInterleaved cfg
        then newInterleaveChannel modifier
        else newAppendChannel modifier

-- | Allocate a channel and evaluate the stream concurrently using the channel
-- and the supplied evaluator function. The evaluator is run in a worker
-- thread.
{-# INLINE withChannelK #-}
withChannelK :: MonadAsync m =>
       (Config -> Config) -- ^ config modifier
    -> K.StreamK m a -- ^ input stream
    -> (Channel m b -> K.StreamK m a -> K.StreamK m b) -- ^ stream evaluator
    -> K.StreamK m b -- ^ output stream
withChannelK modifier input evaluator = K.concatEffect action

    where

    action = do
        chan <- newChannel modifier
        toChannelK chan (evaluator chan input)
        let cfg = modifier defaultConfig
        return $ fromChannelK (getCleanup cfg) chan

-- | A wrapper over 'withChannelK', converts 'Stream' to 'StreamK' and invokes
-- 'withChannelK'.
{-# INLINE withChannel #-}
withChannel :: MonadAsync m =>
       (Config -> Config)
    -> Stream m a
    -> (Channel m b -> Stream m a -> Stream m b)
    -> Stream m b
withChannel modifier input evaluator =
    let f chan stream = K.fromStream $ evaluator chan (K.toStream stream)
     in K.toStream $ withChannelK modifier (K.fromStream input) f

-------------------------------------------------------------------------------
-- Evaluator
-------------------------------------------------------------------------------

-- | @concatMapHeadK consumeTail mapHead stream@, maps a stream generation
-- function on the head element and performs a side effect on the tail.
--
-- Used for concurrent evaluation of streams using a Channel. A worker
-- evaluating the stream would queue the tail and go on to evaluate the head.
-- The tail is picked up by another worker which does the same.
{-# INLINE concatMapHeadK #-}
concatMapHeadK :: Monad m =>
       (K.StreamK m a -> m ()) -- ^ Queue the tail
    -> (a -> K.StreamK m b) -- ^ Generate a stream from the head
    -> K.StreamK m a
    -> K.StreamK m b
concatMapHeadK consumeTail mapHead stream =
    K.mkStream $ \st yld sng stp -> do
        let foldShared = K.foldStreamShared st yld sng stp
            single a = foldShared $ mapHead a
            yieldk a r = consumeTail r >> single a
         in K.foldStreamShared (adaptState st) yieldk single stp stream

-------------------------------------------------------------------------------
-- concat streams
-------------------------------------------------------------------------------

-- | 'mkEnqueue chan f returns a queuing function @enq@. @enq@ takes a
-- @stream@ and enqueues @f enq stream@ on the channel. One example of @f@ is
-- 'concatMapHeadK'. When the enqueued value with 'concatMapHeadK' as @f@ is
-- evaluated, it generates an output stream from the head and enqueues @f enq
-- tail@ on the channel. Thus whenever the enqueued stream is evaluated it
-- generates a stream from the head and queues the tail on the channel.
--
-- Note that @enq@ and runner are mutually recursive, mkEnqueue ties the
-- knot between the two.
--
{-# INLINE mkEnqueue #-}
mkEnqueue :: MonadAsync m =>
    Channel m b
    -- | @divider enq stream@
    -> ((K.StreamK m a -> m ()) -> K.StreamK m a -> K.StreamK m b)
    -- | Queuing function @enq@
    -> m (K.StreamK m a -> m ())
mkEnqueue chan runner = do
    runInIO <- askRunInIO
    return
        $ let f stream = do
                liftIO $ enqueue chan (runInIO, runner f stream)
                -- XXX In case of eager dispatch we can just directly dispatch
                -- a worker with the tail stream here rather than first queuing
                -- and then dispatching a worker which dequeues the work. The
                -- older implementation did a direct dispatch here and its perf
                -- characterstics looked much better.
                eagerDispatch chan
           in f

{-# INLINE parConcatMapChanKAll #-}
parConcatMapChanKAll :: MonadAsync m =>
    Channel m b -> (a -> K.StreamK m b) -> K.StreamK m a -> K.StreamK m b
parConcatMapChanKAll chan f stream =
   let run q = concatMapHeadK q f
    in K.concatMapEffect (`run` stream) (mkEnqueue chan run)
    -- K.parConcatMap (_appendWithChanK chan) f stream

{-# INLINE parConcatMapChanKAny #-}
parConcatMapChanKAny :: MonadAsync m =>
    Channel m b -> (a -> K.StreamK m b) -> K.StreamK m a -> K.StreamK m b
parConcatMapChanKAny chan f stream =
   let done = K.nilM (shutdown chan)
       run q = concatMapHeadK q (\x -> K.append (f x) done)
    in K.concatMapEffect (`run` stream) (mkEnqueue chan run)

{-# INLINE parConcatMapChanKFirst #-}
parConcatMapChanKFirst :: MonadAsync m =>
    Channel m b -> (a -> K.StreamK m b) -> K.StreamK m a -> K.StreamK m b
parConcatMapChanKFirst chan f stream =
   let done = K.nilM (shutdown chan)
       run q = concatMapHeadK q f
    in K.concatEffect $ do
        res <- K.uncons stream
        case res of
            Nothing -> return K.nil
            Just (h, t) -> do
                q <- mkEnqueue chan run
                q t
                return $ K.append (f h) done

-- | Make a concurrent stream evaluator from a stream, to be used in
-- 'withChannelK' or 'toChannelK'. Maps a stream generation function on each
-- element of the stream, the evaluation of the map on each element happens
-- concurrently. All the generated streams are merged together in the output of
-- the channel. The scheduling and termination behavior depends on the channel
-- settings.
--
-- Note that if you queue a stream on the channel using 'toChannelK', it will
-- be picked up by a worker and the worker would evaluate the entire stream
-- serially and emit the results on the channel. However, if you transform the
-- stream using 'parConcatMapChanK' and queue it on the channel, it
-- parallelizes the function map on each element of the stream. The simplest
-- example is @parConcatMapChanK id id@ which is equivalent to evaluating each
-- element of the stream concurrently.
--
-- A channel worker evaluating this function would enqueue the tail on the
-- channel's work queue and go on to evaluate the head generating an output
-- stream. The tail is picked up by another worker which does the same and so
-- on.
{-# INLINE chanConcatMapK #-}
chanConcatMapK :: MonadAsync m =>
       (Config -> Config)
    -> Channel m b
    -> (a -> K.StreamK m b)
    -> K.StreamK m a
    -> K.StreamK m b
chanConcatMapK modifier chan f stream = do
        let cfg = modifier defaultConfig
        case getStopWhen cfg of
            AllStop -> parConcatMapChanKAll chan f stream
            FirstStops -> parConcatMapChanKFirst chan f stream
            AnyStops -> parConcatMapChanKAny chan f stream