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

前面介绍了bind和continuation,以及包装类型的使用,现在我们已经准备讲述“builder”类型的一系列的方法了。

如果你看过MSDN documentation,你会发现“builder”类型的方法不仅仅是Bind和Return,还有其他方法如Delay和Zero,这两个方法是干啥用的?这正是本篇以及以后几篇讲述的主题。

计划

为了论证如何创建一个builder类,我们将创建一个自定义工作流,它使用所有可能的builder类方法。

但是比起从头开始,没有上下文就解释那些builder类方法是什么意思,我们不如采用自底向上的方式,从一个简单的工作流开始,并在需要解决某个问题或者错误的时候添加有关的方法,这样会更加自然些。在这个过程中,你将逐渐理解F#在细节上如何处理computation expression的。

对这个过程做一个提纲

  1. 首先看一看,作为一个基本工作流,哪些方法是必需的。我们将介绍Zero,Yield,Combine和For
  2. 然后,我们将讨论如何延迟执行代码,即,只在需要的时候计算值。我们介绍Delay和Run,然后看延迟计算的具体实现。
  3. 最后,我们讨论剩下的方法:While,Using和异常处理。

开始前

在我们开始创建工作流之前,有几点说明

computation expressions文档说明

也许你已经注意到,MSDN文档中关于computation expressions的说明是非常简陋的,尽管它并非不准确,但是仍然容易会误导初学者。例如,builder类方法的签名比看上去更加灵活,这有助于实现一些特性(features),你在工作中如果不随时查阅文档说明,那这些特性则很不明显。后面我们会给出一个例子说明。

如果你需要更详细的文档说明,那么我将推荐两个源。如果想更为详细地纵览computation expressions背后的概念,有一个很好的资源,是由Tomas Petricek和Don Syme写的论文"The F# Expression Zoo"。如果需要最新的准确的科技文档,可以阅读F# language specification,它有一章节是关于computation expressions的。

包装类型和未包装类型

当想去理解文档中的函数签名,记住一点,我们通常把未包装类型记为'T,包装类型记为M<'T>。这样以后当看到Return方法的签名为'T -> M<'T>,它表示Return方法有一个未包装类型的输入参数,输出参数是要给包装类型。

跟这个系列的前面几篇文章相同,我将继续使用“未包装”和“包装”来描述这些类型之间的关系,但是我们接下去的讨论中可能会稍作改变,使用其他术语,如“computation type”代替“wrapped type”。我希望当碰到这个称呼改变时,我们对这个作改变的理由是清楚的并能理解。

当然,在我的例子中,我将尽量让事情简单,比如使用

let! x = ...wrapped type value...

但是,这其实是一个过分简化的表达式,也可以准确的说,“x”可以是任意模式而不仅是一个值,“wrapped type value”可以是一个表达式,这个表达式能计算得到一个包装类型。MSDN文档使用这种更加精确的方法,它在定义中使用“模式”和“表达式”,如

let! pattern = expr in cexpr

以下是maybe工作流中使用模式和表达式的一些例子,其中,Option是包装类型,

// let! pattern = expr in cexpr
maybe {
let! x,y = Some(,)
let! head::tail = Some( [;;] )
// etc
}

之前说过,我将继续使用这种非常简单的例子,不会对已经很复杂的主题再添加更多复杂的东西。

实现builder类(或者不是builder类)的特殊方法

MSDN文档说明了,每个特殊操作(如for..in,或yield)是如何被转换成builder类中的一个或多个方法调用的。

虽然没有一对一的对应关系(特殊操作与builder类方法),但是通常地,为了支持特殊操作的语法,你必须实现builder类中的一个对应的方法,否则编译器无法通过编译。

另一方面,你不必实现每个方法,如果不需要支持某个语法的话。例如,通过实现Bind和Return两个方法,我们就已经实现了maybe工作流。如果我们不需要使用Delay,Use等等,我们就不必实现这些方法。

为了能够看到如果不实现必需的方法,将会发生什么,让我们尝试在maybe工作流中石油for..in..do语法,如下

maybe { for i in [;;] do i }

我们将会获得一个编译错误

This control construct may only be used if the computation expression builder defines a 'For' method

有时候会获得隐晦的错误,除非你知道后台进行了什么操作。例如,如果忘记在工作流中加Return操作,如下

maybe {  }

那你会获得编辑错误

This control construct may only be used if the computation expression builder defines a 'Zero' method

你可能会问:Zero方法是什么?为什么需要这个方法?答案下面会给出。

带"!"和不带"!"的操作

显然,很多特殊操作是成对出现的,即带"!"和不带"!"符号。例如,let 和 let! (发音为 let-bang),return 和 return!,yield和yield!等等。

区别其实很简单。当实现不带"!"的操作时,右边总是非包装类型,而带"!"的操作右边总是包装类型。

例如,使用maybe工作流,其中Option是包装类型,我们可以比较下面代码的语法的不同

let x =            // 1 is an "unwrapped" type
let! x = (Some ) // Some 1 is a "wrapped" type
return // 1 is an "unwrapped" type
return! (Some ) // Some 1 is a "wrapped" type
yield // 1 is an "unwrapped" type
yield! (Some ) // Some 1 is a "wrapped" type

带"!"的对工作流组合非常重要,因为包装类型可以是另一个相同类型的computation expression的结果

let! x = maybe {...)       // "maybe" returns a "wrapped" type

// bind another workflow of the same type using let!
let! aMaybe = maybe {...) // create a "wrapped" type
return aMaybe // return it // bind two child asyncs inside a parent async using let!
let processUri uri = async {
let! html = webClient.AsyncDownloadString(uri)
let! links = extractLinks html
... etc ...
}

以上代码中,webClinet.AsyncDownloadString(uri) 与 extractLinks html 结果为一个包装类型,故可以放在let!操作的右边。

深入——创建一个工作流的最小实现

让我们开始吧!我们开始创建一个maybe工作流的最小版本(我们将重命名它为“trace”),实现上面说过的每一个方法,可以让我们知道有了这些实现会发生什么。以后,我们就使用这个工作流作为我们的试验台。

给出trace工作流的第一个版本

type TraceBuilder() =
member this.Bind(m, f) =
match m with
| None ->
printfn "Binding with None. Exiting."
| Some a ->
printfn "Binding with Some(%A). Continuing" a
Option.bind f m member this.Return(x) =
printfn "Returning a unwrapped %A as an option" x
Some x member this.ReturnFrom(m) =
printfn "Returning an option (%A) directly" m
m // make an instance of the workflow
let trace = new TraceBuilder()

这里的代码没有什么新的内容,都是之前见过的。

现在让我们运行一些示例代码

trace {
return
} |> printfn "Result 1: %A" trace {
return! Some
} |> printfn "Result 2: %A" trace {
let! x = Some
let! y = Some
return x + y
} |> printfn "Result 3: %A" trace {
let! x = None
let! y = Some
return x + y
} |> printfn "Result 4: %A"

一切都应该如期工作,尤其,你应该能够看到第四个例子中使用了None,这导致接下来的两行(let! y = ... return x+y)代码被跳过执行,整个表达式的结果是None。

介绍"do!"

我们的表达式支持let!,但是do!是什么?

在正常F#中,do就像let,除了do表达式不返回值(即,返回unit值)。

在computation expression中,do!也非常类似let!,传入一个包装类型到Bind方法中,但是do!的结果值是unit,所以传给Bind方法的是一个unit的包装类型。

以下是使用trace工作流的简单例子

trace {
do! Some (printfn "...expression that returns unit")
do! Some (printfn "...another expression that returns unit")
let! x = Some ()
return x
} |> printfn "Result from do: %A"

输出结果为

...expression that returns unit
Binding with Some(<null>). Continuing
...another expression that returns unit
Binding with Some(<null>). Continuing
Binding with Some(). Continuing
Returning a unwrapped as an option
Result from do: Some

介绍"Zero"

最小的工作流会是什么样子?如果什么都不做,如下代码

trace {
} |> printfn "Result for empty: %A"

我们会获得一个错误

This value is not a function and cannot be applied

想一下,在工作流中没有任何东西,这也没什么意义,毕竟本来的目的就是链接表达式的。

接着,如果在工作流中添加一个简单的不带let!或者return的表达式呢?

trace {
printfn "hello world"
} |> printfn "Result for simple expression: %A"

于是,将会获得如下错误

This control construct may only be used if the computation expression builder defines a 'Zero' method

所以你知道了什么需要Zero方法了吧?答案就是在这种情况下,没有显式返回任何东西,而computation expression整体必须返回一个包装值。那应该返回什么呢?

事实上,只要没有显式给出computation expression的返回值,就会发生上面这种情况,比如你写if..then表达式但少了else从句,就会发生同样的状况。

trace {
if false then return
} |> printfn "Result for if without else: %A"

在正常的F#代码中,“if..then"语句不带“else”的会产生一个unit类型的结果,但是在computation expression中,返回值必须是一个包装类型,而编译器不知道这个包装值是什么。

办法就是告诉编译器该使用什么——这就是Zero方法的目的。

Zero应该使用什么值?

那Zero应该使用什么值呢?这依赖于你创建的是什么工作流。

以下几点说明也许会有帮助:

  • 工作流是否有“成功”或者“失败”的概念?如果是,则为Zero使用“失败”的结果值。例如,在我们的trace工作流中,我们使用None表示失败,所以我们可以使用None作为Zero的值
  • 工作流是否是顺序执行?也就是,在工作流中,一步一步的执行,包括一些在后台的处理。在一般的F#代码中,有显式返回的表达式可能会计算得到unit,所以这里类似处理,即Zero是unit的包装版本。例如,在一个option-based 工作流中,我们可能使用Some()来表示Zero(顺便一提,这跟Return()是相同的)。
  • 工作流是否主要关心数据结构的操作?如果是,Zero应该是“空”数据结构。例如,在一个“list builder”工作流中,我们将使用空列表作为Zero值。
  • 在组合包装类型时Zero值也扮演着重要的作用。

一个Zero的实现

让我们扩展一下我们的测试代码,使用一个返回None的Zero方法,如下

type TraceBuilder() =
member this.Zero () =
printfn "Zero"
None
let trace = new TraceBuilder() trace {
printfn "hello world"
} |> printfn "Result for simple expression: %A" trace {
if false then return
| |> printfn "Return for if without else clause: %A"

通过以上测试代码我们很清楚地看到,Zero在后台被默默的调用。整个表达式的返回值为None。注意:None可能被打印成<null>。

是否总是需要Zero?

记住,不要求必须有Zero,除非它在工作流的上下文中有意义。例如,seq不允许有Zero,而async则可以

let s = seq {printfn "zero" }    // Error
let a = async {printfn "zero" } // OK

介绍“Yield”

C#中有“yield”语句,其作用是提前返回一个可枚举类型,以后通过迭代器在需要用到可枚举类型的元素时才获取元素值。

F# computation expressions中也有一个“yield”语句,它是干嘛的呢?

让我们尝试一下

trace {
yield
} |> printfn "Result for yield: %A"

运行这段代码会获得一个错误

This control construct may only be used if the computation expression builder defines a 'Yield' method

这里不奇怪。那“yield”方法的实现是什么样的呢?MSDN文档给出了它的签名'T -> M<'T>,跟Return方法签名相同,即包装一个非包装类型。

所以,我们类似Return方法来实现Yield方法,并测试这个表达式

type TraceBuilder() =
// other members as before member this.Yield(x) =
printfn "Yield an unwrapped %A as an option" x
Some x // make a new instance
let trace = new TraceBuilder() // test
trace {
yield
} |> printfn "Result for yield: %A"

此时,这个工作流可以工作,看起来yield可以是return的替代品。

当然还有一个YieldFrom方法,它对应ReturnFrom方法。YieldFrom的行为类似,产生一个包装类型

那下面将YieldFrom加入builder类方法

type TraceBuilder() =
// other members as before member this.YieldFrom(m) =
printfn "Yield an option (%A) directly" m
m // make a new instance
let trace = new TraceBuilder() // test
trace {
yield! Some
} |> printfn "Result for yield!: %A"

此时,可能会想:如果return和yield几乎相同,那为何要存在这两个关键字呢?答案是,实现其中一个方法可以让我们使用某个语法,而实现另一个则不能使用这个语法。例如,seq表达式允许yield但是不允许return,而async允许return但不允许yield,如下面代码片段

let s = seq {yield }    // OK
let s = seq {return } // error let a = async {return } // OK
let a = async {yield } // error

事实上,你可以实现让return与yield方法之间存在一些差别,那样就能达到某种目的,比如使用return停止(return)后面的computation expression的计算,而yield则不会。

更一般地,yield应该用在sequence/enumeration的场景,而return通常一次作用在一个表达式。(我们将在下一篇中看到如何多次使用yield。)

复习“For”

我们在上一篇中讨论了for..in..do语法,现在再来回顾一下“list builder”并向其中添加一些其他的方法。我们已经知道如何对一个列表定义Bind和Return方法了,故这里只要实现其他的方法定义。

Zero方法只返回一个空列表。

Yield方法实现与Return相同。

For方法实现与Bind相同。

type ListBuilder() =
member this.Bind(m, f) =
m |> List.collect f member this.Zero() =
printfn "Zero"
[] member this.Return(x) =
printfn "Return an unwrapped %A as a list" x
[x] member this.Yield(x) =
printfn "Yield an unwrapped %A as a list" x
[x] member this.For(m,f) =
printfn "For %A" m
this.Bind(m,f) // make an instance of the workflow
let listbuilder = new ListBuilder()

下面是使用let!的代码

listbuilder {
let! x = [..]
let! y = [;;]
return x + y
} |> printfn "Result: %A"

下面是使用for的代码

listbuilder {
for x in [..] do
for y in [;;] do
return x + y
} |> printfn "Result: %A"

可以看到这两段代码的输出结果相同

总结

本篇我们看到了对一个简单的computation expression,如何实现它的基本的方法。

重申几点:

对简单的表达式来说,不必实现所有的方法。

  • 带“!”的语句,表达式右边是包装类型
  • 不带“!”的表达式右边是非包装类型
  • 如果想让一个工作流不显式返回值,则必须实现Zero
  • Yield与Return基本相同,但是Yield应该在sequence/enumeration语义中使用
  • 在一些简单的场景中For与Bind基本相同

下一篇,我们将看到当组合多个值时会发生什么。

Implementing a builder: Zero and Yield的更多相关文章

  1. Implementing a builder: Combine

    原文地址:点击这里 本篇我们继续讨论从一个使用Combine方法的computation expression中返回多值. 前面的故事 到现在为止,我们的表达式建造(builder)类如下 type ...

  2. Unity3D &amp; C# 设计模式--23

     Unity3D & C#Design Patterns 23 design patterns. Creational Patterns 1. Abstract Factory抽象工厂 创 ...

  3. 转载yield关键字理解

    实现IEnumerable接口及理解yield关键字   [摘要]本文介绍实现IEnumerable接口及理解yield关键字,并讨论IEnumerable接口如何使得foreach语句可以使用. 本 ...

  4. Python关键字yield详解以及Iterable 和Iterator区别

    迭代器(Iterator) 为了理解yield是什么,首先要明白生成器(generator)是什么,在讲生成器之前先说说迭代器(iterator),当创建一个列表(list)时,你可以逐个的读取每一项 ...

  5. Introducing Makisu: Uber’s Fast, Reliable Docker Image Builder for Apache Mesos and Kubernetes

    转自:https://eng.uber.com/makisu/?amp To ensure the stable, scalable growth of our diverse tech stack, ...

  6. [转]Implementing User Authentication in ASP.NET MVC 6

    本文转自:http://www.dotnetcurry.com/aspnet-mvc/1229/user-authentication-aspnet-mvc-6-identity In this ar ...

  7. builder pattern

    design patterns 结合书本和这个网站的这个系列的文章来看: https://www.tutorialspoint.com/design_pattern/builder_pattern.h ...

  8. [原译]实现IEnumerable接口&理解yield关键字

    原文:[原译]实现IEnumerable接口&理解yield关键字 著作权声明:本文由http://leaver.me 翻译,欢迎转载分享.请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢 ...

  9. What is the yield keyword used for in C#?

    What is the yield keyword used for in C#? https://stackoverflow.com/a/39496/3782855 The yield keywor ...

随机推荐

  1. xtrabackup在线备份及还原

    xtrabackup下载https://www.percona.com/downloads/XtraBackup/LATEST/xtrabackup文档https://www.percona.com/ ...

  2. Windows离开模式(AwayMode)

    Windows Registry Editor Version 5.00[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Man ...

  3. 网页视频下载牛逼工具,支持各种格式转换,比如腾讯视频格式qlv转mp4

    这种思路真是创新,原文地址:http://jingyan.baidu.com/article/5225f26b03f047e6fb090860.html 软件工具名字:维棠下载. 上图: 1:搜索视频 ...

  4. hadoop(二)

    三 Hive和Hbase #安装配置Hbase环境#主要参考https://my.oschina.net/zc741520/blog/388718网站配置的是集群,这里是伪分布,将网站中涉及多个主机的 ...

  5. SQL总结之创建实例表空间监听

    [创建数据库实例]cmd------>dbca[创建表空间-sql创建]create tablespace NSTC_WS logging datafile 'D:\app\dell\orada ...

  6. CentOS7安装Zabbix

    一.Zabbix简介 Zabbix是一个基于WEB界面的提供分布式系统监视以及网络监视功能的企业级的开源解决方案. Zabbix能监视各种网络参数,保证服务器系统的安全运营:并提供灵活的通知机制以让系 ...

  7. 一个简单的例子说明windows环境变量配置

    关于win下环境变量的问题 配置环境变量其实就像是创建一个快捷键一样,我们把安装程序的路径告诉系统环境变量,这样下次我们在命令行中就可以直接使用一个简单的命令来调用我们安装的程序,因为此时计算机已经知 ...

  8. 使用Jax-rs 开发RESTfull API 入门

    使用Jax-rs 开发RESTfull API 入门 本文使用 Jersey 2开发RESTfull API.Jersey 2 是 JAX-RS 接口的参考实现 使用到的工具 Eclipse Neon ...

  9. Python Data Visualization Cookbook 2.9.2

    import numpy as np import matplotlib.pyplot as plt def is_outlier(points, threshold=3.5): if len(poi ...

  10. Kostya the Sculptor

    Kostya the Sculptor 题目链接:http://codeforces.com/problemset/problem/733/D 贪心 以次小边为第一关键字,最大边为第二关键字,最小边为 ...