Implementing a builder: Combine
原文地址:点击这里
本篇我们继续讨论从一个使用Combine方法的computation expression中返回多值。
前面的故事
到现在为止,我们的表达式建造(builder)类如下
- 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
- member this.Zero() =
- printfn "Zero"
- None
- member this.Yield(x) =
- printfn "Yield an unwrapped %A as an option" x
- Some x
- member this.YieldFrom(m) =
- printfn "Yield an option (%A) directly" m
- m
- // make an instance of the workflow
- let trace = new TraceBuilder()
这个类到现在工作正常。但是,我们即将看到一个问题
两个‘yield’带来的问题
之前,我们看到yield可以像return一样返回值。
通常来说,yield不会只使用一次,而是使用多次,以便在一个过程中的不同阶段返回多个值,如枚举(enumeration)。如下代码所示
- trace {
- yield
- yield
- } |> printfn "Result for yield then yield: %A"
但是运行这段代码,我们获得一个错误
- This control construct may only be used if the computation expression builder defines a 'Combine' method.
并且如果你使用return来代替yield,你会获得同样的错误
- trace {
- return
- return
- } |> printfn "Result for return then return: %A"
在其他上下文中,也同样会有这个错误,比如我们想在做某事后返回一个值,如下代码
- trace {
- if true then printfn "hello"
- return
- } |> printfn "Result for if then return: %A"
我们会获得同样的错误。
理解这个问题
那这里该怎么办呢?
为了帮助理解,我们回到computation expression的后台视角,我们能看到return和yield是一系列计算的最后一步,就比如
- Bind(,fun x ->
- Bind(,fun y ->
- Bind(x + y,fun z ->
- Return(z) // or Yield
可以将return(或yield)看成是对行首缩进的复位,那样当我们再次return/yield时,我们可以这么写代码
- Bind(,fun x ->
- Bind(,fun y ->
- Bind(x + y,fun z ->
- Yield(z)
- // start a new expression
- Bind(,fun w ->
- Bind(,fun u ->
- Bind(w + u,fun v ->
- Yield(v)
然而这段代码可以被简化成
- let value1 = some expression
- let value2 = some other expression
也就是说,我们在computation expression中有两个值,现在问题很明显,如何让这两个值结合成一个值作为整个computation expression的返回结果?
这是一个关键点。对单个computation expression,return和yield不提前返回。Computation expression的每个部分总是被计算——不会有短路。如果我们想短路并提前返回,我们必须写代码来实现。
回到刚才提出的问题。我们有两个表达式,这两个表达式有两个结果值:如何将多个值结合到一个值里面?
介绍"Combine"
上面问题的答案就是使用“combine”方法,这个方法输入参数为两个包装类型值,然后将这两个值结合生成另外一个包装值。
在我们的例子中,我们使用int option,故一个简单的实现就是将数字加起来。每个参数是一个option类型,需要考虑四种情况,代码如下
- type TraceBuilder() =
- // other members as before
- member this.Combine (a,b) =
- match a,b with
- | Some a', Some b' ->
- printfn "combining %A and %A" a' b'
- Some (a' + b')
- | Some a', None ->
- printfn "combining %A with None" a'
- Some a'
- | None, Some b' ->
- printfn "combining None with %A" b'
- Some b'
- | None, None ->
- printfn "combining None with None"
- None
- // make a new instance
- let trace = new TraceBuilder()
运行测试代码
- trace {
- yield
- yield
- } |> printfn "Result for yield then yield: %A"
然而,这次却获得了一个不同的错误
- This control construct may only be used if the computation expression builder defines a 'Delay' method
Delay方法类似一个钩子,使computation expression延迟计算,直到需要用到其值时才进行计算。一会我们将讨论这其中的细节。现在,我们创建一个默认实现
- type TraceBuilder() =
- // other members as before
- member this.Delay(f) =
- printfn "Delay"
- f()
- // make a new instance
- let trace = new TraceBuilder()
再次运行测试代码
- trace {
- yield
- yield
- } |> printfn "Result for yield then yield: %A"
最后我们获得结果如下
- Delay
- Yield an unwrapped as an option
- Delay
- Yield an unwrapped as an option
- combining and
- Result for yield then yield: Some
整个工作流的结果为所有yield的和,即3。
如果在工作流中发生一个“错误”(例如,None),那第二个yield不发生,总的结果为Some 1
- trace {
- yield
- let! x = None
- yield
- } |> printfn "Result for yield then None: %A"
使用三个yield
- trace {
- yield
- yield
- yield
- } |> printfn "Result for yield x 3: %A"
结果如期望,为Some 6
我们甚至可以混用yield和return。除了语法不同,结果是相同的
- trace {
- yield
- return
- } |> printfn "Result for yield then return: %A"
- trace {
- return
- return
- } |> printfn "Result for return then return: %A"
使用Combine实现顺序产生结果
将数值加起来不是yield真正的目的,尽管你也可以使用yield类似地将字符串连接起来,就像StringBuilder一样。
yield更一般地是用来顺序产生结果,现在我们已经知道Combine,我们可以使用Combine和Delay方法来扩展“ListBuilder”工作流
- Combine方法是连接list
- Delay方法使用默认的实现
整个建造类如下
- type ListBuilder() =
- member this.Bind(m, f) =
- m |> List.collect f
- member this.Zero() =
- printfn "Zero"
- []
- member this.Yield(x) =
- printfn "Yield an unwrapped %A as a list" x
- [x]
- member this.YieldFrom(m) =
- printfn "Yield a list (%A) directly" m
- m
- member this.For(m,f) =
- printfn "For %A" m
- this.Bind(m,f)
- member this.Combine (a,b) =
- printfn "combining %A and %A" a b
- List.concat [a;b]
- member this.Delay(f) =
- printfn "Delay"
- f()
- // make an instance of the workflow
- let listbuilder = new ListBuilder()
下面使用它的代码
- listbuilder {
- yield
- yield
- } |> printfn "Result for yield then yield: %A"
- listbuilder {
- yield
- yield! [;]
- } |> printfn "Result for yield then yield! : %A"
以下是一个更为复杂的例子,这个例子使用了for循环和一些yield
- listbuilder {
- for i in ["red";"blue"] do
- yield i
- for j in ["hat";"tie"] do
- yield! [i + " " + j;"-"]
- } |> printfn "Result for for..in..do : %A"
然后结果为
- ["red"; "red hat"; "-"; "red tie"; "-"; "blue"; "blue hat"; "-"; "blue tie"; "-"]
可以看到,结合for..in..do和yield,我们已经很接近内建的seq表达式语法了(当然,除了不像seq那样的延迟特性)。
我强烈建议你再回味一下以上那些内容,直到非常清楚在那些语法的背后发生了什么。正如你在上面的例子中看到的一样,你创造性地可以使用yeild产生各种不规则list,而不仅仅是简单的list
说明:如果想知道while,我们将延后一些,直到我们在下一篇中讲完了Delay之后再来讨论while。
"Combine"处理顺序
Combine方法只有两个输入参数,那如果组合多个两个的值呢?例如,下面代码组合4个值
- listbuilder {
- yield
- yield
- yield
- yield
- } |> printfn "Result for yield x 4: %A"
如果你看输出,你将会知道是成对地组合值
- combining [] and []
- combining [] and [; ]
- combining [] and [; ; ]
- Result for yield x : [; ; ; ]
更准确地说,它们是从最后一个值开始,向后被组合起来。“3”和“4”组合,结果再与“2”组合,如此类推。
无序的Combine
在之前的第二个有问题的例子中,表达式是无序的,我们只是让两个独立的表达式处于同一行中
- trace {
- if true then printfn "hello" //expression 1
- return //expression 2
- } |> printfn "Result for combine: %A"
此时,如何组合组合表达式?
有很多通用的方法,具体是哪种方法还依赖于工作流想实现什么目的。
为有“success”或“failure”的工作流实现combine
如果工作流有“success”或者“failure”的概念,则一个标准的方法是:
- 如果第一个表达式“succeeds”(执行成功),则使用表达式的值
- 否则,使用第二个表达式的值
在本例中,我们通常对Zero使用“failure”值。
在将一系列的“or else”表达式链接起来时,这个方法非常有用,第一个成功的表达式的值将成为整体的返回值。
- if (do first expression)
- or else (do second expression)
- or else (do third expression)
例如对maybe工作流,如果第一个表达式结果是Some,则返回第一个表达式的值,否则返回第二个表达式的值,如下所示
- type TraceBuilder() =
- // other members as before
- member this.Zero() =
- printfn "Zero"
- None // failure
- member this.Combine (a,b) =
- printfn "Combining %A with %A" a b
- match a with
- | Some _ -> a // a succeeds -- use it
- | None -> b // a fails -- use b instead
- // make a new instance
- let trace = new TraceBuilder()
例子:解析
试试一个有解析功能的例子,其实现如下
- type IntOrBool = I of int | B of bool
- let parseInt s =
- match System.Int32.TryParse(s) with
- | true,i -> Some (I i)
- | false,_ -> None
- let parseBool s =
- match System.Boolean.TryParse(s) with
- | true,i -> Some (B i)
- | false,_ -> None
- trace {
- return! parseBool "" // fails
- return! parseInt ""
- } |> printfn "Result for parsing: %A"
结果如下
- Some (I )
可以看到第一个return!表达式结果为None,它被忽略掉,所以整个表达式结果为第二个表达式的值,Some (I 42)
例子:查字典
在这个例子中,我们在一些字典中查询一些键,并在找到对应的值的时候返回
- let map1 = [ ("","One"); ("","Two") ] |> Map.ofList
- let map2 = [ ("A","Alice"); ("B","Bob") ] |> Map.ofList
- trace {
- return! map1.TryFind "A"
- return! map2.TryFind "A"
- } |> printfn "Result for map lookup: %A"
结果如下
- Result for map lookup: Some "Alice"
可以看到,第一个查询结果为None,它被忽略掉,故整个语句结果为第二次查询结果值
从上面的讨论可见,这个技术在解析或者计算一系列操作(可能不成功)时非常方便。
为带有顺序步骤的工作流实现“combine”
如果工作流的操作步骤是顺序的,那整体的结果就是最后一步的值,而前面步骤的计算仅是为了获得边界效应(副作用,如改变某些变量的值)。
通常在F#中,顺序步骤可能会写成这样
- do some expression
- do some other expression
- final expression
或者使用分号语法,即
- some expression; some other expression; final expression
在普通的F#语句中,最后一个表达式除外的每个表达式的计算结果值均为unit。
Computation expression的等效顺序操作是将每个表达式(最后一个表达式除外)看成一个unit的包装类型值,然后将这个值传入下一个表达式,如此类推,直到最后一个表达式。
这就跟bind所做的事情差不多,所以最简单的实现就是再次利用Bind方法。当然,这里Zero就是unit的包装值
- type TraceBuilder() =
- // other members as before
- member this.Zero() =
- printfn "Zero"
- this.Return () // unit not None
- member this.Combine (a,b) =
- printfn "Combining %A with %A" a b
- this.Bind( a, fun ()-> b )
- // make a new instance
- let trace = new TraceBuilder()
与普通的bind不同的是,这个continuation有一个unit类型的输入,然后计算b。这反过来要求a是WrapperType<unit>类型,或者更具体地,如我们这里例子中的unit option
以下是一个顺序过程的例子,实现了Combine
- trace {
- if true then printfn "hello......."
- if false then printfn ".......world"
- return
- } |> printfn "Result for sequential combine: %A"
输出结果为
- hello.......
- Zero
- Returning a unwrapped <null> as an option
- Zero
- Returning a unwrapped <null> as an option
- Returning a unwrapped as an option
- Combining Some null with Some
- Combining Some null with Some
- Result for sequential combine: Some
注意整个语句的结果是最后一个表达式的值。
为创建数据结构的工作流实现“combine”
最后,还有一个工作流的常见模式是创建数据结构。在这种情况下,Combine应该合并两个数据结构,并且如果需要的话(如果可能),Zero方法应该创建一个空数据结构。
在前面的“list builder”例子中,我们使用的就是这个方法。Combine结合两个列表,并且Zero是空列表。
混合“Combine”与“Zero”的说明
我们已经看到关于option类型的两种不同的Combine实现。
- 第一个使用options指示“success/failure”,第一个成功的表达式结果即为最终的结果值,在这个情况下,Zero被定义成None。
- 第二个是顺序步骤操作的例子,在这种情况下,Zero被定义成Some ()
两种情况均能良好的工作,但是这两个例子是否只是侥幸能正常工作?有没有关于正确实现Combine和Zero的指导说明?
首先,如果输入参数交换位置,Combine不必返回相同的结果值,即,Combine(a,b)和Combine(b,a)不需要相同。“list builder”就是一个很好的例子
另外,把Zero与Combine连接起来是很有用的。
规则:Combine(a,Zero)应该与Combine(Zero,a)相同,而Combine(Zero,a)应该与a相同。
为了使用算法的类比,你可以把Combine看成加法(这不是一个差劲的类比——它确实将两个值相加)。当然,Zero就是数字0,故上面的这条规则可以表述成:
规则:a+0与0+a相同,与a相同,而+表示Combine,0表示Zero。
如果你观察有关option类型的第一个Combine实现(“success/failure”),你会发现它确实与这条规则符合,第二个实现(“bind” with Some())也是如此。
另外一方面,如果我们已经使用“bind”来实现Combine,将Zero定义成None,则它不遵循这个规则,这意味着我们已经碰到一些错误。
不带bind的“Combine”
关于其他的builder方法,如果不需要它们,则不必实现这些方法。故对一个严格顺序的工作流而言,可以简单地创建一个包含Combine、Zero和Yield方法的建造类(builder class),也就是,不用实现Bind和Return。
以下是一个最简单的实现
- type TraceBuilder() =
- member this.ReturnFrom(x) = x
- member this.Zero() = Some ()
- member this.Combine (a,b) =
- a |> Option.bind (fun ()-> b )
- member this.Delay(f) = f()
- // make an instance of the workflow
- let trace = new TraceBuilder()
使用方法如下
- trace {
- if true then printfn "hello......."
- if false then printfn ".......world"
- return! Some
- } |> printfn "Result for minimal combine: %A"
类似地,如果你有一个面向数据结构的工作流,可以只实现Combine和其他一些帮助方法。例如,以下为一个list builder类的简单实现
- type ListBuilder() =
- member this.Yield(x) = [x]
- member this.For(m,f) =
- m |> List.collect f
- member this.Combine (a,b) =
- List.concat [a;b]
- member this.Delay(f) = f()
- // make an instance of the workflow
- let listbuilder = new ListBuilder()
尽管这是最简单的实现,我们依然可以如下写使用代码
- listbuilder {
- yield
- yield
- } |> printfn "Result: %A"
- listbuilder {
- for i in [..] do yield i +
- yield
- } |> printfn "Result: %A"
独立的Combine函数
在上一篇中,我们看到“bind”函数通常被当成一个独立函数来使用,并用操作符 >>= 来表示。
Combine函数亦是如此,常被当成一个独立函数来使用。跟bind不同的是,Combine没有一个标准符号——它可以变化,取决于combine函数的用途。
一个符号化的combination操作通常写成 ++ 或者 <+>。我们之前对options使用的“左倾”的combination(即,如果第一个表达式失败,则只执行第二个表达式)有时候写成 <++。
以下是一个关于options的独立的左倾combination,跟上面那个查询字典的例子类似。
- module StandaloneCombine =
- let combine a b =
- match a with
- | Some _ -> a // a succeeds -- use it
- | None -> b // a fails -- use b instead
- // create an infix version
- let ( <++ ) = combine
- let map1 = [ ("","One"); ("","Two") ] |> Map.ofList
- let map2 = [ ("A","Alice"); ("B","Bob") ] |> Map.ofList
- let result =
- (map1.TryFind "A")
- <++ (map1.TryFind "B")
- <++ (map2.TryFind "A")
- <++ (map2.TryFind "B")
- |> printfn "Result of adding options is: %A"
总结
这篇文章中我们学到Combine的哪些内容?
- 如果在一个computation expression中需要combine或者“add”不止一个的包装类型值,则需要实现Combine(和Delay)
- Combine方法从后往前地将值成对地结合起来
- 没有一个通用的Combine实现能处理所有情况——需要根据工作流具体的需要定义不同的Combine实现
- 有将Combine关系到Zero的敏感规则
- Combine不依赖Bind的实现
- Combine可以被当成一个独立的函数暴露出来
下一篇中,当计算内部的表达式时,我们增加一些逻辑控制,并引入正确的短路和延迟计算。
Implementing a builder: Combine的更多相关文章
- Implementing a builder: Zero and Yield
原文地址:http://fsharpforfunandprofit.com/posts/computation-expressions-builder-part1/ 前面介绍了bind和continu ...
- ASP.NET Core 认证与授权[5]:初识授权
经过前面几章的姗姗学步,我们了解了在 ASP.NET Core 中是如何认证的,终于来到了授权阶段.在认证阶段我们通过用户令牌获取到用户的Claims,而授权便是对这些的Claims的验证,如:是否拥 ...
- Unity3D & C# 设计模式--23
Unity3D & C#Design Patterns 23 design patterns. Creational Patterns 1. Abstract Factory抽象工厂 创 ...
- Asp.Net Core AuthorizeAttribute 和AuthorizeFilter 跟进及源码解读
一.前言 IdentityServer4已经分享了一些应用实战的文章,从架构到授权中心的落地应用,也伴随着对IdentityServer4掌握了一些使用规则,但是很多原理性东西还是一知半解,故我这里持 ...
- The JSR-133 Cookbook for Compiler Writers(an unofficial guide to implementing the new JMM)
The JSR-133 Cookbook for Compiler Writers by Doug Lea, with help from members of the JMM mailing lis ...
- 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, ...
- 基于用Path.Combine的优化
Path.Combine: 什么时候会用到Path.Combine呢?,当然是连接路径字符串的时候! 所以下面的代码可以完美的工作: public static void Main() { strin ...
- [转]Implementing User Authentication in ASP.NET MVC 6
本文转自:http://www.dotnetcurry.com/aspnet-mvc/1229/user-authentication-aspnet-mvc-6-identity In this ar ...
- builder pattern
design patterns 结合书本和这个网站的这个系列的文章来看: https://www.tutorialspoint.com/design_pattern/builder_pattern.h ...
随机推荐
- TCP的滑动窗口机制【转】
原文链接:http://www.cnblogs.com/luoquan/p/4886345.html TCP这个协议是网络中使用的比较广泛,他是一个面向连接的可靠的传输协议.既然是一个可靠的 ...
- c# PictureBox 的图像上使用鼠标画矩形框
C# 中在图像上画框,通过鼠标来实现主要有四个消息响应函数MouseDown, MouseMove, MouseUp, Paint重绘函数实现.当鼠标键按下时开始画框,鼠标键抬起时画框结束. Poin ...
- asp.net如何把一个tostring类型转化为dateTime类型
Convert.ToDateTime(dr["consult_DealTime"].ToString()).ToString("yyyy-MM-dd"); co ...
- Zabbix概念、安装以及快速入门
Zabbix is an enterprise-class open source distributed monitoring solution.[1] Zabbix是一个企业级的.开源的.分布式的 ...
- CentOS 6.5 安装MySQL5.7 RPM
一.新特性 MySQL 5.7可谓是一个令人激动的里程碑,在默认了InnoDB引擎的基础上,新增了ssl.json.虚拟列等新特性.相对于postgreSQL和MariaDB而言,MySQL5.7做了 ...
- "malloc: * error for object 0x17415d0c0: Invalid pointer dequeued from free list * set a breakpoint in malloc_error_break to debug";
I've fixed this error with Xcode 8 on iOS 8.3. I've just changed Deployment Target from 8.3 to 8.0. ...
- Unity3DGUI:GUILayout
显示效果,注意GUILayout控件默认垂直布局,且在水平布局模块里控件大小默认按控件内容来显示,因此对于水平滑块HorizontalSlider来说需要自定义大小避免变形
- XTU 1250 Super Fast Fourier Transform
$2016$长城信息杯中国大学生程序设计竞赛中南邀请赛$H$题 排序,二分. 对$a$数组,$b$数组从小到大进行排序. 统计每一个$a[i]$作为较大值的时候与$b[i]$对答案的贡献.反过来再统计 ...
- Fiddler的安装设置
一.安装设置Fiddler2 下载完成后安装,安装完成后打开 如下图设置Fiddler 代理: 二.设置手机代理 快捷键win+r打开运行窗口à输入:cmdà确定 在界面上输入:ipconfig,查 ...
- Chapter 2 Open Book——32
I paused for a long moment, and then made the mistake of meeting his gaze. 我停顿了很长时间,然后错误的去对视了他的凝视 我停 ...