[Haskell-cafe] How to write Source for TChan working with LC.take?

Michael Snoyman michael at snoyman.com
Mon May 21 05:07:14 CEST 2012


I agree that this behavior is non-intuitive, but still believe it's
the necessary approach. The short answer to why it's happening is that
there's no exit path in the yield version of the function. To
understand why, let's expand the code a little bit. Realizing that

    liftIO = lift . liftIO

and

    lift mr = PipeM (Done Nothing `liftM` mr) (Finalize mr)

we can expand the yield version into:

sourceTChanYield2 ch = forever $ do
 let action = liftIO . atomically $ readTChan ch
 ans <- PipeM (Done Nothing `liftM` action) (FinalizeM action)
 yield ans

So the first hint that something is wrong is that the finalize
function is calling the action. If you try to change that finalize
action into a no-op, e.g.:

sourceTChanYield3 :: MonadIO m => TChan a -> Source m a
sourceTChanYield3 ch = forever $ do
 let action = liftIO . atomically $ readTChan ch
 ans <- PipeM (Done Nothing `liftM` action) (return ())
 yield ans

then you get an error message:

test.hs:36:53:
    Could not deduce (a ~ ())

The problem is that, as the monadic binding is set up here, the code
says "after running the PipeM, I want you to continue by yielding, and
then start over again." If you want to expand it further, you can
change `forever` into a recursive call, expand `yield`, and then
expand all the monadic binding. Every finalization call is forcing
things to keep running.

And remember: all of this is the desired behavior of conduit, since we
want to guarantee finalizers are always called. Imagine that, instead
of reading data from a TChan, you were reading from a Handle. In the
code above, there was no way to call out to the finalizers.

Not sure if all of that rambling was coherent, but here's my
recommended solution. What we need is a helper function that allows
you to branch based on whether or not it's time to clean up. `lift`,
`liftIO`, and monadic bind all perform the same actions regardless of
whether or not finalization is being called. The following code,
however, works correctly:

liftFinal :: Monad m => m a -> Finalize m () -> (a -> Source m a) -> Source m a
liftFinal action final f = PipeM (liftM f action) final

sourceTChanYield :: Show a => MonadIO m => TChan a -> Source m a
sourceTChanYield ch = liftFinal
    (liftIO . atomically $ readTChan ch)
    (return ())
    $ \ans -> do
        yield ans
        sourceTChanYield ch

Michael

On Sun, May 20, 2012 at 4:22 PM, Hiromi ISHII <konn.jinro at gmail.com> wrote:
> Oops, sorry.
> The last case's behaviour was not as I expected... A correct log is below:
>
> ~~~~
> ghci> sourceTChanRaw ch $$ LC.isolate 10 =$= LC.mapM_ print
> ()
> ()
> ()
> ()
> ()
> ()
> ()
> ()
> ()
> ()
> ghci> sourceTChanState ch $$ LC.isolate 10 =$= LC.mapM_ print
> ()
> ()
> ()
> ()
> ()
> ()
> ()
> ()
> ()
> ()
> ghci> sourceTChanYield ch $$ LC.isolate 10 =$= LC.mapM_ print
> ()
> ()
> ()
> ()
> ()
> ()
> ()
> ()
> ()
> ()
> *blocks*
> ~~~~
>
> So again, sourceTChanYield blocks here even if it is already supplied with enough values!
>
> -- Hiromi ISHII
> konn.jinro at gmail.com
>
>
>
>
> _______________________________________________
> Haskell-Cafe mailing list
> Haskell-Cafe at haskell.org
> http://www.haskell.org/mailman/listinfo/haskell-cafe



More information about the Haskell-Cafe mailing list