我试图理解读者monad,但似乎无法理解bind(>>=)在这个monad中做了什么。
这是我正在分析的实现:
newtype Reader e a = Reader { runReader :: (e -> a) }
instance Monad (Reader e) where
return a = Reader $ e -> a
(Reader r) >>= f = Reader $ e -> runReader (f (r e)) e
- 我的第一个问题是,为什么阅读器部分应用于左侧 束缚的手边?
(Reader r)
而不是(Reader r a)
. - 这部分定义是怎么回事:
(f (r e))
,它的目的是什么?
非常感谢你帮助我。
我的第一个问题是,为什么 Reader 部分应用于绑定的左侧?
(Reader r)
而不是(Reader r a)
.
其实不然。Reader
的使用已经完全饱和,因为它必须如此。但是,我可以理解您的困惑...请记住,在 Haskell 中,类型和值位于不同的命名空间中,使用data
或newtype
定义数据类型会将新名称引入两个命名空间的作用域。例如,请考虑以下声明:
data Foo = Bar | Baz
这个定义绑定了三个名称,Foo
、Bar
和Baz
。但是,等号左侧的部分绑定在类型命名空间中,因为Foo
是一个类型,右侧的构造函数绑定在值命名空间中,因为Bar
和Baz
本质上是值。
所有这些东西也有类型,这有助于可视化。Foo
有一个类型,本质上是"类型级事物的类型",Bar
和Baz
都有一个类型。这些类型可以编写如下:
Foo :: *
Bar :: Foo
Baz :: Foo
。其中*
是类型。
现在,考虑一个稍微复杂的定义:
data Foo a = Bar Integer String | Baz a
再一次,这个定义绑定了三个名称:Foo
、Bar
和Baz
。同样,Foo
位于类型命名空间中,Bar
和Baz
位于值命名空间中。然而,它们的类型更为复杂:
Foo :: * -> *
Bar :: Integer -> String -> Foo a
Baz :: a -> Foo a
在这里,Foo
是一个类型构造函数,所以它本质上是一个接受类型(*
)作为参数的类型级函数。同时,Bar
和Baz
是接受各种值作为参数的值级函数。
现在,回到Reader
的定义。暂时避免使用记录语法,我们可以将其重新表述如下:
newtype Reader r a = Reader (r -> a)
这将绑定类型命名空间中的一个名称,在值命名空间中绑定一个名称,但令人困惑的部分是它们都命名为Reader
!不过,这在 Haskell 中是完全允许的,因为命名空间是分开的。在这种情况下,每个Reader
都有一个种类/类型:
Reader :: * -> * -> *
Reader :: (r -> a) -> Reader r a
请注意,类型级Reader
接受两个参数,但值级Reader
只接受一个参数。当您对值进行模式匹配时,您使用的是值级构造函数(因为您正在解构使用同一构造函数构建的值),并且该值仅包含一个值(因为它必须,因为Reader
是一个newtype
),因此模式仅绑定单个变量。
这部分定义是怎么回事:
(f (r e))
,它的目的是什么?
Reader
本质上是一种机制,用于组合许多函数,这些函数都采用相同的参数。这是一种避免在任何地方串起值的方法,因为各种实例将自动执行管道。
为了理解Reader
>>=
的定义,我们专门化Reader
>>=
的类型:
(>>=) :: Reader r a -> (a -> Reader r b) -> Reader r b
为了清楚起见,我们还可以将Reader r a
扩展到r -> a
,只是为了更好地了解类型的实际含义:
(>>=) :: (r -> a) -> (a -> r -> b) -> (r -> b)
为了讨论的目的,让我们在这里命名参数:
(>>=) :: (r -> a) -> (a -> r -> b) -> (r -> b)
(>>=) f g = ...
让我们考虑一下这个问题。我们被赋予了两个函数,f
和g
,并且我们期望生成一个函数,该函数从a
类型的值生成b
类型的值。我们只有一种方法可以产生b
,那就是调用g
。但是为了呼叫g
,我们必须有一个a
,而我们只有一个方法可以得到一个a
:呼叫f
!我们可以调用f
,因为它只需要一个r
,我们已经有了,因此我们可以开始将函数附加到一起以产生我们需要的b
。
这有点令人困惑,因此直观地查看此值流可能会有所帮助:
+------------+
| input :: r |
+------------+
| |
v |
+--------------+ |
| f input :: a | |
+--------------+ |
| |
v v
+------------------------+
| g (f input) input :: b |
+------------------------+
在 Haskell 中,这看起来像这样:
f >>= g = input -> g (f input) input
。或者,稍微重命名一下以匹配您问题中的定义:
r >>= f = e -> f (r e) e
现在,我们需要重新引入一些包装和解包,因为真正的定义是在Reader
类型上,而不是直接(->)
。这意味着我们需要添加一些Reader
包装器和runReader
解包器的用法,但除此之外,定义是相同的:
Reader r >>= f = Reader (e -> runReader (f (r e)) e)
在这一点上,你可以检查你的直觉:Reader
是一种在许多函数之间串起一个值的方法,这里我们组合了两个函数,r
和f
。因此,我们需要将值传入两次,我们这样做:在上面的定义中,e
有两种用途。
为了回答你的函数,Monad
类是类于类* -> *
类型。
[Int] :: *
= 整数列表[] :: * -> *
= 列表Reader e a :: *
= 环境 e 的读取器导致Reader e :: * -> *
= 具有环境 e 的读取器Reader :: * -> * -> *
= 读取器
当我们说Reader
有一个monad实例时,我们很糟糕,当我们的意思是任何环境中Reader
都有一个 monad 实例。
(类似地,当w
是Monoid
时,Writer w
是一个Monad
,Writer
不是Monad
)。
要回答您的第二个问题,更容易从以下方面思考Reader e a
与函数e -> a
相同。
函数具有相同的 monad 定义,但没有newtype
包装器和 展开器。Reader = (->)
思考:
instance Monad ((->) e) where
return x = _ -> x -- alternatively, return = const !
r >>= f = e -> f (r e) e
然而,join
定义可能是最有见地的:
join :: Reader e (Reader e a) -> Reader e a
但具有裸功能:
join :: (e -> e -> a) -> (e -> a)
join f e = f e e
如果我们为Reader
编写它,我们必须添加runReader
并Reader
正确的位置(并将变量绑定移动到 RHS 上的 lambda):
join f = Reader $ e -> runReader (runReader f e) e
-
没有部分应用程序。
Reader
是一个newtype
定义,它只"包装">一个值(runReader
),它是类型e -> a
的函数。因此,Reader r
只是模式匹配Reader
包装器中的函数。r
绑定到类型e -> a
的函数。 -
f
是一个函数,r
也是如此。r e
使用值e
调用函数r
,然后使用该函数调用的结果调用f
。