1
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

它打印:

苹果 x 梨 y 梨 z

为什么?

4

4 回答 4

7

问题出在r. 给定以下Readermonad 的定义:

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's(>>)只是const一个更具体的类型。

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

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

或使用Applicative

r = (>>) <$> p1 <*> p2

扩展Reader部分,Control.Monad.Reader提供了Reader.

  • 隐含的(->) e,这是函数r使用的
  • monad 转换ReaderT e m器,一个新的类型的函数包装器e -> m a
  • 显式Reader e, 定义ReaderTReaderT e Identity

没有任何进一步的信息,将使用隐式(->) e。为什么?

块的整体类型do由最后一个表达式给出,它也被限制Monad m => m a为 some mand的形式a

回头看r,很明显该do块具有由and的类型String -> IO ()给出的类型。它也需要是. 现在,统一这两种类型:rp2String -> IO ()Monad m => m a

m = (->) String
a = IO ()

这通过选择匹配(->) emonad 实例e = String


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

r :: ReaderT String IO ()
r = do ? p1
       ? p2

r' :: String -> IO ()
r' = runReaderT r

这带来了另一个问题,p1并且p2有一个String -> IO ()与 required 不匹配的type 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)

实际的类型类可以在mtl包中找到(类型类),包中的新类型和Monad实例transformers类型类)。


至于做一个e -> m a Monad实例,你不走运。Monad需要 kind 的类型构造函数* -> *,这意味着我们正在尝试做这样的事情(在伪代码中):

instance Monad m => Monad (/\a -> e -> m a) where
    -- ...

其中/\代表类型级 lambda。然而,最接近类型级别 lambda 的东西是类型同义词(在我们可以创建类型类实例之前必须完全应用它,所以这里没有运气)或类型族(不能用作类型的参数类)。使用类似的东西再次(->) e . m导致newtype

于 2012-12-14T13:12:52.217 回答
2

我们先重写一下body

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

使用(>>),

r = p1 >> p2

所以p1必须有m a一些类型Monad m,并且p2必须有m b相同的类型m

现在,

p1, p2 :: String -> IO ()

其中的顶级类型构造函数是函数箭头(->)。因此Monad使用的 inr必须是

(->) String

[aka reader 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

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

r = p2
于 2012-12-14T13:11:41.223 回答
2

对于r您使用(->) String (IO ()) which is aMonad ((->) String)返回类型的值IO ()

您没有使用ReaderT任何单子变压器。你使用了一个返回不同单子的单子。它意外地编译并运行,几乎符合您的预期。

您需要使用runReaderTand lift(or liftIO) 来实现r我认为您正在尝试实现的目标。

于 2012-12-14T13:20:31.767 回答
0

当您在 r 中调用 p1 和 p2 时,您省略了参数。然后,您编写的内容将被解释为无点表示法,因此只有第二个 IO 操作有参数。这有效:

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

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

r = p1 >> p2

编译器将其解释为

r x = (p1 >> p2) x

这不是你想要的。

于 2012-12-14T13:07:25.123 回答