Monads可以被解释为容器的形式:
我想知道如何以有意义的方式将continuation在此视图中解释为包/容器的形式。谢谢!
答案 0 :(得分:8)
我喜欢将延续视为带有漏洞的程序。我想我最初从Tekmo's blog收集了这个见解。
看看这个小延续:
import Control.Monad.Trans.Cont
program :: ContT () IO Char
program = ContT $ \doThing -> do
c <- getChar
doThing c
这是一个“缺少一块”的程序 - 即如何处理从Char
检索到的getChar
。我们可以通过用putChar
之类的东西填充缺失的部分来运行它;通过runContT program putChar
评估延续将得到一个字符,然后将其打印到stdout。
如果您对通过抽象语法树表示程序感到满意,那么容器类比可能是直观的。
为了更清楚,你可以建立一个包含DoThing
术语的AST,代表一个需要填充的洞:
{-# LANGUAGE DeriveFunctor #-}
import Control.Monad.Free
data ExprF a =
GetChar (Char -> a)
| DoThing Char a
deriving Functor
type Expr = Free ExprF
getChar' :: Expr Char
getChar' = liftF (GetChar id)
doThing' :: Char -> Expr ()
doThing' c = liftF (DoThing c ())
program' :: Expr ()
program' = do
c <- getChar'
doThing' c
program'
希望更清楚一个容器;要运行它,我们需要以与任何其他递归容器类似的方式处理AST:
eval :: Expr () -> (Char -> IO ()) -> IO ()
eval prog f = iterM alg prog where
alg (GetChar k) = getChar >>= k
alg (DoThing c k) = f c >> k
通过program'
评估eval program' putChar
类似于通过program
投放runContT program putChar
。
答案 1 :(得分:6)
理解monad的真正关键是停止尝试说
monad X
而是开始说
X 是monad
如果是has a certain structure and obeys certain laws,那就是monad。出于在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的最大类事物。
Monad
的最大类是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
对于普通树木,嫁接步骤不进行任何简化。在检查这些操作是否符合monad法则之后我们可以说
具有嫁接且没有简化的树是monad
列表,行李,集合,Maybe
和Identity
将生成的树的所有级别展平为一个级别。在树上任何地方嫁接的所有内容都会在同一个列表或包或集中或Just
中结束。集合还会从生成的单层树中删除任何重复项。
*
/ \
[0, 1]
如果我们将它绑定到\x -> [x*2, x*2 + 1]
,我们用新树替换每个叶子
__*__
/ \
* *
/ \ / \
[0, 1] [2, 3]
然后展平中间层
____*____
/ | | \
[0, 1, 2, 3]
我们可以说
展平的聚合是一棵具有嫁接和简化的树
并且,在检查了monad法律后,我们可以说
扁平化的聚合是monad
Reader e
这样的{p> data Pair a = Pair a a
和pairs有点不同。他们不能将所有结果压平成单层,或者至少不能立即这样做。相反,他们修剪那些与父母没有相同方向的分支。
如果我们从一对开始
*
/ \
<0, 1>
当我们将它绑定到\x -> <x*2, x*2 + 1>
时,我们用新树替换每个叶子
__*__
/ \
* *
/ \ / \
<0, 1> <2, 3>
我们修剪那些没有分支同一方向的分支
__*__
/ \
* *
/ \
<0, 3>
然后可以通过展平图层来进一步简化这一过程
*
/ \
<0, 3>
正如您所指出的,Reader e a
分支的方向数等于e
的可能值数。
我们可以说
修剪和展平的聚合是一棵具有嫁接和简化的树
并且,在检查了monad法律后,我们可以说
修剪和展平的聚合是monad
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
continuation monad有许多分支等于|r| ^ |a|
,即延续a -> r
的可能值的数量。每个分支都标有相应的功能。 延续始终在每个叶子中保持相同的值,我们稍后会证明这一点。我们还将向树的内部节点添加标签,即函数r -> r
,我将在稍后讨论。
我们将使用以下数据类型来编写示例树。
data Tri = A | B | C
我们的示例树将用于return A :: Cont Bool Tri
。树中保存的值的类型Tri
具有三个构造函数,并且控制monad的结果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
类型的a -> Cont r b
的Kleisli箭头,或者当我们查看Cont
构造函数时
a -> (b -> r) -> r
我们可以将契约monad 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
的。
同样,每个延续monad (a -> r) -> r
都可以用
(a -> r) -> r
\f -> r (f a)
某些a :: a
和r :: r -> r
的。这些组合起来构成了一个继续monad总是在每一片叶子中保持相同价值的理由。
当我们将函数\x -> \f -> r x (f (g x))
绑定到continuation monad树上时,我们将g x
记录为新的叶子并记录(r x, g x)
作为新中间节点的标签。树很大,但是我们将使用\x -> \f -> r x (f (g x)) :: Tri -> (Bit -> Bool) -> Bool
绘制另一个完整示例的角落,其中Bit
只有两个构造函数。由此产生的延续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
对于某些\f -> r (f a)
和a :: a
,r :: r -> r
形式的任何续约monad将由树表示
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是一个树,其内部节点用函数标记。它的绑定操作是树移植,然后是简化步骤。简化步骤组成内部节点上的功能。我们可以说
continuation monad是一棵带有移植和简化的树
并且,在检查了monad法律后,我们可以说
延续monad是monad