这是Cont monad的定义方式:
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
instance Monad (Cont r) where
return a = Cont ($ a)
m >>= k = Cont $ \c -> runCont m $ \a -> runCont (k a) c
您能解释一下它如何运作以及为什么起作用吗?到底在做什么
Answers:
关于延续monad的第一件事是,从根本上讲,它根本没有做任何事情。这是真的!
通常,延续的基本思想是它代表计算的其余部分。假设我们有一个这样的表达:foo (bar x y) z
。现在,仅提取括号部分,bar x y
这是总表达式的一部分,但不仅仅是我们可以应用的函数。相反,它的东西,我们需要一个功能应用到。因此,在这种情况下,我们可以将“其余的计算”称为\a -> foo a z
,我们可以将其bar x y
应用于重构完整的表单。
现在,碰巧“其余的计算”这一概念很有用,但是处理起来很尴尬,因为它不在我们正在考虑的子表达式之外。为了使事情更好,我们可以将事情由内而外:提取我们感兴趣的子表达式,然后将其包装在一个函数中,该函数接受一个表示其余计算的参数:\k -> k (bar x y)
。
这个修改后的版本为我们提供了很大的灵活性-不仅从其上下文中提取了子表达式,而且还使我们能够在子表达式本身内操纵外部上下文。我们可以将其视为一种暂停的计算,从而使我们可以对接下来发生的事情进行明确控制。现在,我们如何概括这一点?好吧,子表达式几乎没有什么变化,所以我们只需要用一个由内而外的函数的参数替换它,\x k -> k x
换句话说,就是给函数应用程序“ reversed”。我们既可以轻松地编写代码flip ($)
,也可以添加一些具有异国情调的外语风格并将其定义为运算符|>
。
现在,将表达式的每一部分转换为这种形式将是简单,繁琐且令人费解的。幸运的是,有更好的方法。作为Haskell程序员,当我们认为在后台上下文中构建计算时,我们认为接下来要说的是,这是monad吗?在这种情况下,答案是肯定的,是的。
为了将其变成单子,我们从两个基本的构建块开始:
m
,type的值m a
表示可以a
在monad的上下文中访问type的值。a
在这种情况下访问某种类型的东西意味着什么?这只是意味着,对于某些值x :: a
,我们已经应用flip ($)
到它x
,给我们一个函数,该函数采用一个接受类型为实参的a
函数,然后将该函数应用于x
。假设我们有一个暂挂的计算,它持有type的值Bool
。这给我们什么类型?
> :t flip ($) True
flip ($) True :: (Bool -> b) -> b
因此对于暂停的计算,该类型m a
适用于(a -> b) -> b
...,这可能是一个反高潮,因为我们已经知道了的签名Cont
,但现在让我很幽默。
这里要注意的一个有趣的事情是,一种“逆转”也适用于monad的类型:Cont b a
表示一个函数,该函数接受一个函数a -> b
并求值为b
。由于延续表示计算的“未来”,因此a
签名中的类型在某种意义上表示“过去”。
因此,更换(a -> b) -> b
用Cont b a
,有什么单子类型为我们的反向功能的应用程序的基本构建块?a -> (a -> b) -> b
转换为与a -> Cont b a
...具有相同的类型签名return
,实际上,这正是它的本质。
从现在开始,几乎所有内容都直接从类型中掉了出来:>>=
除了实际的实现之外,基本上没有明智的实现方法。但是它实际上在做什么呢?
在这一点上,我们回到我最初所说的:延续monad实际上并没有做任何事情。类型的东西Cont r a
是平凡只相当于类型的东西a
,只需通过提供id
作为参数传递给暂停计算。这可能会引起一个问题:如果Cont r a
是单声道,但转换是否如此琐碎,难道不是a
一个人也应该是单声道吗?当然,这并不能正常工作,因为没有类型构造函数可以定义为Monad
实例,但是要说我们添加了一个琐碎的包装器,例如data Id a = Id a
。这确实是一个monad,即标识monad。
什么是>>=
对身份的单子呢?类型签名是Id a -> (a -> Id b) -> Id b
,相当于a -> (a -> b) -> b
,再次是简单函数应用程序。建立了Cont r a
与之等效的东西Id a
,我们就可以得出在这种情况下,(>>=)
也仅仅是功能应用。
当然,这Cont r a
是一个疯狂的倒置世界,每个人都有山羊胡子,因此实际发生的事情是以令人困惑的方式将周围的事物改组,以便将两个暂挂的计算链在一起成为一个新的暂挂的计算,但实际上,没有任何异常上!将函数应用于参数,等等,这是函数式程序员的第二天。
flip ($) a
为($ a)
。
这是斐波那契:
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)
假设您有一台没有调用堆栈的机器-它只允许尾部递归。如何fib
在那台机器上执行?您可以轻松地重写该函数以使其线性工作,而不是指数时间工作,但这需要很少的洞察力,并且不是机械的。
使其成为尾递归的障碍是第三行,其中有两个递归调用。我们只能打一个电话,这也必须给出结果。这是连续性输入的地方。
我们将fib (n-1)
采用其他参数,该参数将是一个函数,指定在计算其结果后应执行的操作,将其称为x
。当然,它将添加fib (n-2)
到它。因此:fib n
计算fib (n-1)
之后,如果调用结果x
,就进行计算fib (n-2)
,如果调用结果y
,则就进行返回x+y
。
换句话说,您必须告诉:
如何进行以下计算:“ fib' n c
=计算fib n
并应用于c
结果”?
答案是您要执行以下操作:“计算fib (n-1)
并应用于d
结果”,其中d x
表示“计算fib (n-2)
并应用于e
结果”,其中e y
表示c (x+y)
。在代码中:
fib' 0 c = c 0
fib' 1 c = c 1
fib' n c = fib' (n-1) d
where d x = fib' (n-2) e
where e y = c (x+y)
同样,我们可以使用lambda:
fib' 0 = \c -> c 0
fib' 1 = \c -> c 1
fib' n = \c -> fib' (n-1) $ \x ->
fib' (n-2) $ \y ->
c (x+y)
要获取实际的斐波那契使用标识:fib' n id
。您可以认为该行fib (n-1) $ ...
会将其结果传递x
给下一个。
最后三行闻起来像do
块,实际上
fib' 0 = return 0
fib' 1 = return 1
fib' n = do x <- fib' (n-1)
y <- fib' (n-2)
return (x+y)
根据monad的定义,直到新类型都是相同的Cont
。注意差异。有\c ->
开头,而不是x <- ...
有... $ \x ->
和c
代替return
。
尝试factorial n = n * factorial (n-1)
使用CPS以尾部递归样式编写。
>>=
工作如何?m >>= k
相当于
do a <- m
t <- k a
return t
使得翻译回来,在相同的样式在fib'
,你
\c -> m $ \a ->
k a $ \t ->
c t
简化\t -> c t
为c
m >>= k = \c -> m $ \a -> k a c
添加新类型
m >>= k = Cont $ \c -> runCont m $ \a -> runCont (k a) c
在此页面顶部。这很复杂,但是如果您知道如何在do
符号和直接使用之间进行转换,则无需知道>>=
!的确切定义。如果您看一下do-block,Continuation monad会更加清晰。
单声道和延续
如果您看一下list monad的这种用法...
do x <- [10, 20]
y <- [3,5]
return (x+y)
[10,20] >>= \x ->
[3,5] >>= \y ->
return (x+y)
([10,20] >>=) $ \x ->
([3,5] >>=) $ \y ->
return (x+y)
看起来像延续!实际上,(>>=)
当您应用一个参数时,其类型(a -> m b) -> m b
为Cont (m b) a
。有关详细信息,请参见sigfpe的《单子之母》。我认为这是一个不错的续集monad教程,尽管可能并非如此。
由于延续和单子在两个方向上都紧密相关,因此我认为适用于单子的东西也适用于延续:只有努力工作才能教给您它们,而不是读一些墨西哥卷饼的比喻或类比。
where e y = c (x+y)
编辑:文章迁移到下面的链接。
我已经写了一个直接解决该主题的教程,希望对您有用。(它肯定有助于巩固我的理解!)由于太长了,无法轻松地适应Stack Overflow主题,因此我已将其迁移到Haskell Wiki。
请参阅:引擎盖下的MonadCont
我认为掌握Cont
monad的最简单方法是了解如何使用其构造函数。尽管transformers
软件包的实际情况略有不同,但我现在将假定以下定义:
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
这给出:
Cont :: ((a -> r) -> r) -> Cont r a
因此,要构建type的值Cont r a
,我们需要给以下函数Cont
:
value = Cont $ \k -> ...
现在,k
它本身具有类型a -> r
,并且lambda的主体需要具有type r
。显而易见的事情是将k
type应用于值a
,并获得type的值r
。是的,我们可以这样做,但这实际上只是我们可以做的许多事情之一。请记住,value
不需要在中是多态的r
,它可以是类型的Cont String Integer
或其他具体的东西。所以:
k
几个type的值a
,并以某种方式组合结果。k
类型的值应用于a
,观察结果,然后k
基于该值决定将其应用于其他对象。k
完全忽略,而只是r
自己产生类型的值。但是,这一切意味着什么?是什么k
最终会被?好吧,在一个do-block中,我们可能会有类似以下的内容:
flip runCont id $ do
v <- thing1
thing2 v
x <- Cont $ \k -> ...
thing3 x
thing4
这是有趣的部分:我们可以在脑海中以某种非正式的方式将do块在Cont
构造函数出现时一分为二,然后将整个计算后的其余部分视为自身的值。但是且慢,什么是取决于什么x
是,所以这是一个真正的函数从数值x
型的a
一些结果值:
restOfTheComputation x = do
thing3 x
thing4
事实上,这restOfTheComputation
是粗略地说什么k
了是结束。换句话说,你可以调用k
与变成结果的值x
您的Cont
计算,计算运行的其余部分,然后将r
产生的风的方式回到你的lambda作为调用的结果k
。所以:
k
多次调用,则其余计算将多次运行,并且结果可以按您希望的方式合并。k
,则将跳过整个计算的其余部分,而封闭的runCont
调用将只返回r
您设法合成的任何类型的值。也就是说,除非计算的其他部分在呼唤你从他们的 k
,和摆弄结果...如果此时您仍然与我在一起,那么应该很容易看到它可能非常强大。为了说明这一点,让我们实现一些标准类型类。
instance Functor (Cont r) where
fmap f (Cont c) = Cont $ \k -> ...
我们给定了Cont
带有x
类型为bind结果的值a
和一个函数f :: a -> b
,并且我们想为一个Cont
带有f x
类型为bind结果的值b
。好吧,要设置绑定结果,只需调用k
...
fmap f (Cont c) = Cont $ \k -> k (f ...
等一下,我们从哪里来x
?好吧,它将涉及到c
,我们还没有使用过。记住c
工作原理:给它提供一个函数,然后用绑定结果调用该函数。我们要调用我们的功能与f
应用,其结合的结果。所以:
fmap f (Cont c) = Cont $ \k -> c (\x -> k (f x))
多田 接下来Applicative
:
instance Applicative (Cont r) where
pure x = Cont $ \k -> ...
这很简单。我们希望绑定结果是x
我们得到的。
pure x = Cont $ \k -> k x
现在,<*>
:
Cont cf <*> Cont cx = Cont $ \k -> ...
这有点棘手,但使用的原理与fmap基本相同:首先Cont
通过创建一个lambda使其调用来从first中获取函数:
Cont cf <*> Cont cx = Cont $ \k -> cf (\fn -> ...
然后x
从第二个值获取值,并fn x
生成绑定结果:
Cont cf <*> Cont cx = Cont $ \k -> cf (\fn -> cx (\x -> k (fn x)))
Monad
几乎相同,尽管需要runCont
打开包装或拆开包装。
这个答案已经很长了,所以我不再赘述ContT
(简而言之:它与Cont
!完全一样!唯一的区别在于类型构造函数的类型,所有实现都相同)或callCC
(提供了一种忽略的便捷方法k
,实现了子块的提前退出。
对于一个简单而合理的应用程序,请尝试Edward Z. Yang的博客文章,该文章实现了带标签的break并继续进行for循环。
试图补充其他答案:
嵌套的lambda可读性极差。这就是为什么让...在...和...在...存在的地方通过使用中间变量来摆脱嵌套lambda的原因。使用这些绑定操作可以重构为:
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
instance Monad (Cont r) where
return a = Cont ($ a)
m >>= k = k a
where a = runCont m id
希望这使正在发生的事情更加清晰。返回实现框的值带有延迟应用。使用runCont id将id应用于装箱的值,该值将返回原始值。
对于可以将任何装箱的值简单地拆箱的任何monad,通常都有bind的简单实现,即简单地拆开该值并将其应用monadic函数。
要在原始问题中获得模糊的实现,请先用Cont $ runCont(ka)替换ka,然后用Cont $ \ c-> runCont(ka)c替换ka
现在,我们可以将where移到子表达式中,这样我们就可以
Cont $ \c-> ( runCont (k a) c where a = runCont m id )
括号内的表达式可以替换为\ a-> runCont(ka)c $ runCont m id。
最后,我们使用runCont的属性,f(runCont mg)= runCont m(fg),我们回到原始的混淆表达式。