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

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

使用computation expression前的代码

let log p = printfn "expression is %A" p

let loggedWorkflow =
let x =
log x
let y =
log y
let z = x + y
log z
//return
z

使用computation expression后的代码

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

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

Continuations

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

典型的C#代码

public int Divide(int top, int bottom)
{
if (bottom==)
{
throw new InvalidOperationException("div by 0");
}
else
{
return top/bottom;
}
} public bool IsEven(int aNumber)
{
var isEven = (aNumber % == );
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==)
{
return ifZero();
}
else
{
return ifSuccess( top/bottom );
}
} public T IsEven<T>(int aNumber, Func<int,T> ifOdd, Func<int,T> ifEven)
{
if (aNumber % == )
{
return ifEven(aNumber);
}
else
{ return ifOdd(aNumber);
}
}

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

使用continuations前的代码

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

使用continuations后的代码

let divide ifZero ifSuccess top bottom =
if (bottom=)
then ifZero()
else ifSuccess (top/bottom) let isEven ifOdd ifEven aNumber =
if (aNumber % = )
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
let bad1 = divide1 // 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
let bad2 = divide2 // 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
let bad3 = divide3

现在,调用者不需要到处对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
let bad1 = isEven1 // 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
let bad2 = isEven2 // 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
let bad3 = isEven3

此时,调用者不用到处对返回的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 =
let y =
let z = x + y

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

let x =  in
let y = 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基本一样了。

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

 |> (fun x ->
|> (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 (, fun x ->
pipeInto (, fun y ->
pipeInto (x + y, fun z ->
z)))

去掉行首缩进则为

pipeInto (, fun x ->
pipeInto (, 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 (, fun x ->
pipeInto (, fun y ->
pipeInto (x + y, fun z ->
z
)))

这段代码的输出

expression is
expression is
expression is

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

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

回顾“安全除法”例子

先给出原先的代码

let divideBy bottom top =
if bottom =
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 =
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
let bad = divideByWorkflow

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

总结

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

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

Understanding continuations的更多相关文章

  1. GOOD MEETINGS CREATE SHARED UNDERSTANDING, NOT BRDS!

      Deliverables and artifacts were a focal point of BA work during the early part of my career. If I ...

  2. Understanding delete

    简述 我们都知道无法通过delete关键字针对变量和函数进行操作,而对于显示的对象属性声明却可以进行,这个原因需要深究到js的实现层上去,让我们跟随 Understanding delete 来探究一 ...

  3. Life Cycle of Thread – Understanding Thread States in Java

    Life Cycle of Thread – Understanding Thread States in Java 深入理解java线程生命周期. Understanding Life Cycle ...

  4. [转]Part 3: Understanding !PTE - Non-PAE and X64

    http://blogs.msdn.com/b/ntdebugging/archive/2010/06/22/part-3-understanding-pte-non-pae-and-x64.aspx ...

  5. Understanding the Internal Message Buffers of Storm

    Understanding the Internal Message Buffers of Storm Jun 21st, 2013 Table of Contents Internal messag ...

  6. Understanding theory (1)

    Source: verysmartbrothas.com It has been confusing since my first day as a PhD student about theory ...

  7. Understanding Convolutions

    http://colah.github.io/posts/2014-07-Understanding-Convolutions/ Posted on July 13, 2014 neural netw ...

  8. Understanding, Operating and Monitoring Apache Kafka

    Apache Kafka is an attractive service because it's conceptually simple and powerful. It's easy to un ...

  9. [翻译]Understanding Weak References(理解弱引用)

    原文 Understanding Weak References Posted by enicholas on May 4, 2006 at 5:06 PM PDT 译文 我面试的这几个人怎么这么渣啊 ...

随机推荐

  1. 安卓自动化测试工具一:Monkey

    一:monkey的用途:主要用于稳定性测试,模拟用户操作 二.monkey的基本使用 monkey文档地址:"<android_sdk>/docs/tools/help/monk ...

  2. # 关于string

    关于string 头文件 #include <string> using std::string; string定义和初始化 string s1; string s2(s1); strin ...

  3. bootstrap 混合标签

    <html lang="zh_cn"> <head> <meta charset="utf-8"> <meta htt ...

  4. Logstash安装搭建(一)

    Logstash是一个具有实时管道的开源数据收集引擎.可以动态地统一不同来源的数据,并将数据归到不同目的地.也是一个管理事件和日志工具.你可以用它来收集日志,分析它们,并将它们储存起来以供以后使用. ...

  5. sublime 2中Package control安装和使用

    安装: 安装时,如果想查看安装进度,可打开console(View->Show Console) 安装Package control有两中方法: 方法1:通过代码安装 import urllib ...

  6. C# asp.net PhoneGap html5

    很久没写博客,今天自己写一篇吧.来谈一谈c# PhoneGap,html5 与asp.net.能搜到这篇博客就说明你是一位.net开发者,即将或者正在从事移动开发. 大家可能都有疑,我是一名.net开 ...

  7. redis单主机多实例

    假设我们服务器上面已经安装好了redis: 可参看:http://zlyang.blog.51cto.com/1196234/1834700 下面我们来配置redis单主机多实例: 我们首先拷贝两份文 ...

  8. 【Java】ArrayList 的 toArray() 方法抛出 ClassCastException 异常

    第一次用这个方法,结果冒出个莫名其妙的异常来: String[] names = (String[]) mTags.toArray(); 结果会抛出 java.lang.ClassCastExcept ...

  9. 拒绝深坑!记录找了多半天时间的C++编译失败的错误

    采用新的源码,和原来的服务改动也不是很大,但是拒绝深坑啊,找了半天以为是源码的问题,结果倒好原来是环境的问题,还是要感谢一个神一样的人物的帮助 编译的时候一直出现undefined reference ...

  10. tomcat改端口的一些问题

    cmd运行netstat -anp查看端口使用情况,找到被占用端口的PID