答案 0 :(得分:107)
答案 1 :(得分:39)
答案 2 :(得分:17)
答案 3 :(得分:9)
我认为掌握Cont
monad的最简单方法是了解如何使用它的构造函数。我现在将假设以下定义,尽管transformers
包的实际情况略有不同:
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
这给出了:
Cont :: ((a -> r) -> r) -> Cont r a
所以要构建Cont r a
类型的值,我们需要为Cont
提供一个函数:
value = Cont $ \k -> ...
现在,k
本身的类型为a -> r
,而lambda的主体需要具有类型r
。一个显而易见的事情是将k
应用于a
类型的值,并获取类型r
的值。我们可以这样做,是的,但这只是我们能做的很多事情中的一件事。请记住,value
r
中的Cont String Integer
不一定是多态的,它可能是k
类型或其他具体的东西。所以:
a
应用于多个k
类型的值,并以某种方式合并结果。a
应用于k
类型的值,观察结果,然后决定基于此将k
应用于其他内容。r
,只是自己生成k
类型的值。但这一切意味着什么? flip runCont id $ do
v <- thing1
thing2 v
x <- Cont $ \k -> ...
thing3 x
thing4
最终会成为什么?好吧,在一个块中,我们可能会看到这样的东西:
Cont
这是有趣的部分:我们可以在我们的脑海中以某种非正式的方式在x
构造函数出现时将do-block拆分为两个,然后考虑整个计算的其余部分 它本身就是一个价值。但请坚持,它取决于x
是什么,所以它实际上是一个函数,从类型a
的值restOfTheComputation x = do
thing3 x
thing4
到某个结果值:< / p>
restOfTheComputation
事实上,k
粗略地说 k
最终会是什么。换句话说,您使用的值调用x
,该值将成为Cont
计算的结果r
,其余计算运行,然后生成的k
作为调用k
的结果,回到你的lambda。所以:
k
,其余的计算将会多次运行,结果可能会合并到你想要的地方。runCont
,则会跳过整个计算的其余部分,并且封闭的r
调用将只返回k
类型的任何值你设法综合了。也就是说,除非计算的其他部分从他们的 instance Functor (Cont r) where
fmap f (Cont c) = Cont $ \k -> ...
中调用你,并且弄乱了结果...... 如果你现在还和我在一起,应该很容易看出这可能非常强大。为了说明一点,让我们实现一些标准类型类。
Cont
我们的x
值为a
,类型为f :: a -> b
,函数为Cont
,我们想要f x
类型为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 -> ...
:
Cont
这有点棘手,但使用与fmap基本相同的想法:首先从第一个 Cont cf <*> Cont cx = Cont $ \k -> cf (\fn -> ...
获取函数,通过为它调用lambda来调用:
x
然后从第二个获取值fn x
,并使 Cont cf <*> Cont cx = Cont $ \k -> cf (\fn -> cx (\x -> k (fn x)))
绑定结果:
Monad
runCont
大致相同,但需要ContT
或案例或者解压新类型。
这个答案已经很长了,所以我不会进入Cont
(简而言之:它与callCC
完全相同!唯一的区别在于类型构造函数的种类,一切的实现都是相同的)或k
(一个有用的组合器,它提供了一种方便的方法来忽略{{1}},实现从子块中提前退出)。
对于简单易懂的应用程序,请尝试执行labelled break and continue in for loops的Edward Z. Yang的博客文章。
答案 4 :(得分:1)
试图补充其他答案:
嵌套的lambda可读性很差。这正是为什么让...在...和...在...存在的地方通过使用中间变量来摆脱嵌套lambda的原因。使用这些,可以将bind实现重构为:
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应用于装箱的值,该值将返回原始值。
对于任何可以简单地取消装箱值的单子,通常有一个绑定的简单实现,即简单地将值拆箱并对其应用单子函数。
要在原始问题中获得模糊的实现,请先用Cont $ runCont(k a)替换k a,然后用Cont $ \ c-> runCont(k a)c替换
现在,我们可以将where移到子表达式中,以便我们离开
Cont $ \c-> ( runCont (k a) c where a = runCont m id )
括号内的表达式可以改为\ a-> runCont(ka)c $ runCont m id。
最后,我们使用runCont的属性,f(runCont m g)= runCont m(f.g),然后回到原始的混淆表达式。