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 =
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 |> ifEven和aNumber |> 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
有趣的是,有一个类似let的lambda
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] )
两者都有x和someExpression,在lambda的主体的任何地方只有见到x就将它替换成someExpression。嗯,在lambda中,x和someExpression仅仅是位置反过来了,否则基本跟let基本一样了。
所以,我们也可以写成如下形式
|> (fun x ->
|> (fun y ->
x + y |> (fun z ->
z)))
当写成这种形式时,我们已经将let风格转变成CPS了。
代码说明:
- 第一行我们获取值42——如何处理?将它传入一个continuation,正如前面我们对isEven函数所做的一样。在此处的continuation上下文中,我们将42重写标为x
- 第二行我们有值43——如何处理?将它传入一个continuation,在这个上下文中,将它重新标为y
- 第三行我们把x和y加在一起创建一个新值——如何处理这个新值?再来一个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版本,发现仅仅语法不同
总结
这篇文章中,我们讨论了continuation和continuation passing style(CPS),以及为什么认为let是一个优秀的语法,因为let在后台进行了continuation处理。
现在我们可以定义自己的let版本,下一篇我们将把这些付诸实际。
Understanding continuations的更多相关文章
- 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 ...
- Understanding delete
简述 我们都知道无法通过delete关键字针对变量和函数进行操作,而对于显示的对象属性声明却可以进行,这个原因需要深究到js的实现层上去,让我们跟随 Understanding delete 来探究一 ...
- Life Cycle of Thread – Understanding Thread States in Java
Life Cycle of Thread – Understanding Thread States in Java 深入理解java线程生命周期. Understanding Life Cycle ...
- [转]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 ...
- Understanding the Internal Message Buffers of Storm
Understanding the Internal Message Buffers of Storm Jun 21st, 2013 Table of Contents Internal messag ...
- Understanding theory (1)
Source: verysmartbrothas.com It has been confusing since my first day as a PhD student about theory ...
- Understanding Convolutions
http://colah.github.io/posts/2014-07-Understanding-Convolutions/ Posted on July 13, 2014 neural netw ...
- Understanding, Operating and Monitoring Apache Kafka
Apache Kafka is an attractive service because it's conceptually simple and powerful. It's easy to un ...
- [翻译]Understanding Weak References(理解弱引用)
原文 Understanding Weak References Posted by enicholas on May 4, 2006 at 5:06 PM PDT 译文 我面试的这几个人怎么这么渣啊 ...
随机推荐
- 安卓自动化测试工具一:Monkey
一:monkey的用途:主要用于稳定性测试,模拟用户操作 二.monkey的基本使用 monkey文档地址:"<android_sdk>/docs/tools/help/monk ...
- # 关于string
关于string 头文件 #include <string> using std::string; string定义和初始化 string s1; string s2(s1); strin ...
- bootstrap 混合标签
<html lang="zh_cn"> <head> <meta charset="utf-8"> <meta htt ...
- Logstash安装搭建(一)
Logstash是一个具有实时管道的开源数据收集引擎.可以动态地统一不同来源的数据,并将数据归到不同目的地.也是一个管理事件和日志工具.你可以用它来收集日志,分析它们,并将它们储存起来以供以后使用. ...
- sublime 2中Package control安装和使用
安装: 安装时,如果想查看安装进度,可打开console(View->Show Console) 安装Package control有两中方法: 方法1:通过代码安装 import urllib ...
- C# asp.net PhoneGap html5
很久没写博客,今天自己写一篇吧.来谈一谈c# PhoneGap,html5 与asp.net.能搜到这篇博客就说明你是一位.net开发者,即将或者正在从事移动开发. 大家可能都有疑,我是一名.net开 ...
- redis单主机多实例
假设我们服务器上面已经安装好了redis: 可参看:http://zlyang.blog.51cto.com/1196234/1834700 下面我们来配置redis单主机多实例: 我们首先拷贝两份文 ...
- 【Java】ArrayList 的 toArray() 方法抛出 ClassCastException 异常
第一次用这个方法,结果冒出个莫名其妙的异常来: String[] names = (String[]) mTags.toArray(); 结果会抛出 java.lang.ClassCastExcept ...
- 拒绝深坑!记录找了多半天时间的C++编译失败的错误
采用新的源码,和原来的服务改动也不是很大,但是拒绝深坑啊,找了半天以为是源码的问题,结果倒好原来是环境的问题,还是要感谢一个神一样的人物的帮助 编译的时候一直出现undefined reference ...
- tomcat改端口的一些问题
cmd运行netstat -anp查看端口使用情况,找到被占用端口的PID