zoukankan      html  css  js  c++  java
  • Understanding continuations

    原文地址http://fsharpforfunandprofit.com/posts/computation-expressions-continuations/

    上一篇中我们看到复杂代码是如何通过使用computation expressions得到简化。

    使用computation expression前的代码

    let log p = printfn "expression is %A" p
    
    let loggedWorkflow = 
        let x = 42
        log x
        let y = 43
        log y
        let z = x + y
        log z
        //return
        z

    使用computation expression后的代码

    let loggedWorkflow = 
        logger
            {
            let! x = 42
            let! y = 43
            let! z = x + y
            return z
            }

    这里使用的是let!(let bang)而非普通的let。我们是否可以自己来模拟let!从而深入理解这里面到底发生了什么?当然可以,不过我们首先需要搞懂什么是continuations

    Continuations

    命令式编程中,一个函数中有“returning”的概念。当调用一个函数时,先进入这个函数,然后退出,类似于入栈和出栈。

    典型的C#代码

    public int Divide(int top, int bottom)
    {
        if (bottom==0)
        {
            throw new InvalidOperationException("div by 0");
        }
        else
        {
            return top/bottom;
        }
    }
    
    public bool IsEven(int aNumber)
    {
        var isEven = (aNumber % 2 == 0);
        return isEven;
    }

    这样的函数我们已经很熟悉了,但是也许我们未曾考虑过这样一个问题:被调用的函数本身就决定了要做什么事情。

    例如,函数Divide的实现就决定了可能会抛出一个异常,但如果我们不想得到一个异常呢?也许是想得到一个nullable<int>,又或者是将有关信息打印到屏幕如"#DIV/0"。简单来说,为什么不能由函数调用者而非函数自身来决定应该怎么处理。

    IsEven函数也是如此,对于其返回的boolean类型的值我们将如何处理?对它进行分支讨论,还是打印到报告中?比起返回一个boolean类型的值让调用者来处理,为什么不考虑让调用者告诉被调用方下一步如何处理。

    这就是continuation,它是一个简单的函数,你可以向其中传入另一个函数来指示下一步该做什么。

    重写刚才的C#代码,由调用方传入一个函数,被调用方用这个函数来处理一些事情。

    public T Divide<T>(int top, int bottom, Func<T> ifZero, Func<int,T> ifSuccess)
    {
        if (bottom==0)
        {
            return ifZero();
        }
        else
        {
            return ifSuccess( top/bottom );
        }
    }
    
    public T IsEven<T>(int aNumber, Func<int,T> ifOdd, Func<int,T> ifEven)
    {
        if (aNumber % 2 == 0)
        {
            return ifEven(aNumber);
        }
        else
        {   return ifOdd(aNumber);
        }
    }

    嗯,在C#中,传入太多的Func类型的参数会使代码丑陋,所以C#中这不常见,但是在F#中使用则非常简单。

    使用continuations前的代码

    let divide top bottom = 
        if (bottom=0) 
        then invalidOp "div by 0"
        else (top/bottom)
        
    let isEven aNumber = 
        aNumber % 2 = 0

    使用continuations后的代码

    let divide ifZero ifSuccess top bottom = 
        if (bottom=0) 
        then ifZero()
        else ifSuccess (top/bottom)
        
    let isEven ifOdd ifEven aNumber = 
        if (aNumber % 2 = 0)
        then aNumber |> ifEven 
        else aNumber |> ifOdd

    有几点说明。首先,函数参数靠前,如isZero作为第一个参数,isSuccess作为第二个参数。这会方便我们使用函数的部分应用 partial application

    其次,在isEven例子中,aNumber |> ifEvenaNumber |> ifOdd的写法表明我们将当前值加入到continuation的管道中,这篇文章的后面部分也会使用相同的模式。

    Continuation的例子

    采用推荐的Continuation方式,我们可以用三种不同的方式调用divide函数,这取决于调用者的意图。

    以下是调用divide函数的三个应用场景:

    1、将结果加入到消息管道并且打印

    2、将结果转换成option,除法失败时返回None,除法成功时返回Some

    3、除法失败时抛出异常,除法成功时返回结果值

    // Scenario 1: pipe the result into a message
    // ----------------------------------------
    // setup the functions to print a message
    let ifZero1 () = printfn "bad"
    let ifSuccess1 x = printfn "good %i" x
    
    // use partial application
    let divide1  = divide ifZero1 ifSuccess1
    
    //test
    let good1 = divide1 6 3
    let bad1 = divide1 6 0
    
    // Scenario 2: convert the result to an option
    // ----------------------------------------
    // setup the functions to return an Option
    let ifZero2() = None
    let ifSuccess2 x = Some x
    let divide2  = divide ifZero2 ifSuccess2
    
    //test
    let good2 = divide2 6 3
    let bad2 = divide2 6 0
    
    // Scenario 3: throw an exception in the bad case
    // ----------------------------------------
    // setup the functions to throw exception
    let ifZero3() = failwith "div by 0"
    let ifSuccess3 x = x
    let divide3  = divide ifZero3 ifSuccess3
    
    //test
    let good3 = divide3 6 3
    let bad3 = divide3 6 0

    现在,调用者不需要到处对divide抓异常了。调用者决定一个异常是否被抛出,而不是被调用的函数决定是否抛出一个异常。即,如果调用者想对除法失败时抛出异常处理,则将isZero3函数传入到divide函数中。

    同样的三个调用isEven的场景类似处理

    // Scenario 1: pipe the result into a message
    // ----------------------------------------
    // setup the functions to print a message
    let ifOdd1 x = printfn "isOdd %i" x
    let ifEven1 x = printfn "isEven %i" x
    
    // use partial application
    let isEven1  = isEven ifOdd1 ifEven1
    
    //test
    let good1 = isEven1 6 
    let bad1 = isEven1 5
    
    // Scenario 2: convert the result to an option
    // ----------------------------------------
    // setup the functions to return an Option
    let ifOdd2 _ = None
    let ifEven2 x = Some x
    let isEven2  = isEven ifOdd2 ifEven2
    
    //test
    let good2 = isEven2 6 
    let bad2 = isEven2 5
    
    // Scenario 3: throw an exception in the bad case
    // ----------------------------------------
    // setup the functions to throw exception
    let ifOdd3 _ = failwith "assert failed"
    let ifEven3 x = x
    let isEven3  = isEven ifOdd3 ifEven3
    
    //test
    let good3 = isEven3 6 
    let bad3 = isEven3 5

    此时,调用者不用到处对返回的boolean类型值使用if/then/else结构处理了,调用者想怎么处理,就将对应的处理函数作为参数传入divide函数中即可。

    在 designing with types中,我们已经见识过continuations了。

    type EmailAddress = EmailAddress of string
    
    let CreateEmailAddressWithContinuations success failure (s:string) = 
        if System.Text.RegularExpressions.Regex.IsMatch(s,@"^S+@S+.S+$") 
            then success (EmailAddress s)
            else failure "Email address must contain an @ sign"

    测试代码如下

    // setup the functions 
    let success (EmailAddress s) = printfn "success creating email %s" s        
    let failure  msg = printfn "error creating email: %s" msg
    let createEmail = CreateEmailAddressWithContinuations success failure
    
    // test
    let goodEmail = createEmail "x@example.com"
    let badEmail = createEmail "example.com"

    Continuation passing style

    使用continuation将产生一种编程风格"continuation passing style" (CPS),其中,调用每个函数时,都有一个函数参数指示下一步做什么。

    为了看到区别,我们首先看一个标准的直接风格的编程,进入和退出一个函数,如

    call a function ->
       <- return from the function
    call another function ->
       <- return from the function
    call yet another function ->
       <- return from the function

    在CPS中,则是用一个函数链

    evaluate something and pass it into ->
       a function that evaluates something and passes it into ->
          another function that evaluates something and passes it into ->
             yet another function that evaluates something and passes it into ->
                ...etc...

    直接风格的编程中,存在一个函数之间的层级关系。最顶层的函数类似于“主控制器”,调用一个分支,然后另一个,决定何时进行分支,何时进行循环等。

    在CPS中,则不存在这样一个“主控制器”,而是有一个“管道”的控制流,负责单个任务的函数在其中按顺序依次执行。

    如果你曾在GUI中附加一个事件句柄(event handler)到一个按钮单击,或者通过BeginInvoke使用回调,那你已经在无意中使用了CPS。事实上,这种风格是理解async工作流的关键,这一点会在以后的文章中讨论。

    Continuations and 'let'

    以上讨论的CPS等在let内部是如何组合的?

    首先回顾(revisit)一下 let 的本质。

    记住非顶级的let不能被外界访问,它总是作为其外层代码块的一部分存在。

    let x = someExpression

    真正含义是

    let x = someExpression in [an expression involving x]

    然后每次在第二个表达式(主体表达式)见到x,用第一个表达式(someExpression)替换。例如

    let x = 42
    let y = 43
    let z = x + y

    其实是指(使用in关键字)

    let x = 42 in   
      let y = 43 in 
        let z = x + y in
           z    // the result

    有趣的是,有一个类似letlambda

    fun x -> [an expression involving x]

    如果我们把x加入管道,则

    someExpression |> (fun x -> [an expression involving x] )

    是不是非常像let?下面是一个let和一个lambda

    // let
    let x = someExpression in [an expression involving x]
    
    // pipe a value into a lambda
    someExpression |> (fun x -> [an expression involving x] )

    两者都有xsomeExpression,在lambda的主体的任何地方只有见到x就将它替换成someExpression。嗯,在lambda中,xsomeExpression仅仅是位置反过来了,否则基本跟let基本一样了。

    所以,我们也可以写成如下形式

    42 |> (fun x ->
      43 |> (fun y -> 
         x + y |> (fun z -> 
           z)))

    当写成这种形式时,我们已经将let风格转变成CPS了。

    代码说明:

    • 第一行我们获取值42——如何处理?将它传入一个continuation,正如前面我们对isEven函数所做的一样。在此处的continuation上下文中,我们将42重写标为x
    • 第二行我们有值43——如何处理?将它传入一个continuation,在这个上下文中,将它重新标为y
    • 第三行我们把xy加在一起创建一个新值——如何处理这个新值?再来一个continuation,并且再来一个标签z指示这个新值
    • 最后完成并且整个表达式计算结果为z

    包装continuation到一个函数中

    我们想避开显式的管道操作(|>),而是用一个函数来包装这个逻辑。无法称这个函数为let因为let是一个保留字,更重要的是,let的参数位置是反过来的。注意,x在右边而someExpression在左边,所以现在称此函数为“pipeInto”,定义如下

    let pipeInto (someExpression,lambda) =
        someExpression |> lambda

    注意这里参数是一个元组而非两个独立的参数。使用pipeInto函数,我们可以重写上面的代码为

    pipeInto (42, fun x ->
      pipeInto (43, fun y -> 
        pipeInto (x + y, fun z -> 
           z)))

    去掉行首缩进则为

    pipeInto (42, fun x ->
    pipeInto (43, fun y -> 
    pipeInto (x + y, fun z -> 
    z)))

    也许你会认为:几个意思?为啥要包装管道符为一个函数咧?

    答案是这样,我们可以添加额外的代码到pipeInto函数中来处理一些幕后事情,正如computation expression那样。

    回顾“logging”例子

    重新定义pipeInto,增加一个logging功能,如下

    let pipeInto (someExpression,lambda) =
       printfn "expression is %A" someExpression 
       someExpression |> lambda

    如此,本篇一开始的代码则可重写为

    pipeInto (42, fun x ->
    pipeInto (43, fun y -> 
    pipeInto (x + y, fun z -> 
    z
    )))

    这段代码的输出

    expression is 42
    expression is 43
    expression is 85

    这跟早期的实现,输出结果相同。至此,我们已经实现了自己的小小的computation expression 工作流。

    如果我们将这个pipeInto实现与computation expression版本比较,我们可以发现我们自己写的版本是非常接近let!的,除了将参数位置反过来了,还有就是有为了continuation而显式写的->符号。

    computation expression: logging

    回顾“安全除法”例子

    先给出原先的代码

    let divideBy bottom top =
        if bottom = 0
        then None
        else Some(top/bottom)
    
    let divideByWorkflow x y w z = 
        let a = x |> divideBy y 
        match a with
        | None -> None  // give up
        | Some a' ->    // keep going
            let b = a' |> divideBy w
            match b with
            | None -> None  // give up
            | Some b' ->    // keep going
                let c = b' |> divideBy z
                match c with
                | None -> None  // give up
                | Some c' ->    // keep going
                    //return 
                    Some c'

    看看是否可以将额外的代码加入pipeInto函数。我们想要的逻辑是

    如果someExpression参数为None,则不调用continuation lambda

    如果someExpression参数是Some,则调用continuation lambda,传入Some的内容

    逻辑实现如下

    let pipeInto (someExpression,lambda) =
       match someExpression with
       | None -> 
           None
       | Some x -> 
           x |> lambda

    使用这个版本的pipeInto函数,可以重写刚才的原始代码

    let divideByWorkflow x y w z = 
        let a = x |> divideBy y 
        pipeInto (a, fun a' ->
            let b = a' |> divideBy w
            pipeInto (b, fun b' ->
                let c = b' |> divideBy z
                pipeInto (c, fun c' ->
                    Some c' //return 
                    )))

    可以将这段代码再简化一下。首先去除a,b,c,用divideBy表达式代替,即

    let a = x |> divideBy y 
    pipeInto (a, fun a' ->

    变成

    pipeInto (x |> divideBy y, fun a' ->

    将a'重新标记为a,b和c类似处理,去除行首缩进,则代码变成

    let divideByResult x y w z = 
        pipeInto (x |> divideBy y, fun a ->
        pipeInto (a |> divideBy w, fun b ->
        pipeInto (b |> divideBy z, fun c ->
        Some c //return 
        )))

    最后,我们定义一个帮助函数return'用以包装一个值为一个option,全部代码如下

    let divideBy bottom top =
        if bottom = 0
        then None
        else Some(top/bottom)
    
    let pipeInto (someExpression,lambda) =
       match someExpression with
       | None -> 
           None
       | Some x -> 
           x |> lambda 
    
    let return' c = Some c
    
    let divideByWorkflow x y w z = 
        pipeInto (x |> divideBy y, fun a ->
        pipeInto (a |> divideBy w, fun b ->
        pipeInto (b |> divideBy z, fun c ->
        return' c 
        )))
    
    let good = divideByWorkflow 12 3 2 1
    let bad = divideByWorkflow 12 3 0 1

    比较我们自己实现的版本与computation expression版本,发现仅仅语法不同

    computation expression: logging

    总结

    这篇文章中,我们讨论了continuationcontinuation passing style(CPS),以及为什么认为let是一个优秀的语法,因为let在后台进行了continuation处理。

    现在我们可以定义自己的let版本,下一篇我们将把这些付诸实际。

  • 相关阅读:
    15.Numpy之点乘、算术运算、切片、遍历和下标取值
    13.python-列表排序
    [Js-c++]c++中的指针、引用和数组名
    [Hadoop]Windows下用eclipse远程连接hdfs报错Connection denied解决方案
    [Java-JVM]Centos7编译openjdk7
    [Js-Java SE]Java中的Native关键字与JNI
    [Js-C++]C++中赋值表达式的结果
    [Js-C++].h文件与#include详解
    [Js-C++]C++中*&(指针引用)和*(指针)的区别
    [Js-Python]解决pip安装安装源速度慢的问题
  • 原文地址:https://www.cnblogs.com/sjjsxl/p/4980429.html
Copyright © 2011-2022 走看看