Is it possible to reuse a monad composition function if one of the monads is wrapped inside a monad transformer?

StackOverflow https://stackoverflow.com/questions/18388563

Pergunta

Let's say I have function that composes two monad actions:

co :: Monad m => m a -> m a -> m a

You can think of co as a higher order function that describes how two monadic actions may cooperate with each other to complete a task.

But now I find that the first monadic action may be wrapped inside a monad transformer, while the second one is not:

one :: (MonadTrans t, Monad m) => t m a

two :: Monad m => m a

But would like to compose them together still, so I need a function:

co' :: (MonadTrans t, Monad m) => t m a -> m a -> t m a

So that the first t m a can cooperate with m a by just lifting all m primitives into the context t.

The trick here is to build co without really knowing the implementation of m or t. I feel like the answer is somewhere in the MFunctor package, and in fact asked a similar question yesterday. But cannot come up with anything good, any thoughts?

Foi útil?

Solução

You can do this using hoist from the mmorph package. hoist lets you modify the base monad of anything that implements MFunctor (which is most monad transformers):

hoist :: (MFunctor t) => (forall x . m x -> n x) -> t m r -> t n r

You can then use it for your problem like this:

co' tma ma = hoist (co ma) tma

Then you're done!

To understand why that works, let's go through the types step by step:

co :: (Monad m) => m a -> m a -> m a

ma :: (Monad m) => m a

co ma :: (Monad m) => m a -> m a

hoist (co ma) :: (Monad m, MFunctor t) => t m a -> t m a

tma :: (Monad m, MFunctor t) => t m a

hoist (co ma) tma :: (Monad m, MFunctor t) => t m a

Note that hoist has certain laws it must satisfy that ensure that it does "the right thing" that you would expect:

hoist id = id

hoist (f . g) = hoist f . hoist g

These are just functor laws, guaranteeing that hoist behaves intuitively.

hoist is provided in the Control.Monad.Morph module of the mmorph package, which you can find here. The bottom of the main module has a tutorial teaching how to use the package

Outras dicas

Quick Definition

You need t m to have a monad instance for this to work.

import Control.Monad
import Control.Monad.Trans

co :: Monad m => m a -> m a -> m a
co = undefined

co' :: (MonadTrans t, Monad m, Monad (t m)) => t m a -> m a -> t m a
co' one two = lift . flip co two . return  =<< one

Alternative view on the problem

Seeing the definition of lift from transformers package should help you find the answer as you want something similar

lift . return = return
lift (m >>= f) = lift m >>= (lift . f)

As you have flip co one :: Monad m => m a -> m a and you want to lift it to get a function of type (MonadTrans t, Monad m, Monad (t m)) => t m a -> t m a. So following on the footsteps of lift

lift' :: (MonadTrans t, Monad m, Monad (t m)) => (m a -> m a) -> t m a -> t m a
lift' f tma = tma >>= (lift . f . return)

Now defining co' is trivial

co' one two = lift' (flip co two) one

Problem

The above solutions just satisfy the type, but do they satisfy the semantics too? To see the problem lets take a co function which always returns the second action without even looking at the first. Now the above co' will never be able to do that as it always runs the first action before deciding anything. So the side effect of first action will still occur in the co' even though they dont occur in co.

Does the general solution exist?

I suppose not. Because what you want to actually implement that function generically, is a function like t m a -> (m a -> m b) -> t m b, where you need to get action m a out of t m a without actually running the side effects of m a.

So suppose m is IO monad and t is State transformer with some state and your one action actually launches a missile and depending on the success or failure of that modifies the State. Now what you want is to actually modify the state without launching the missiles. In general that is not possible.

If you know some information about co like which action is run first, then you can implement co' using the above method.

The monad-control package can be used to lift functions and control operations from a base monad into a monad transformer, even those functions that take callbacks.

The lifted-base package builds on monad-control and contains lifted versions of many common functions dealing with exceptions, concurrency... In particular, it has a lifted version of finally from Control.Exception, whose signature is very similar to that of co.

Here are some links:

http://www.yesodweb.com/book/monad-control

http://www.yesodweb.com/blog/2013/08/exceptions-and-monad-transformers

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top