理解 monad 的真正关键是不要试图说出来
单子是 X
然后开始说
X 是一个单子
如果它是has a certain structure and obeys certain laws,那么它就是一个单子。出于在 Haskell 中编程的目的,如果它具有正确的种类和类型并遵守 Monad laws,那么它就是 Monad。
return a >>= f ≡ f a
m >>= return ≡ m
(m >>= f) >>= g ≡ m >>= (\x -> f x >>= g)
Gabriel Gonzales 指出the monad laws are "the category laws in disguise"。我们可以使用>=>,其定义如下,而不是>>=。
(f >=> g) = \x -> f x >>= g
当我们这样做时,monad 法则变成了 category laws 与身份 return 和关联组合 >=>。
return >=> f ≡ f
f >=> return ≡ f
f >=> (g >=> h) ≡ (f >=> g) >=> h
在您的示例中,您已经讨论了 monad 要成为的两种不同的东西:扁平化的聚合和修剪的聚合。与其说 monad 是这两种东西,不如说这两种东西都是 monad。为了培养对什么是 monad 的直觉,让我们来谈谈最大的一类东西,它们都是 monad。
嫁接的树木
Monads 的最大类是 trees with grafting 和简化。这个类是如此之大,以至于我在 Haskell 中所知道的每个 Monad 都是一棵嫁接的树。这些 monad 中的每一个都包含一个 return,它构造一棵在叶子上保存值的树,以及一个绑定 >>=,它做两件事。 Bind 用一棵新树替换每片叶子,将新树嫁接到叶子所在的树上,并简化生成的树。您提到的示例在简化树的方式上有所不同。允许哪些简化受Monad 法律的约束。
如果我们有一棵简单的树在它的叶子上保存值,它就是一个 monad。
data Tree a = Branch [Tree a] | Leaf a
它的return 构造一个Leaf。这里是return 1:
1
它的绑定用整棵树替换叶子。我们从以下树开始:
*
/ \
0 1
并将其绑定到\x -> Branch [x*2, x*2 + 1]。我们用新计算的树替换每个叶子。
__*__
/ \
* *
/ \ / \
0 1 2 3
对于普通树木,嫁接步骤不会进行任何简化。在检查这些操作是否符合单子定律之后,我们可以说
一棵经过嫁接且没有简化的树是一个单子
展平
列表、袋子、集合、Maybe 和 Identity 将生成的树的所有级别扁平化为一个级别。任何嫁接在树上的东西都会出现在同一个列表、袋子或套装或Just 中。集合还会从生成的单层树中删除任何重复项。
*
/ \
[0, 1]
如果我们将它绑定到\x -> [x*2, x*2 + 1],我们会用新树替换每个叶子
__*__
/ \
* *
/ \ / \
[0, 1] [2, 3]
然后将中间层展平
____*____
/ | | \
[0, 1, 2, 3]
我们可以这么说
扁平化的聚合是一棵经过嫁接和简化的树
而且,在检查了单子定律之后,我们可以说
扁平化的聚合是一个单子
修剪
Reader e 和 pairs 像 data Pair a = Pair a a 有点不同。他们无法将所有结果扁平化为单个图层,或者至少无法立即这样做。相反,他们会修剪与父级分支方向不同的分支。
如果我们从一对开始
*
/ \
<0, 1>
当我们将它绑定到\x -> <x*2, x*2 + 1> 时,我们用新树替换每个叶子
__*__
/ \
* *
/ \ / \
<0, 1> <2, 3>
我们修剪没有分支的分支
__*__
/ \
* *
/ \
<0, 3>
然后可以通过扁平化层来进一步简化这一点
*
/ \
<0, 3>
正如您所指出的,Reader e a 分支的方向数量等于e 的可能值数量。
我们可以这么说
修剪和扁平化的聚合是嫁接和简化的树
而且,在检查了单子定律之后,我们可以说
修剪和展平的聚合是单子
继续
continuation monad 是一棵树,每个可能的continuation 都有一个分支。我们将采用terminology Philip JF suggested for continuations。 continuation monad 是整个(a -> r) -> r。 continuation 是作为第一个参数传入的 a -> r 函数
-- continuation
-- |------|
data Cont r a = Cont {runCont :: (a -> r) -> r}
-- |-----------|
-- continuation monad
延续单子的分支数量等于|r| ^ |a|,即延续a -> r 的可能值的数量。每个分支都标有相应的功能。 每个叶子中的延续总是具有相同的值,我们稍后会证明这一点。我们还将为树的内部节点添加标签,这是一个函数r -> r,我将在稍后讨论。
我们将使用以下数据类型来编写示例树。
data Tri = A | B | C
我们的示例树将用于return A :: Cont Bool Tri。树中保存的值的类型Tri 具有三个构造函数,而连续单子的结果Bool 具有两个构造函数。有2 ^ 3 = 8 可能的函数Tri -> Bool,每一个都构成树的一个分支。
id *
____________________________|____________________________
false | a | b | c | aOrB | aOrC | bOrC | true |
A A A A A A A A
"The way to a Monad's heart is through its Kleisli arrows"。 Kleisli arrows 是你可以传递给>>= 的第二个参数的东西;他们的类型为a -> m b。我们将研究Cont 的Kleisli 箭头,其类型为a -> Cont r b,或者,当我们查看Cont 构造函数时
a -> (b -> r) -> r
我们可以将连续单子a -> (b -> r) -> r 的 Kleisli 箭头分成两部分。第一部分是决定将什么b 传递给延续b -> r 的函数。它唯一需要使用的是a 参数,因此它必须是g :: a -> b 函数之一。第二部分是合并结果的函数。它可以看到参数a 和将g a 传递给延续的结果。我们将调用第二个函数r :: a -> r -> r。 a -> (b -> r) -> r类型的所有函数都可以写成形式
a -> (b -> r) -> r
\x -> \f -> r x (f (g x))
对于一些g :: a -> b 和r :: a -> r -> r。
同样,每个延续单子(a -> r) -> r 都可以写成形式
(a -> r) -> r
\f -> r (f a)
对于某些a :: a 和r :: r -> r。结合这些构成了延续单子在每个叶子中始终保持相同值的理由。
当我们将函数 \x -> \f -> r x (f (g x)) 绑定到延续单子树上时,我们会将 g x 记录为新叶,并将 (r x, g x) 记录为新中间节点的标签。树真的很大,但我们将使用\x -> \f -> r x (f (g x)) :: Tri -> (Bit -> Bool) -> Bool 绘制另一个完整示例的角,其中Bit 只有两个构造函数。生成的 continuation monad 应该只有 |Bool| ^ |Bit| = 4 分支,但我们还没有简化它。
id *
_______________________________|_...
false | a |
r A * r A *
_________|____________ _____|_...
bfalse | b0 | b1 | btrue | bfalse | b0 |
g A g A g A g A g A g A
由于每个叶子都具有相同的值,因此通过树的路径之间的唯一区别是标记每个分支的函数。我们将从分支中删除标签,只绘制一个分支。 return a 的第一个示例现在将绘制为
id *
|
A
而将\x -> \f -> r x (f (g x))绑定到return A的例子会画成
id *
|
r A *
|
g A
对于某些a :: a 和r :: r -> r,任何以\f -> r (f a) 形式编写的延续单子都将由树表示
r *
|
a
绑定\x -> \f' -> r' x (f' (g' x))时,会用下面的树表示(这里只是嫁接)
r *
|
r' a *
|
g' a
我们将从definition of >>= for Cont 中找出简化步骤。
m >>= k = Cont $ \ c -> runCont m (\x -> runCont (k x) c)
(\f -> r (f a)) >>= (\x' -> \f' -> r' x' (f' (g' x')))
= \c -> (\f -> r (f a)) (\x -> (\x' -> \f' -> r' x' (f' (g' x'))) x c) -- by definition
= \c -> (\f -> r (f a)) (\x -> ( \f' -> r' x (f' (g' x ))) c) -- beta reduction
= \c -> (\f -> r (f a)) (\x -> r' x (c (g' x )) ) -- beta reduction
= \c -> r ((\x -> r' x (c (g' x))) a) -- beta reduction
= \c -> r ( r' a (c (g' a)) ) -- beta reduction
= \c -> (r . r' a) (c (g' a)) -- f (g x) = (f . g) x
= \c -> (r . r' a) (c (g' a)) -- whitespace
格式为\f -> r (f a)。我们的树将被简化为
r . r' a *
|
g' a
continuation monad 是一棵树,其内部节点用函数标记。它的绑定操作是树嫁接,然后是简化步骤。简化步骤将内部节点上的功能组合起来。可以这么说
延续单子是一棵具有嫁接和简化的树
而且,在检查了单子定律之后,我们可以说
延续单子是单子