本文仅为对原文的翻译,主要是记录以方便以后随时查看。原文地址为http://fsharpforfunandprofit.com/posts/computation-expressions-intro/

背景

是时候揭开计算表达式(Computation expression)的神秘面纱了。现有的解释说明都令人难以理解。比如查阅MSDN官方说明,则对初学者来说虽然简单明确,却对理解没有什么太大帮助。例如当你看到如下代码

{| let! pattern = expr in cexpr |}

它只是如下方法调用的一个简单的语法糖:

builder.Bind(expr, (fun pattern -> {| cexpr |}))

但,这是个什么鬼?

实战

首先看一段简单的代码,然后再用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

运行结果为

expression is
expression is
expression is

看起来很简单,但是每次都显式的调用log语句还是挺烦的,考虑隐藏这些log语句的方法,这时候,Computation expression派上用场。

首先定义一个类型

type LoggingBuilder() =
let log p = printfn "expression is %A" p member this.Bind(x, f) =
log x
f x member this.Return(x) =
x

先不用管这段代码中的BindReturn成员方法,后面会给出解释。接着看如下代码,实现刚才的log功能

let logger = new LoggingBuilder()

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

运行这段代码可以获得跟刚才同样的输出结果,但是很明显,刚才代码中重复的log语句已经被隐藏了。

安全除法

现在让我们看一个经典的例子。

假如要除以一系列的数,即一个接一个将这些数作为除数,但是这些数其中可能有0。如何处理?抛出一个异常会使代码丑陋,使用option类型好像是一个不错的方法。

先定义一个帮助函数,实现除法功能并返回一个int option,正常情况下则为Some,否则为None。然后将这些除法过程链接起来,并且在每个除法过程后判定除法是否成功(返回Some),只有在成功的时候才会继续下一个除法过程。帮助函数如下

let divideBy bottom top =
if bottom =
then None
else Some(top/bottom)

注意第一个参数为除数,故我们可以将除法表达式写成 12 |> divideBy 3 (表示12/3)的形式,这样更容易将整个除法过程串联起来。

看一个具体的实例,用三个数依次去除一个初始数

let divideByWorkflow init x y z =
let a = init |> divideBy x
match a with
| None -> None // give up
| Some a' -> // keep going
let b = a' |> divideBy y
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'

函数调用

let good = divideByWorkflow
let bad = divideByWorkflow

bad变量为None,因为有一个除数为0

注意到以上例子中,返回结果必须为int option,不能返回int。然后,这个例子中的连续测试除法是否失败的代码依然丑陋,考虑使用computation expression

我们定义一个新的类型如下,并实例化

type MaybeBuilder() =

    member this.Bind(x, f) =
match x with
| None -> None
| Some a -> f a member this.Return(x) =
Some x let maybe = new MaybeBuilder()

重写除以一系列数的工作流的函数,隐藏了之前的判断分支逻辑

let divideByWorkflow init x y z =
maybe
{
let! a = init |> divideBy x
let! b = a |> divideBy y
let! c = b |> divideBy z
return c
}

测试以上函数,可得到同样的结果

let good = divideByWorkflow     

let bad = divideByWorkflow    

链接“or else” 测试

上一个例子中,我们在某一步除法执行成功后才继续执行下一个除法,但是,有时候的控制流程并非如此,而是“or else”,即,第一个事情没有成功,则尝试第二个事情,第二个事情如果也失败,则尝试第三个事情,依次类推。

看一个简单例子。假设我们有三个字典,并且我们想查找某一键对应的值,查询每一个字典的结果可能是成功或者失败。

let map1 = [ ("","One"); ("","Two") ] |> Map.ofList
let map2 = [ ("A","Alice"); ("B","Bob") ] |> Map.ofList
let map3 = [ ("CA","California"); ("NY","New York") ] |> Map.ofList let multiLookup key =
match map1.TryFind key with
| Some result1 -> Some result1 // success
| None -> // failure
match map2.TryFind key with
| Some result2 -> Some result2 // success
| None -> // failure
match map3.TryFind key with
| Some result3 -> Some result3 // success
| None -> None // failure

这个查询函数的使用如下

multiLookup "A" |> printfn "Result for A is %A"
multiLookup "CA" |> printfn "Result for CA is %A"
multiLookup "X" |> printfn "Result for X is %A"

代码运行良好,但同样查询函数multiLookup的定义代码太烦,简化一下,首先定义一个bulider类如下

type OrElseBuilder() =
member this.ReturnFrom(x) = x
member this.Combine (a,b) =
match a with
| Some _ -> a // a succeeds -- use it
| None -> b // a fails -- use b instead
member this.Delay(f) = f() let orElse = new OrElseBuilder()

重写查询函数

let map1 = [ ("","One"); ("","Two") ] |> Map.ofList
let map2 = [ ("A","Alice"); ("B","Bob") ] |> Map.ofList
let map3 = [ ("CA","California"); ("NY","New York") ] |> Map.ofList let multiLookup key = orElse {
return! map1.TryFind key
return! map2.TryFind key
return! map3.TryFind key
}

使用示例代码如下,运行后发现结果同期望一样

multiLookup "A" |> printfn "Result for A is %A"
multiLookup "CA" |> printfn "Result for CA is %A"
multiLookup "X" |> printfn "Result for X is %A"

带回调的异步调用

.net中异步操作的标准方法是使用AsyncCallback delegate,这在异步操作完成时被调用。

举个例子,网页下载

open System.Net
let req1 = HttpWebRequest.Create("http://tryfsharp.org")
let req2 = HttpWebRequest.Create("http://google.com")
let req3 = HttpWebRequest.Create("http://bing.com") req1.BeginGetResponse((fun r1 ->     // 请求1异步获取响应,完成后,请求2异步获取响应,完成后,请求3异步获取响应
use resp1 = req1.EndGetResponse(r1)
printfn "Downloaded %O" resp1.ResponseUri req2.BeginGetResponse((fun r2 ->
use resp2 = req2.EndGetResponse(r2)
printfn "Downloaded %O" resp2.ResponseUri req3.BeginGetResponse((fun r3 ->
use resp3 = req3.EndGetResponse(r3)
printfn "Downloaded %O" resp3.ResponseUri ),null) |> ignore
),null) |> ignore
),null) |> ignore

以上代码使用太多的BeginGetResponse和EndGetResponse,以及嵌套的lambda,使得代码阅读费力。

事实上,在需要连接回调函数的代码中,管理这种级联方法总是显得困难,这甚至被称为 "Pyramid of Doom"(尽管 none of the solutions are very elegant, IMO)

当然,在F#中我们将不再写类似的代码,因为F#有内建的async computation expression ,这简化了代码。

open System.Net
let req1 = HttpWebRequest.Create("http://tryfsharp.org")
let req2 = HttpWebRequest.Create("http://google.com")
let req3 = HttpWebRequest.Create("http://bing.com") async {
use! resp1 = req1.AsyncGetResponse()
printfn "Downloaded %O" resp1.ResponseUri use! resp2 = req2.AsyncGetResponse()
printfn "Downloaded %O" resp2.ResponseUri use! resp3 = req3.AsyncGetResponse()
printfn "Downloaded %O" resp3.ResponseUri } |> Async.RunSynchronously

在这个系列的后面部分会看到 async 工作流是如何实现的。

总结

至此我们见到一些简单的computation expression的例子。

  • logging例子中,我们想在每一步中添加一些自定义的逻辑,如打印log信息。
  • 安全除法例子中,我们想更为优雅的处理除法出错的情况,以便我们更加专注其他一些事情。
  • 在多字典查询例子中,我们想在第一次查询字典成功后就结束并返回。
  • 最后在异步操作例子中,我们想隐藏大段的回调函数的代码。

这些例子的一个共同点就是在每个表达式中,computation expression做了一些后台的事情。

打一个不是很好的比方,可以把computation expression想象成SVN或者Git的一个提交后钩子,或者数据库的每次更新后被调用的触发器。这就是computation expression,它可以隐藏一些代码,从而让我们更专注于业务逻辑。

至于computation expressionworkflow(工作流)之间的区别,我使用computation expression表示{...}let!语法,而workflow(工作流)表示具体实现。当然,不是所有computation expression的实现都是工作流,例如,说“async”工作流或者“maybe”工作流是合适的,但是说“seq”工作流就显得不太合适。

也就是说,如下面的代码

maybe
{
let! a = x |> divideBy y
let! b = a |> divideBy w
let! c = b |> divideBy z
return c
}

可以说maybe是我们使用的工作流,而{ let! a = .... return c }computation expression

附:

state type的文章。

Computation expressions: Introduction的更多相关文章

  1. [Regular Expressions] Introduction

    var str = "Is this This?"; //var regex = new RegExp("is", "gi"); var r ...

  2. Computation expressions and wrapper types

    原文地址:http://fsharpforfunandprofit.com/posts/computation-expressions-wrapper-types/ 在上一篇中,我们介绍了“maybe ...

  3. The week in .NET - 1/12/2015

    On.NET Last week, we had Mads Torgersen on the show to talk about language design in general, and C# ...

  4. 我们为什么要使用NodeJS

    科普文一则,说说我对NodeJS(一种服务端JavaScript实现)的一些认识,以及我为什么会向后端工程师推荐NodeJS. "Node.js 是服务器端的 JavaScript 运行环境 ...

  5. Implementing a builder: Zero and Yield

    原文地址:http://fsharpforfunandprofit.com/posts/computation-expressions-builder-part1/ 前面介绍了bind和continu ...

  6. Introducing 'bind'

    原文地址:http://fsharpforfunandprofit.com/posts/computation-expressions-bind/ 上一篇讨论了如何理解let作为一个能实现contin ...

  7. Understanding continuations

    原文地址http://fsharpforfunandprofit.com/posts/computation-expressions-continuations/ 上一篇中我们看到复杂代码是如何通过使 ...

  8. 函数式编程之-bind函数

    Bind函数 Bind函数在函数式编程中是如此重要,以至于函数式编程语言会为bind函数设计语法糖.另一个角度Bind函数非常难以理解,几乎很少有人能通过简单的描述说明白bind函数的由来及原理. 这 ...

  9. 了解 node.js

    原文为: 我们为什么要使用NodeJS 写的好,就收藏于此,供学习之用. 科普文一则,说说我对NodeJS(一种服务端JavaScript实现)的一些认识,以及我为什么会向后端工程师推荐NodeJS. ...

随机推荐

  1. Qt编译

    版本及安装环境 项目 版本 位 Windows 7 x64 Visual Studio 2010 x64 qt 4.8.6 x64 下载源码 进入下载列表,下载qt-everywhere-openso ...

  2. [Jenkins]怎么删除jenkins里项目配置的svn记录

    问题出现原因: 当持续集成项目运行一段时间之后,你会发现不知不觉中,配置了不少过往的SVN账号,都已经不能用了,失效了,影响选择有时候,想要删除 问题解决办法: 1.选择$JENKINS_HOME/c ...

  3. 冰刃IceSword中文版 V1.22 绿色汉化修正版

    软件名称: 冰刃IceSword中文版 V1.22 绿色汉化修正版 软件语言: 简体中文 授权方式: 免费软件 运行环境: Win 32位/64位 软件大小: 2.1MB 图片预览: 软件简介: Ic ...

  4. CentOS7 安装 OpenSSL 1.0.1m 和 OpenSSH 6.8p1

    # 下载软件 wget http://zlib.net/zlib-1.2.8.tar.gz wget ftp://ftp.openssl.org/source/openssl-1.0.1m.tar.g ...

  5. sql优化--in和exists效率

    系统要求进行SQL优化,对效率比较低的SQL进行优化,使其运行效率更高,其中要求对SQL中的部分in/not in修改为exists/not exists 修改方法如下: in的SQL语句 SELEC ...

  6. windows下寻找端口

    netstat -aon|findstr "5037" 找到占用5037的进程号: 根据进程号杀死进程 taskkill /pid 5136 /f tasklist|findstr ...

  7. ios 关于tableview小技巧

    第一个:cell中的分割线不顶头 首先在viewDidLoad方法加入以下代码: if ([self.tableView respondsToSelector:@selector(setSeparat ...

  8. 在Java中system.out.println使用方法

    先输入sysout,然后输入辅助快捷键:Alt+/ 常用快捷键: 1. ctrl+shift+r:打开资源 这可能是所有快捷键组合中最省时间的了.这组快捷键可以打开工作区中任何一个文件,只需要按下文件 ...

  9. 连续多个git提交发生了冲突时

    git checkout -b test 创建并切换到分支test git clone git branch master git merge test 合并test到master (git merg ...

  10. date格式化

    Linux: [ghsea@localhost ~]date +%Y:%m:%d [ghsea@localhost ~]date +%Y-%m%d [ghsea@localhost ~]date +% ...