原文地址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. node-canvas

    1.使用之前需要预先安装  Cairo 本人安装遇到各种各样的坑,可以参考这里来填坑:https://github.com/Automattic/node-canvas/wiki/Installati ...

  2. 一个App从创意到最终上架到App Store里的整个过程是怎样的?

    一个App从创意到最终上架到App Store里的整个过程是怎样的? 制作App需要什么软件?应该看什么书?需要哪些设备?推到App Store里,需要注册什么网站?是否需要付费?需要什么证书之类的? ...

  3. jQuery操作radio

    JQuery获取选中的radio $radio = $('input:radio[name="sex"][class="xxxx"]:checked') 获取n ...

  4. sphinx cmd command

    D:\iso\gaoqiao\app\sphinx\bin\indexer.exe -c D:\iso\gaoqiao\app\sphinx\bin\sphinx.conf --all --rotat ...

  5. 爬虫代码实现五:解析所有分页url并优化解析实现类

    如图,我们进入优酷首页,可以看到电视剧列表,我们称这个页面为电视剧列表页,而点击进入某个电视剧,则称为电视剧详情页.那么如何获取所有分页以及对应的详情页呢,通过下面的分页得到. 因此,首先,我们将St ...

  6. 【NOIP2006提高组】能量项链

    说好的好好写人话的题解 嗯很多题解都说过这是一个石子合并的模型它也确实就是一个石子合并的模型.然而就算这样我也不会写最后仍然写了个记忆化搜索 首先我们不论环状,就直接一条链型,当只剩下两个珠子的时候, ...

  7. ACM网络镜像赛

    参加了东大,南理的网络镜像赛,分别答对了两道题和三道题.感觉自己完全被虐了.发现了很多自己应该去掌握的知识,找到了自己的差距.容斥原理,博弈论,等等,这将是我努力的方向. 在跟大神同学和学姐交流的过程 ...

  8. Chronodex:视觉时间管理,让你的生活更有序

    我喜欢把时间安排的有条不紊,看看清晰的时间安排心理有种踏实感,只有你是"纸爱好者" - 才能最终寻找完美组织时间的方式方法. 我记得自从我是一个小女孩以来,我喜欢纸和笔和颜色和标记 ...

  9. 用sql实现汉字转拼音

    有时我们会需要将汉字转为拼音,例如需要将省市转为拼音后当做编码存储(尽管国家有统一的标识码,但有时候我们还是会用到),网络上也有工具提供汉字转拼音的功能,但各有优劣,一般转拼音后还会存在带声调的字母, ...

  10. iPhone doesn’t support any of GongShangJ.app’s architectures. You can add iPhone’s armv7s architectu

    iPhone doesn't support any of GongShangJ.app's architectures. You can add iPhone's armv7s architectu