为什么以下ReaderT字符串IO会丢失IO操作?

时间:2012-12-14 13:01:30

标签: haskell

module Main (main) where

import Control.Monad.Reader

p1 :: String -> IO ()
p1 = putStrLn . ("Apple "++)

p2 :: String -> IO ()
p2 = putStrLn . ("Pear "++)

main :: IO ()
main = do
    p1 "x"
    p2 "y"
    r "z"

r :: String -> IO ()
r = do
    p1
    p2

打印:

Apple x 梨子 梨z

为什么?

4 个答案:

答案 0 :(得分:7)

问题出在r。鉴于Reader monad的以下定义:

instance Monad ((->) e) where
    return = const
    f >>= g = \x -> g (f x) x

我们可以简化r

r = p1 >> p2    
  = (>>=) p1 (\_ -> p2)    
  = (\f g x -> g (f x) x) p1 (\_ -> p2)    
  = \x -> (\_ -> p2) (p1 x) x    
  = \x -> p2 x

这也表明Reader的{​​{1}}只是(>>),具有更具体的类型。

如果要分发环境然后执行这两个操作,则必须将应用const的结果绑定到环境中,例如:

p1

或使用r = do a1 <- p1 a2 <- p2 return (a1 >> a2)

Applicative

r = (>>) <$> p1 <*> p2 部分展开,Reader提供了Control.Monad.Reader的三种变体。

  • 隐式Reader,这是函数(->) e使用的
  • monad transformer r,类型为ReaderT e m的函数的newtype包装器
  • 明确的e -> m a,根据Reader e定义为ReaderT

如果没有任何进一步的信息,将使用隐式ReaderT e Identity。为什么呢?

(->) e块的总体类型由最后一个表达式给出,对于某些doMonad m => m a,该表达式也被约束为m形式。

回顾a,很明显r块的类型doString -> IO ()的类型r。它还需要p2String -> IO ()。现在,统一这两种类型:

Monad m => m a

通过选择m = (->) String a = IO () 来匹配(->) e monad实例。


作为monad变换器,e = String负责内部管道,以确保内部monad的操作正确排序和执行。要选择ReaderT,有必要明确提及它(通常在类型签名中,但将类型固定为ReaderT的函数,例如ReaderT,也可以):

runReaderT

这附带另一个问题,r :: ReaderT String IO () r = do ? p1 ? p2 r' :: String -> IO () r' = runReaderT r p1的类型为p2,与所需的String -> IO ()不符。

ad-hoc解决方案(完全根据这种情况量身定制)只是为了应用

ReaderT String IO ()

要获得更通用的内容,ReaderT :: (e -> m a) -> ReaderT e m a 类型类可以将MonadIO操作提升到转换器中,IO类型类允许访问环境。只要变换器堆栈中的某处有MonadReader(或IO),这两个类型就可以工作。

ReaderT

或者更简洁:

lift' :: (MonadIO m, MonadReader a m) => (a -> IO b) -> m b
lift' f = do
    env <- ask     -- get environment
    let io = f env -- apply f to get the IO action
    liftIO io      -- lift IO action into transformer stack

关于您在评论中的问题,您可以通过以下方式实施相关实例:

lift' f = ask >>= liftIO . f

实际的类型类可以在newtype ReaderT e m a = ReaderT { runReaderT :: e -> m a } instance Monad m => Monad (ReaderT e m) where return = ReaderT . const . return -- The transformers package defines it as "lift . return". -- These two definitions are equivalent, though. m >>= f = ReaderT $ \e -> do a <- runReaderT m e runReaderT (f a) e instance Monad m => MonadReader e (ReaderT e m) where ask = ReaderT return local f m = ReaderT $ runReaderT m . f reader f = ReaderT (return . f) 包(packagetype class),mtl包中的newtype和Monad实例中找到({{ 3}},package)。


至于制作transformers e -> m a个实例,你运气不好。 Monad需要类型Monad的类型构造函数,这意味着我们正在尝试执行类似的操作(在伪代码中):

* -> *

其中instance Monad m => Monad (/\a -> e -> m a) where -- ... 代表类型级lambda。但是,我们可以得到类型级别lambda的最接近的东西是类型同义词(在我们可以创建类型类实例之前必须完全应用,所以这里没有运气)或类型族(不能用作类型的参数)班级)。使用/\之类的内容会再次导致(->) e . m

答案 1 :(得分:2)

让我们先重写一下

的主体
r :: String -> IO ()
r = do
    p1
    p2

使用(>>)

r = p1 >> p2

所以p1必须为某些m a Monad提供m类型,p2必须为同一{{1}提供类型m b }}

现在,

m

,其中的顶级类型构造函数是函数箭头p1, p2 :: String -> IO () 。因此(->)中使用的Monad必须

r

(->) String [又名读者monad]的Monad实例是

(->) e

因此,

instance Monad ((->) e) where
    -- return :: a -> (e -> a)
    return = const
    -- (>>=) :: (e -> a) -> (a -> (e -> b)) -> (e -> b)
    f >>= g = \x -> g (f x) x

所以这只是一种复杂的写作方式

p1 >> p2 = p1 >>= \_ -> p2
         = \x -> (\_ -> p2) (p1 x) x   -- apply (\_ -> p2) to (p1 x)
         = \x -> p2 x                  -- eta-reduce
         = p2

答案 2 :(得分:2)

对于r,您使用了(->) String (IO ())Monad ((->) String)返回IO ()类型的值。

您没有使用ReaderT或任何monad变压器。你使用了一个返回不同monad的monad。它意外编译并运行,几乎完成了你的预期。

您需要使用runReaderTlift(或liftIO)来实现我认为您想要制作的r

答案 3 :(得分:0)

在r中调用p1和p2时,你停止了参数。然后,您编写的内容将被解释为无点符号,因此只有第二个IO操作才会获得参数。这有效:

r :: String -> IO ()
r x = do
    p1 x
    p2 x

要理解为什么会发生这种情况,请考虑您最初编写的内容等同于

r = p1 >> p2

编译器将其解释为

r x = (p1 >> p2) x

这不是你想要的。