Question

For example, having...

consumer :: Proxy p => () -> Consumer p a (EitherT String IO) ()
producer :: Proxy p => () -> Producer p a (EitherT ByteString IO) r

... how do I make this work?

session :: EitherT ByteString (EitherT String IO) ()
session = runProxy $ producer >-> consumer

Note: I've read Mixing Base Monads in Control.Proxy.Tutorial. I get the first example but can't understand the contrived example. More examples, not so obvious but not so contrived, might clarify how to use hoist and lift to match any combination of base monads.

Was it helpful?

Solution

Suppose you have a monad transformer stack like MT1 MT2 MT3 M a where M is the base monad.

Using lift, you can add a new monad transformer to the left. It can be any transformer, so let's symbolize it by ?.

lift :: MT1 MT2 MT3 M a -> ? MT1 MT2 MT3 M a

Using hoist, you can manipulate the monad stack to the right of the leftmost element. Manipulate it how? For example by supplying a lift:

hoist lift :: MT1 MT2 MT3 M a -> MT1 ? MT2 MT3 M a

Using combinations of hoist and lift, you can insert these "wildcards" at any point in the monad transformer stack.

hoist (hoist lift) :: MT1 MT2 MT3 M a -> MT1 MT2 ? MT3 M a

hoist (hoist (hoist lift)) :: MT1 MT2 MT3 M a -> MT1 MT2 MT3 ? M a

This technique can be used to equalize the two monad stacks from your example.

OTHER TIPS

The either package (where I guess your EitherT type is coming from) provide several functions for modifying the first argument, e.g.

bimapEitherT :: Functor m => (e -> f) -> (a -> b) -> EitherT e m a -> EitherT f m b

You can use this, along with some appropriate encoding (or decoding), to turn an EitherT String IO a into an EitherT ByteString IO a (or vice versa), then hoist that transformation into the Consumer or Producer monad transformer.

There are actually two solutions.

The first solution is the one Daniel Wagner proposed: you modify the two base monads to use the same Left type. For example, we could normalize them to both use ByteString. To do this, we first take ByteString's pack function:

pack :: String -> ByteString

Then we lift it to work on the left value of an EitherT:

import Control.Error (fmapLT)  -- from the 'errors' package

fmapLT pack :: (Monad m) => EitherT String m r -> EitherT ByteString m r

Now we need to target that transformation to your Consumer's base monad, using hoist:

hoist (fmapLT pack)
 :: (Monad m, Proxy p)
 => Consumer p a (EitherT String m) r -> Consumer p a (EitherT ByteString m) r

Now you can compose your consumer directly with your producer since they have the same base monad.

The second solution is the one Daniel Diaz Carrete proposed. You instead get your two pipes to agree on a common monad transformer stack that contains both EitherT layers. All you have to do is decide in which order to nest those two layers.

Let's imagine that you choose to layer the EitherT String transformer outside of the EitherT ByteString transformer. That would mean that your final target monad transformer stack would be:

(Proxy p) => Session (EitherT String (EitherT ByteString p)) IO r

Now you need to promote both of your pipes to target that transformer stack.

For your Consumer, you need to insert an EitherT ByteString layer in between EitherT String and IO if you want to match that final monad transformer stack. Creating the layer is easy: you just use lift, but you need to target that lift in between those two specific layers, so you use hoist, twice, because you need to skip over both the proxy monad transformer and the EitherT String monad transformer:

hoist (hoist lift) . consumer
 :: Proxy p => () -> Consumer p a (EitherT String (EitherT ByteString IO)) ()

For your Producer, you need to insert an EitherT String layer in between the proxy monad transformer and the EitherT ByteString transformer if you want to match the final monad transformer stack. Again, creating the layer is easy: we just use lift, but you need to target that lift in between those two specific layers. You just hoist, but this time you only use it once, since you only need to skip over the proxy monad transformer to nestle the lift in the right spot:

hoist lift . producer
 :: Proxy p => () -> Producer p a (EitherT String (EitherT ByteString IO)) r

Now your producer and consumer have the same monad transformer stack and you can compose them directly.

Now, you might wonder: Is this process of hoisting lifts doing the "right thing"? The answer is yes. Part of the magic of category theory is that we can rigorously define what it means to correctly insert an "empty monad transformer layer" using lift and we can similarly rigorously define what it means to "target something in between two monad transformers" using hoist by specifying several theoretically-inspired laws and verifying that lift and hoist observe those laws.

Once we satisfy those laws, we can just ignore all the nitty gritty details of what exactly lift and hoist do. Category theory frees us to work at a very high abstraction level where we just think in terms of "inserting lifts" spatially between monad transformers and the code magically translates our spatial intuition into the rigorously correct behavior.

My guess is that you probably want the first solution since you can then share error-handling between the producer and consumer in a single EitherT layer.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top