Sal
Sal

Reputation: 4326

Wrapping conduit in ExceptT

What is a good way to wrap a conduit in ExceptT? The approach should stop the processing when there is an error, and extract the error message. Here is a toy code without error-handling - it just silently stops:

import Data.Conduit as C
import Data.ByteString as BS
import Control.Monad
import Control.Monad.IO.Class
import Data.Text as T

-- just a dummy processing to simulate errors
process :: BS.ByteString -> Either (Int,T.Text) BS.ByteString
process inp = if (BS.null inp) then Left $ (1,"Empty input") else Right inp

-- silent processing - stops on error but doesn't tell us what it is
sink :: MonadIO m => Consumer BS.ByteString m ()
sink = do
       bs <- await
       case bs of
        Just val -> do
            let msg = process val
            case msg of  
              Left _ -> return ()
              Right x -> (liftIO $ return x) >> sink
        Nothing -> return ()

How could we change the type signature of sink to something like below?

sink :: MonadIO m => ExceptT e m (Consumer BS.ByteString m ()) 

In case of Left, it will be nice to break out of the pipeline, and return the error message to top. I read this blog post but haven't understood it well enough yet to apply it to conduit (which also has complicated type signature). I will like to apply the proposed approach here to conduit - it seems EitherT suggested in the approach has now been subsumed by ExceptT.

Upvotes: 1

Views: 124

Answers (1)

ErikR
ErikR

Reputation: 52029

A useful signature to remember is:

ExceptT :: m (Either e b)  ->  ExceptT e m b

and with that in mind, this code type checks:

{-# LANGUAGE OverloadedStrings #-}

import Control.Monad.Trans.Class
import Control.Monad.Trans.Except

import Data.Conduit as C
import Data.ByteString.Char8 as BS
import Control.Monad
import Control.Monad.IO.Class
import Data.Text as T

-- just a dummy processing to simulate errors
process :: BS.ByteString -> Either (Int,T.Text) BS.ByteString
process inp = if (BS.null inp) then Left $ (1,"Empty input") else Right inp

type Err = (Int,T.Text)

sink' :: MonadIO m => ExceptT Err (ConduitM ByteString Int m) ()
sink' = do  bs <- lift await
            case bs of
              Just inp -> do
                msg <- ExceptT (return $ process inp)   -- ***
                lift $ yield (BS.length msg)
                liftIO $ BS.putStrLn msg
                sink'
              Nothing -> return ()

This isn't exactly a sink, but it should illustrate how to do things.

The typing on line (***) goes like this:

process inp             :: Either Err ByteString
                            -- (a pure value)
return (process inp)    :: m (Either Err ByteString)
                            -- here m = ConduitM ByteString Int mIO
ExceptT (...)           :: ExceptT Err m ()

So the use of return here sets things up so we can apply the ExceptT constructor.

Then when you call bind on an ExceptT ... value, you trigger the error checking code that ExceptT provides. Thus, if inp was a Left an error will be raised.

Update

Here is a version with Sink:

sink' :: MonadIO m => ExceptT Err (Sink ByteString m) ()
sink' = do  bs <- lift await
            case bs of
              Just inp -> do
                msg <- ExceptT (return $ process inp)
                liftIO $ BS.putStrLn msg
                sink'
              Nothing -> return ()

Upvotes: 1

Related Questions