Combining proxies with another EitherT in the base monad

For example, having ...

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

... how to do it?

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

Note I read Mixing Base Monads in Control.Proxy.Tutorial . I get the first example, but I can’t understand the far-fetched example. Other examples, which are not so obvious, but not so far-fetched, can clarify how to use hoist and lift to match any combination of basic monads.

+4
source share
3 answers

Suppose you have a monad transformer block, for example 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 it symbolize it ? .

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

Using hoist , you can control the monad stack to the right of the leftmost element. To manipulate them how? For example, by providing lift :

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

Using combinations of hoist and lift , you can insert these "wildcards" anywhere 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 method can be used to align two monad stacks from your example.

+8
source

The either package (where I assume your EitherT type comes from) provides several functions for changing the first argument, for example

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

You can use this along with some suitable encoding (or decryption) to turn EitherT String IO a into EitherT ByteString IO a (or vice versa) and then hoist that conversion to Consumer or Producer monad transformer.

+5
source

There are actually two solutions.

The first solution is the proposal proposed by Daniel Wagner: you modify two basic monads to use the same type of Left . For example, we could normalize them to use ByteString . To do this, first take the ByteString pack function:

 pack :: String -> ByteString 

Then we raise it to work on the left value of a EitherT :

 import Control.Error (fmapLT) -- from the 'errors' package fmapLT pack :: (Monad m) => EitherT String mr -> EitherT ByteString mr 

Now we need to target your Consumer base monad using hoist :

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

Now you can compose your consumer directly with your producer, as they have the same basic monad.

The second solution is a proposal by Daniel Diaz Carrete. Instead, you agree that your two pipes agree with a common monad transformer stack that contains both layers of EitherT . All you have to do is decide in which order to nest these two layers.

Suppose you decide to put an EitherT String transformer outside of an EitherT ByteString transformer. This would mean that your final monad transformer target stack would be:

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

Now you need to promote both of your channels to target this transformer stack.

For your Consumer you need to insert an EitherT ByteString layer between the EitherT String and IO if you want to combine this final transformer stack. Creating a layer is very simple: you just use lift , but you need to target this level between these two specific layers, so you use hoist twice because you need to skip both the transformer with the proxy monad and the EitherT String monad transformer:

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

For your Producer you need to insert an EitherT String layer between the intermediary transformer and the EitherT ByteString transformer if you want to combine the final transformer stack. Again, creating a layer is very simple: we just use lift , but you need to target this level between these two specific layers. You are just a hoist , but this time you use it only once, since you only need to skip the intermediary monad transformer to attach the lift in the right place:

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

Now your manufacturer and consumer have the same monad transformer unit, and you can assemble them directly.

You may be wondering now: is this hoist ing lift process doing the “right thing”? The answer is yes. Part of the magic of category theory is that we can strictly define what it means to correctly insert the "empty monad transformer layer" using lift , and we can also exactly determine what it means to "target something between two monad transformers" using hoist , pointing out some theoretically inspired laws and verifying that lift and hoist comply with these laws.

Once we satisfy these laws, we can simply ignore all the detailed details of what exactly lift and hoist . Category theory frees us from working with a very high level of abstraction, where we just think about the terms “insert elevators” in the space between monad transformers, and the code magically translates our spatial intuition into strictly correct behavior.

My guess is that you probably want the first solution, since you can share error handling between producer and consumer in the same EitherT layer.

+5
source

All Articles