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
为什么?
答案 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
使用的r
,类型为ReaderT e m
的函数的newtype包装器e -> m a
,根据Reader e
定义为ReaderT
如果没有任何进一步的信息,将使用隐式ReaderT e Identity
。为什么呢?
(->) e
块的总体类型由最后一个表达式给出,对于某些do
和Monad m => m a
,该表达式也被约束为m
形式。
回顾a
,很明显r
块的类型do
和String -> IO ()
的类型r
。它还需要p2
为String -> 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)
包(package,type 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。它意外编译并运行,几乎完成了你的预期。
您需要使用runReaderT
和lift
(或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
这不是你想要的。