简体   繁体   English

Haskell Cont monad 如何以及为什么工作?

[英]How and why does the Haskell Cont monad work?

This is how the Cont monad is defined:这是 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

Could you explain how and why this works?你能解释一下这是如何以及为什么起作用的吗? What is it doing?它在做什么?

The first thing to realize about the continuation monad is that, fundamentally, it's not really doing anything at all.关于延续 monad,首先要意识到的是,从根本上说,它根本没有真正任何事情。 It's true!这是真的!

The basic idea of a continuation in general is that it represents the rest of a computation .一般而言,延续的基本思想是它代表计算的其余部分 Say we have an expression like this: foo (bar xy) z .假设我们有一个这样的表达式: foo (bar xy) z Now, extract just the parenthesized portion, bar xy --this is part of the total expression, but it's not just a function we can apply.现在,只提取括号中的部分bar xy xy——这是整个表达式的一部分,但它不仅仅是我们可以应用的函数。 Instead, it's something we need to apply a function to .相反,我们需要将函数应用于. So, we can talk about the "rest of the computation" in this case as being \\a -> foo az , which we can apply to bar xy to reconstruct the complete form.因此,在这种情况下,我们可以将“其余计算”称为\\a -> foo az ,我们可以将其应用于bar xy以重建完整的形式。

Now, it happens that this concept of "the rest of the computation" is useful, but it's awkward to work with, since it's something outside of the subexpression we're considering.现在,碰巧这个“计算的其余部分”的概念很有用,但使用起来很尴尬,因为它是我们正在考虑的子表达式之外的东西。 To make things work better, we can turn things inside-out: extract the subexpression we're interested in, then wrap it in a function that takes an argument representing the rest of the computation: \\k -> k (bar xy) .为了让事情做得更好,我们可以把事情从里到外:提取我们感兴趣的子表达式,然后将它包装在一个函数中,该函数接受一个表示计算其余部分的参数: \\k -> k (bar xy)

This modified version gives us a lot of flexibility--not only does it extract a subexpression from its context, but it lets us manipulate that outer context within the subexpression itself .这个修改后的版本给了我们很大的灵活性——它不仅从它的上下文中提取子表达式,而且它允许我们在子表达式本身内操纵外部上下文 We can think of it as a sort of suspended computation , giving us explicit control over what happens next.我们可以将其视为一种暂停计算,让我们可以明确控制接下来发生的事情。 Now, how could we generalize this?现在,我们如何概括这一点? Well, the subexpression is pretty much unchanged, so let's just replace it with a parameter to the inside-out function, giving us \\xk -> kx --in other words, nothing more than function application, reversed .好吧,子表达式几乎没有变化,所以让我们用一个由内向外函数的参数替换它,给我们\\xk -> kx -- 换句话说,无非是函数 application, reversed We could just as easily write flip ($) , or add a bit of an exotic foreign language flavor and define it as an operator |> .我们可以很容易地写成flip ($) ,或者添加一点异国情调的外语并将其定义为运算符|>

Now, it would be simple, albeit tedious and horribly obfuscating, to translate every piece of an expression to this form.现在,将表达式的每一个部分翻译成这种形式会很简单,尽管很乏味而且非常令人困惑。 Fortunately, there's a better way.幸运的是,有更好的方法。 As Haskell programmers, when we think building a computation within a background context the next thing we think is say, is this a monad?作为 Haskell 程序员,当我们考虑在后台上下文中构建计算时,我们认为接下来要说的是,这是一个 monad 吗? And in this case the answer is yes , yes it is.在这种情况下,答案是肯定的,是的。

To turn this into a monad, we start with two basic building blocks:为了把它变成一个 monad,我们从两个基本的构建块开始:

  • For a monad m , a value of type ma represents having access to a value of type a within the context of the monad.对于 monad mma类型的值表示可以访问 monad 上下文中的a类型值。
  • The core of our "suspended computations" is flipped function application.我们“暂停计算”的核心是翻转函数应用。

What does it mean to have access to something of type a within this context?在此上下文中访问a类型a内容是什么意思? It just means that, for some value x :: a , we've applied flip ($) to x , giving us a function that takes a function which takes an argument of type a , and applies that function to x .这只是意味着,对于某些值x :: a ,我们已经将flip ($)应用于x ,从而为我们提供了一个函数,该函数采用一个a类型参数的函数,并将该函数应用于x Let's say we have a suspended computation holding a value of type Bool .假设我们有一个挂起的计算,其中包含一个Bool类型的值。 What type does this give us?这给了我们什么类型?

> :t flip ($) True
flip ($) True :: (Bool -> b) -> b

So for suspended computations, the type ma works out to (a -> b) -> b ... which is perhaps an anticlimax, since we already knew the signature for Cont , but humor me for now.所以对于挂起的计算,类型ma计算为(a -> b) -> b ... 这可能是一个反高潮,因为我们已经知道Cont的签名,但现在让我幽默一下。

An interesting thing to note here is that a sort of "reversal" also applies to the monad's type: Cont ba represents a function that takes a function a -> b and evaluates to b .这里要注意的一件有趣的事情是,一种“反转”也适用于 monad 的类型: Cont ba表示一个函数,它采用函数a -> b并计算为b As a continuation represents "the future" of a computation, so the type a in the signature represents in some sense "the past".由于延续代表计算的“未来”,因此签名中的类型a在某种意义上代表“过去”。

So, replacing (a -> b) -> b with Cont ba , what's the monadic type for our basic building block of reverse function application?那么,将(a -> b) -> b替换为Cont ba ,我们反向函数应用程序的基本构建块的单子类型是什么? a -> (a -> b) -> b translates to a -> Cont ba ... the same type signature as return and, in fact, that's exactly what it is. a -> (a -> b) -> b转换为a -> Cont ba ...与return相同的类型签名,事实上,这正是它的含义。

From here on out, everything pretty much falls directly out from the types: There's essentially no sensible way to implement >>= besides the actual implementation.从现在开始,几乎所有的东西都直接从类型中脱离出来:除了实际的实现之外,基本上没有明智的方法来实现>>= But what is it actually doing ?但它实际上在做什么

At this point we come back to what I said initially: the continuation monad isn't really doing much of anything.在这一点上,我们回到我最初所说的:延续 monad 并没有真正任何事情。 Something of type Cont ra is trivially equivalent to something of just type a , simply by supplying id as the argument to the suspended computation.类型的东西Cont ra是平凡相当于只需键入一些a ,只需通过提供id作为参数传递给暂停计算。 This might lead one to ask whether, if Cont ra is a monad but the conversion is so trivial, shouldn't a alone also be a monad?这可能会导致人们问,如果Cont ra是一个 monad 但转换是如此微不足道,那么a难道不应该也是一个 monad 吗? Of course that doesn't work as is, since there's no type constructor to define as a Monad instance, but say we add a trivial wrapper, like data Id a = Id a .当然,这不能正常工作,因为没有类型构造函数可以定义为Monad实例,但是假设我们添加了一个简单的包装器,例如data Id a = Id a This is indeed a monad, namely the identity monad.确实是一个 monad,即身份 monad。

What does >>= do for the identity monad? >>=对身份 monad 有什么作用? The type signature is Id a -> (a -> Id b) -> Id b , which is equivalent to a -> (a -> b) -> b , which is just simple function application again.类型签名为Id a -> (a -> Id b) -> Id b ,相当于a -> (a -> b) -> b ,又是简单的函数应用。 Having established that Cont ra is trivially equivalent to Id a , we can deduce that in this case as well, (>>=) is just function application .确定Cont raId a等价之后,我们也可以推断出,在这种情况下, (>>=)只是函数 application

Of course, Cont ra is a crazy inverted world where everyone has goatees, so what actually happens involves shuffling things around in confusing ways in order to chain two suspended computations together into a new suspended computation, but in essence, there isn't actually anything unusual going on!当然, Cont ra是一个疯狂的颠倒世界,每个人都有山羊胡子,所以实际发生的事情涉及以令人困惑的方式将事物打乱,以便将两个暂停的计算链接到一个新的暂停计算中,但本质上,实际上没有任何东西不寻常的事情! Applying functions to arguments, ho hum, another day in the life of a functional programmer.将函数应用于参数,呵呵,函数式程序员生活中的又一天。

Here's fibonacci:这是斐波那契数列:

fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

Imagine you have a machine without a call stack - it only allows tail recursion.想象一下你有一台没有调用栈的机器——它只允许尾递归。 How to execute fib on that machine?如何在那台机器上执行fib You could easily rewrite the function to work in linear, instead of exponential time, but that requires tiny bit of insight and is not mechanical.您可以轻松地将函数重写为线性工作,而不是指数时间,但这需要一点洞察力,而且不是机械的。

The obstacle to making it tail recursive is the third line, where there are two recursive calls.使其尾递归的障碍是第三行,其中有两个递归调用。 We can only make a single call, which also has to give the result.我们只能进行一次调用,它也必须给出结果。 Here's where continuations enter.这是延续进入的地方。

We'll make fib (n-1) take additional parameter, which will be a function specifying what should be done after computing its result, call it x .我们将让fib (n-1)接受额外的参数,这将是一个函数,指定在计算其结果后应该做什么,称为x It will be adding fib (n-2) to it, of course.当然,它会向其中添加fib (n-2) So: to compute fib n you compute fib (n-1) after that, if you call the result x , you compute fib (n-2) , after that, if you call the result y , you return x+y .所以:要计算fib n你计算fib (n-1) ,如果你调用结果x ,你计算fib (n-2) ,之后,如果你调用结果y ,你返回x+y

In other words you have to tell:换句话说,你必须告诉:

How to do the following computation: " fib' nc = compute fib n and apply c to the result"?如何进行以下计算:“ fib' nc = 计算fib n并将c应用于结果”?

The answer is that you do the following: "compute fib (n-1) and apply d to the result", where dx means "compute fib (n-2) and apply e to the result", where ey means c (x+y) .答案是您执行以下操作:“计算fib (n-1)并将d应用于结果”,其中dx表示“计算fib (n-2)并将e应用于结果”,其中ey表示c (x+y) In code:在代码中:

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)

Equivalently, we can use lambdas:同样,我们可以使用 lambdas:

fib' 0 = \c -> c 0
fib' 1 = \c -> c 1
fib' n = \c -> fib' (n-1) $ \x ->
               fib' (n-2) $ \y ->
               c (x+y)

To get actual Fibonacci use identity: fib' n id .要获得实际的斐波那契数,请使用 identity: fib' n id You can think that the line fib (n-1) $ ... passes its result x to the next one.您可以认为fib (n-1) $ ...行将其结果x传递给下一个。

The last three lines smell like a do block, and in fact最后三行闻起来像一个do块,事实上

fib' 0 = return 0
fib' 1 = return 1
fib' n = do x <- fib' (n-1)
            y <- fib' (n-2)
            return (x+y)

is the same, up to newtypes, by definition of monad Cont .根据 monad Cont的定义,直到新类型都是相同的。 Note differences.注意差异。 There's \\c -> at the beginning, instead of x <- ... there's ... $ \\x -> and c instead of return .\\c ->开头,而不是x <- ... ... $ \\x ->c而不是return

Try writing factorial n = n * factorial (n-1) in a tail recursive style using CPS.尝试使用 CPS 以尾递归样式编写factorial n = n * factorial (n-1)

How does >>= work? >>=如何工作的? m >>= k is equivalent to m >>= k等价于

do a <- m
   t <- k a
   return t

Making the translation back, in the same style as in fib' , you get以与fib'相同的风格进行翻译,你会得到

\c -> m $ \a ->
      k a $ \t ->
      c t

simplifying \\t -> ct to c简化\\t -> ctc

m >>= k = \c -> m $ \a -> k a c

Adding newtypes you get添加您获得的新类型

m >>= k  = Cont $ \c -> runCont m $ \a -> runCont (k a) c

which is on top of this page.这是在这个页面的顶部。 It's complex, but if you know how to translate between do notation and direct use, you don't need to know exact definition of >>= !这很复杂,但是如果您知道如何在do表示法和直接使用之间进行转换,您就不需要知道>>=确切定义! Continuation monad is much clearer if you look at do-blocks.如果您查看 do-blocks,则 Continuation monad 会更加清晰。

Monads and continuations单子和延续

If you look at this usage of list 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)

that looks like continuation!看起来像继续! In fact, (>>=) when you apply one argument has type (a -> mb) -> mb which is Cont (mb) a .事实上, (>>=)当您应用一个参数时,类型为(a -> mb) -> mb ,即Cont (mb) a See sigfpe's Mother of all monads for explanation.有关解释,请参阅 sigfpe 的所有 monad I'd regard that as a good continuation monad tutorial, though it wasn't probably meant as it.我认为这是一个很好的延续 monad 教程,尽管它可能不是这个意思。

Since continuations and monads are so strongly related in both directions, I think what applies to monads applies to continuations: only hard work will teach you them, and not reading some burrito metaphor or analogy.由于延续和 monad 在两个方向上都如此密切相关,我认为适用于 monad 的内容也适用于延续:只有努力工作才能教会你它们,而不是阅读一些墨西哥卷饼的比喻或类比。

EDIT: Article migrated to the link below.编辑:文章迁移到下面的链接。

I've written up a tutorial directly addressing this topic that I hope you will find useful.我已经写了一个直接解决这个主题的教程,我希望你会觉得有用。 (It certainly helped cement my understanding!) It's a bit too long to fit comfortably in a Stack Overflow topic, so I've migrated it to the Haskell Wiki. (它确实帮助巩固了我的理解!)它有点太长,无法舒适地适应 Stack Overflow 主题,所以我已将其迁移到 Haskell Wiki。

Please see: MonadCont under the hood请参阅:引擎盖下的 MonadCont

I think the easiest way to get a grip on the Cont monad is to understand how to use its constructor.我认为掌握Cont monad 的最简单方法是了解如何使用其构造函数。 I'm going to assume the following definition for now, although the realities of the transformers package are slightly different:我现在将假设以下定义,尽管transformers包的实际情况略有不同:

newtype Cont r a = Cont { runCont :: (a -> r) -> r }

This gives:这给出:

Cont :: ((a -> r) -> r) -> Cont r a

so to build a value of type Cont ra , we need to give a function to Cont :所以要构建一个Cont ra类型的值,我们需要给Cont一个函数:

value = Cont $ \k -> ...

Now, k itself has type a -> r , and the body of the lambda needs to have type r .现在, k本身具有类型a -> r ,并且 lambda 的主体需要具有类型r An obvious thing to do would be to apply k to a value of type a , and get a value of type r .一个显而易见的事情是将k应用于 a 类型a值,并获得r类型的r We can do that, yes, but that's really only one of many things we can do.我们可以做到这一点,是的,但这实际上只是我们可以做的许多事情之一。 Remember that value need not be polymorphic in r , it might be of type Cont String Integer or something else concrete.请记住, r中的value不必是多态的,它可能是Cont String Integer类型或其他具体类型。 So:所以:

  • We could apply k to several values of type a , and combine the results somehow.我们可以将k应用于k类型a多个值,并以某种方式组合结果。
  • We could apply k to a value of type a , observe the result, and then decide to apply k to something else based on that.我们可以申请k于类型的值a ,观察效果,然后再决定采取k基于对别的东西。
  • We could ignore k altogether and just produce a value of type r ourselves.我们可以完全忽略k并自己生成一个类型为r的值。

But what does all this mean?但这一切意味着什么? What does k end up being? k最终是什么? Well, in a do-block, we might have something looking like this:好吧,在 do-block 中,我们可能有这样的东西:

flip runCont id $ do
  v <- thing1
  thing2 v
  x <- Cont $ \k -> ...
  thing3 x
  thing4

Here's the fun part: we can, in our minds and somewhat informally, split the do-block in two at the occurrence of the Cont constructor, and think of the rest of the entire computation after it as a value in itself.这是有趣的部分:我们可以,在我们的头脑中,有点非正式地,在Cont构造函数出现时将 do-block 分成两部分,并将其后的整个计算的其余部分视为一个值本身。 But hold on, what it is depends on what x is, so it's really a function from a value x of type a to some result value:但是且慢,什么是取决于什么x是,所以真的从一个值函数x型的a对一些结果值:

restOfTheComputation x = do
  thing3 x
  thing4

In fact, this restOfTheComputation is roughly speaking what k ends up being.事实上,这个restOfTheComputation粗略地说k最终是什么。 In other words, you call k with a value which becomes the result x of your Cont computation, the rest of the computation runs, and then the r produced winds its way back into your lambda as the result of the call to k .换句话说,您使用一个值调用k ,该值成为您的Cont计算的结果x ,其余的计算运行,然后产生的r作为对k调用的结果返回到您的 lambda 中。 So:所以:

  • if you called k multiple times, the rest of the computation will get run multiple times, and the results may be combined however you wish.如果您多次调用k ,其余的计算将运行多次,并且结果可以根据您的意愿进行组合。
  • if you didn't call k at all, the rest of the entire computation will be skipped, and the enclosing runCont call will just give you back whatever value of type r you managed to synthesise.如果您根本没有调用k ,则整个计算的其余部分将被跳过,并且封闭的runCont调用将返回您设法综合的任何类型r值。 That is, unless some other part of the computation is calling you from their k , and messing about with the result...也就是说,除非计算的其他部分从他们的k调用,并弄乱结果......

If you're still with me at this point it should be easy to see this could be quite powerful.如果此时您还和我在一起,应该很容易看出这可能非常强大。 To make the point a little, let's implement some standard type classes.为了说明这一点,让我们实现一些标准类型类。

instance Functor (Cont r) where
  fmap f (Cont c) = Cont $ \k -> ...

We're given a Cont value with bind result x of type a , and a function f :: a -> b , and we want to make a Cont value with bind result fx of type b .我们得到了一个带有类型a绑定结果xCont值和一个函数f :: a -> b ,我们想要创建一个带有类型b绑定结果fxCont值。 Well, to set the bind result, just call k ...好吧,要设置绑定结果,只需调用k ...

  fmap f (Cont c) = Cont $ \k -> k (f ...

Wait, where do we get x from?等等,我们从哪里得到x Well, it's going to involve c , which we haven't used yet.好吧,它将涉及c ,我们还没有使用它。 Remember how c works: it gets given a function, and then calls that function with its bind result.记住c是如何工作的:它得到一个函数,然后用它的绑定结果调用该函数。 We want to call our function with f applied to that bind result.我们想调用我们的函数,并将f应用于该绑定结果。 So:所以:

  fmap f (Cont c) = Cont $ \k -> c (\x -> k (f x))

Tada!多田! Next up, Applicative :接下来, Applicative

instance Applicative (Cont r) where
  pure x = Cont $ \k -> ...

This one's simple.这个很简单。 We want the bind result to be the x we get.我们希望绑定结果是我们得到的x

  pure x = Cont $ \k -> k x

Now, <*> :现在, <*>

  Cont cf <*> Cont cx = Cont $ \k -> ...

This a little trickier, but uses essentially the same ideas as in fmap: first get the function from the first Cont , by making a lambda for it to call:这有点棘手,但使用与 fmap 基本相同的想法:首先从第一个Cont获取函数,通过为它创建一个 lambda 来调用:

  Cont cf <*> Cont cx = Cont $ \k -> cf (\fn -> ...

Then get the value x from the second, and make fn x the bind result:然后从第二个中获取值x ,并使fn x成为绑定结果:

  Cont cf <*> Cont cx = Cont $ \k -> cf (\fn -> cx (\x -> k (fn x)))

Monad is much the same, although requires runCont or a case or let to unpack the newtype. Monad大致相同,但需要runCont或 case 或 let 来解压 newtype。

This answer is already quite long, so I won't go into ContT (in short: it is exactly the same as Cont ! The only difference is in the kind of the type constructor, the implementations of everything are identical) or callCC (a useful combinator that provides a convenient way to ignore k , implementing early exit from a sub-block).这个答案已经很长了,所以我不会进入ContT (简而言之:它与Cont完全相同!唯一的区别在于类型构造函数的类型,所有内容的实现都是相同的)或callCC (一个有用的组合器,提供了一种方便的方法来忽略k ,实现从子块的提前退出)。

For a simple and plausible application, try Edward Z. Yang's blog post implementing labelled break and continue in for loops .对于一个简单而合理的应用程序,请尝试 Edward Z. Yang 的博客文章实现 标记为 break 并在 for 循环中继续

Trying to complement the other answers:试图补充其他答案:

Nested lambdas are horrible for readability.嵌套的 lambda 表达式的可读性很差。 This is exactly the reason why let... in... and ... where ... exist, to get rid of nested lambdas by using intermediate variables.这正是 let... in... 和 ... where ... 存在的原因,以通过使用中间变量摆脱嵌套的 lambda。 Using those, the bind implementation can be refactored into:使用这些,绑定实现可以重构为:

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

Which hopefully makes what is happening clearer.希望这能让正在发生的事情变得更清楚。 The return implementation boxes value with a lazy apply.使用延迟应用返回实现框值。 Using runCont id applies id to the boxed value, which returns the original value.使用 runCont id 将 id 应用于装箱值,它返回原始值。

For any monad where any boxed value can simply be unboxed, there is generally a trivial implementation of bind, which is to simply unbox the value and apply a monadic function to it.对于可以简单地拆箱任何装箱值的任何 monad,通常有一个简单的 bind 实现,即简单地拆箱值并对其应用 monadic 函数。

To get the obfuscated implementation in the original question, first replace ka with Cont $ runCont (ka) , which in turn can be replaced with Cont $ \\c-> runCont (ka) c要获得原始问题中的混淆实现,首先将 ka 替换为 Cont $ runCont (ka) ,而后者又可以替换为 Cont $ \\c-> runCont (ka) c

Now, we can move the where into a subexpression, so that we are left with现在,我们可以将 where 移动到子表达式中,这样我们就剩下

Cont $ \c-> ( runCont (k a) c where a = runCont m id )

The expression within parentheses can be desugared into \\a -> runCont (ka) c $ runCont m id.括号内的表达式可以脱糖为 \\a -> runCont (ka) c $ runCont m id。

To finish, we use the property of runCont, f ( runCont mg) = runCont m (fg), and we are back to the original obfuscated expression.最后,我们使用 runCont 的属性,f (runCont mg) = runCont m (fg),我们回到原始的混淆表达式。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM