时间:2010-07-23 21:31:44

标签: haskell monads continuations

5 个答案:

答案 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),然后回到原始的混淆表达式。