Lift a function and its argument to a different mo

2019-07-03 09:33发布

I am not sure how to formulate this question scientifically exact, so I am just going to show you an example.

I am using state in a StateT transformer. Underlying is IO. Inside the StateT IO operation I need to use alloca. However, I can't lift alloca to StateT IO because it expects an argument of type (Ptr a -> IO a) while I require it to work with an argument of (Ptr a -> StateT IO MyState a).

(However, this is a generic question about monad transformers rather than specific to IO, StateT or alloca.)

I came up with the following, working solution:

-- for reference
-- alloca :: (Storable a) => (Ptr a -> IO b) -> IO b

allocaS :: (Storable a) => (Ptr a -> StateT s IO b) -> StateT s IO b
allocaS f = do
  state <- get
  (res, st) <- liftIO $ alloca $ \ptr -> (runStateT (f ptr) state)
  put st
  return res

However, it seems wrong to me that I should have to de- and reconstruct the StateT action in order to use it with alloca. Also, I have seen this pattern in some variations more than once and it's not always as simple and safe as here with StateT.

Is there a better way to do this?

2条回答
贼婆χ
2楼-- · 2019-07-03 10:26

Good afternoon,

AFAIK, there is no general way to turn a function of type (a -> m b) -> m b into (a -> t m b) -> t m b because that would imply the existence of a function of type MonadTrans t => (a -> t m b) -> (a -> m b).

Such a function cannot possibly exist, since most transformers cannot be stripped so easily from a type signature (how do you turn a MaybeT m a into an m a for all a ?). Hence, the most general way to turn (a -> m b) -> m b to (a -> t m b) -> t m b is undefined.

In the case of StateT s m, there is a loophole that allows you to define it anyway. Since StateT s m a === s -> m (s,a), we can rewrite the type equation to :

(a -> StateT s m b) -> StateT s m b
=== (a -> s -> m (s,b)) -> s -> m (s,b)
=== s -> (s -> (a -> m (s,b)) -> m (s,b) -- we reorder curried arguments
=== s -> (s -> (A -> m B)) -> m B -- where A = a, B = (s,b)

Solving this new type signature is now trivial :

liftedState f s run = f (run s)
allocaS :: Storable a => (Ptr a -> StateT IO b) -> StateT IO b
allocaS = isomorphic (liftedState alloca)

That is about the best we can do in terms of code reuse, short of defining a new subclass of MonadTrans for all monads that exhibit the same behaviour.

I hope I made myself clear enough (I didn't want to go into too much detail for fear of being confusing)

Have an excellent day :-)

查看更多
Rolldiameter
3楼-- · 2019-07-03 10:31

This can be accomplished using MonadBaseControl in monad-control, which has been devised exactly for this purpose:

{-# LANGUAGE FlexibleContexts #-}
import Control.Monad
import Control.Monad.Trans.Control
import qualified Foreign.Ptr as F
import qualified Foreign.Marshal.Alloc as F
import qualified Foreign.Storable as F

alloca :: (MonadBaseControl IO m, F.Storable a) => (F.Ptr a -> m b) -> m b 
alloca f = control $ \runInIO -> F.alloca (runInIO . f)

This enhanced version of alloca can be used with any monad stack based on IO that implements MonadBaseControl, including StateT s IO.

Instances of MonadBaseControl allow their monadic values to be encoded in the base monad (here IO), passed to a function in the base monad (like F.alloca) and then reconstruct them back.

See also What is MonadBaseControl for?

Package lifted-base contains many of the standard IO functions lifted to MonadBaseControl IO, but alloca isn't (yet) among them.

查看更多
登录 后发表回答