原文地址:点击这里

本篇我们继续讨论从一个使用Combine方法的computation expression中返回多值。

前面的故事

到现在为止,我们的表达式建造(builder)类如下

  1. type TraceBuilder() =
  2. member this.Bind(m, f) =
  3. match m with
  4. | None ->
  5. printfn "Binding with None. Exiting."
  6. | Some a ->
  7. printfn "Binding with Some(%A). Continuing" a
  8. Option.bind f m
  9.  
  10. member this.Return(x) =
  11. printfn "Returning a unwrapped %A as an option" x
  12. Some x
  13.  
  14. member this.ReturnFrom(m) =
  15. printfn "Returning an option (%A) directly" m
  16. m
  17.  
  18. member this.Zero() =
  19. printfn "Zero"
  20. None
  21.  
  22. member this.Yield(x) =
  23. printfn "Yield an unwrapped %A as an option" x
  24. Some x
  25.  
  26. member this.YieldFrom(m) =
  27. printfn "Yield an option (%A) directly" m
  28. m
  29.  
  30. // make an instance of the workflow
  31. let trace = new TraceBuilder()

这个类到现在工作正常。但是,我们即将看到一个问题

两个‘yield’带来的问题

之前,我们看到yield可以像return一样返回值。

通常来说,yield不会只使用一次,而是使用多次,以便在一个过程中的不同阶段返回多个值,如枚举(enumeration)。如下代码所示

  1. trace {
  2. yield
  3. yield
  4. } |> printfn "Result for yield then yield: %A"

但是运行这段代码,我们获得一个错误

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

并且如果你使用return来代替yield,你会获得同样的错误

  1. trace {
  2. return
  3. return
  4. } |> printfn "Result for return then return: %A"

在其他上下文中,也同样会有这个错误,比如我们想在做某事后返回一个值,如下代码

  1. trace {
  2. if true then printfn "hello"
  3. return
  4. } |> printfn "Result for if then return: %A"

我们会获得同样的错误。

理解这个问题

那这里该怎么办呢?

为了帮助理解,我们回到computation expression的后台视角,我们能看到return和yield是一系列计算的最后一步,就比如

  1. Bind(,fun x ->
  2. Bind(,fun y ->
  3. Bind(x + y,fun z ->
  1. Return(z) // or Yield

可以将return(或yield)看成是对行首缩进的复位,那样当我们再次return/yield时,我们可以这么写代码

  1. Bind(,fun x ->
  2. Bind(,fun y ->
  3. Bind(x + y,fun z ->
  4. Yield(z)
  5. // start a new expression
  6. Bind(,fun w ->
  7. Bind(,fun u ->
  8. Bind(w + u,fun v ->
  9. Yield(v)

然而这段代码可以被简化成

  1. let value1 = some expression
  2. let value2 = some other expression

也就是说,我们在computation expression中有两个值,现在问题很明显,如何让这两个值结合成一个值作为整个computation expression的返回结果?

这是一个关键点。对单个computation expression,return和yield不提前返回。Computation expression的每个部分总是被计算——不会有短路。如果我们想短路并提前返回,我们必须写代码来实现。

回到刚才提出的问题。我们有两个表达式,这两个表达式有两个结果值:如何将多个值结合到一个值里面?

介绍"Combine"

上面问题的答案就是使用“combine”方法,这个方法输入参数为两个包装类型值,然后将这两个值结合生成另外一个包装值。

在我们的例子中,我们使用int option,故一个简单的实现就是将数字加起来。每个参数是一个option类型,需要考虑四种情况,代码如下

  1. type TraceBuilder() =
  2. // other members as before
  3.  
  4. member this.Combine (a,b) =
  5. match a,b with
  6. | Some a', Some b' ->
  7. printfn "combining %A and %A" a' b'
  8. Some (a' + b')
  9. | Some a', None ->
  10. printfn "combining %A with None" a'
  11. Some a'
  12. | None, Some b' ->
  13. printfn "combining None with %A" b'
  14. Some b'
  15. | None, None ->
  16. printfn "combining None with None"
  17. None
  18.  
  19. // make a new instance
  20. let trace = new TraceBuilder()

运行测试代码

  1. trace {
  2. yield
  3. yield
  4. } |> printfn "Result for yield then yield: %A"

然而,这次却获得了一个不同的错误

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

Delay方法类似一个钩子,使computation expression延迟计算,直到需要用到其值时才进行计算。一会我们将讨论这其中的细节。现在,我们创建一个默认实现

  1. type TraceBuilder() =
  2. // other members as before
  3.  
  4. member this.Delay(f) =
  5. printfn "Delay"
  6. f()
  7.  
  8. // make a new instance
  9. let trace = new TraceBuilder()

再次运行测试代码

  1. trace {
  2. yield
  3. yield
  4. } |> printfn "Result for yield then yield: %A"

最后我们获得结果如下

  1. Delay
  2. Yield an unwrapped as an option
  3. Delay
  4. Yield an unwrapped as an option
  5. combining and
  6. Result for yield then yield: Some

整个工作流的结果为所有yield的和,即3。

如果在工作流中发生一个“错误”(例如,None),那第二个yield不发生,总的结果为Some 1

  1. trace {
  2. yield
  3. let! x = None
  4. yield
  5. } |> printfn "Result for yield then None: %A"

使用三个yield

  1. trace {
  2. yield
  3. yield
  4. yield
  5. } |> printfn "Result for yield x 3: %A"

结果如期望,为Some 6

我们甚至可以混用yield和return。除了语法不同,结果是相同的

  1. trace {
  2. yield
  3. return
  4. } |> printfn "Result for yield then return: %A"
  5.  
  6. trace {
  7. return
  8. return
  9. } |> printfn "Result for return then return: %A"

使用Combine实现顺序产生结果

将数值加起来不是yield真正的目的,尽管你也可以使用yield类似地将字符串连接起来,就像StringBuilder一样。

yield更一般地是用来顺序产生结果,现在我们已经知道Combine,我们可以使用Combine和Delay方法来扩展“ListBuilder”工作流

  • Combine方法是连接list
  • Delay方法使用默认的实现

整个建造类如下

  1. type ListBuilder() =
  2. member this.Bind(m, f) =
  3. m |> List.collect f
  4.  
  5. member this.Zero() =
  6. printfn "Zero"
  7. []
  8.  
  9. member this.Yield(x) =
  10. printfn "Yield an unwrapped %A as a list" x
  11. [x]
  12.  
  13. member this.YieldFrom(m) =
  14. printfn "Yield a list (%A) directly" m
  15. m
  16.  
  17. member this.For(m,f) =
  18. printfn "For %A" m
  19. this.Bind(m,f)
  20.  
  21. member this.Combine (a,b) =
  22. printfn "combining %A and %A" a b
  23. List.concat [a;b]
  24.  
  25. member this.Delay(f) =
  26. printfn "Delay"
  27. f()
  28.  
  29. // make an instance of the workflow
  30. let listbuilder = new ListBuilder()

下面使用它的代码

  1. listbuilder {
  2. yield
  3. yield
  4. } |> printfn "Result for yield then yield: %A"
  5.  
  6. listbuilder {
  7. yield
  8. yield! [;]
  9. } |> printfn "Result for yield then yield! : %A"

以下是一个更为复杂的例子,这个例子使用了for循环和一些yield

  1. listbuilder {
  2. for i in ["red";"blue"] do
  3. yield i
  4. for j in ["hat";"tie"] do
  5. yield! [i + " " + j;"-"]
  6. } |> printfn "Result for for..in..do : %A"

然后结果为

  1. ["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个值

  1. listbuilder {
  2. yield
  3. yield
  4. yield
  5. yield
  6. } |> printfn "Result for yield x 4: %A"

如果你看输出,你将会知道是成对地组合值

  1. combining [] and []
  2. combining [] and [; ]
  3. combining [] and [; ; ]
  4. Result for yield x : [; ; ; ]

更准确地说,它们是从最后一个值开始,向后被组合起来。“3”和“4”组合,结果再与“2”组合,如此类推。

无序的Combine

在之前的第二个有问题的例子中,表达式是无序的,我们只是让两个独立的表达式处于同一行中

  1. trace {
  2. if true then printfn "hello" //expression 1
  3. return //expression 2
  4. } |> printfn "Result for combine: %A"

此时,如何组合组合表达式?

有很多通用的方法,具体是哪种方法还依赖于工作流想实现什么目的。

为有“success”或“failure”的工作流实现combine

如果工作流有“success”或者“failure”的概念,则一个标准的方法是:

  • 如果第一个表达式“succeeds”(执行成功),则使用表达式的值
  • 否则,使用第二个表达式的值

在本例中,我们通常对Zero使用“failure”值。

在将一系列的“or else”表达式链接起来时,这个方法非常有用,第一个成功的表达式的值将成为整体的返回值。

  1. if (do first expression)
  2. or else (do second expression)
  3. or else (do third expression)

例如对maybe工作流,如果第一个表达式结果是Some,则返回第一个表达式的值,否则返回第二个表达式的值,如下所示

  1. type TraceBuilder() =
  2. // other members as before
  3.  
  4. member this.Zero() =
  5. printfn "Zero"
  6. None // failure
  7.  
  8. member this.Combine (a,b) =
  9. printfn "Combining %A with %A" a b
  10. match a with
  11. | Some _ -> a // a succeeds -- use it
  12. | None -> b // a fails -- use b instead
  13.  
  14. // make a new instance
  15. let trace = new TraceBuilder()

例子:解析

试试一个有解析功能的例子,其实现如下

  1. type IntOrBool = I of int | B of bool
  2.  
  3. let parseInt s =
  4. match System.Int32.TryParse(s) with
  5. | true,i -> Some (I i)
  6. | false,_ -> None
  7.  
  8. let parseBool s =
  9. match System.Boolean.TryParse(s) with
  10. | true,i -> Some (B i)
  11. | false,_ -> None
  12.  
  13. trace {
  14. return! parseBool "" // fails
  15. return! parseInt ""
  16. } |> printfn "Result for parsing: %A"

结果如下

  1. Some (I )

可以看到第一个return!表达式结果为None,它被忽略掉,所以整个表达式结果为第二个表达式的值,Some (I 42)

例子:查字典

在这个例子中,我们在一些字典中查询一些键,并在找到对应的值的时候返回

  1. let map1 = [ ("","One"); ("","Two") ] |> Map.ofList
  2. let map2 = [ ("A","Alice"); ("B","Bob") ] |> Map.ofList
  3.  
  4. trace {
  5. return! map1.TryFind "A"
  6. return! map2.TryFind "A"
  7. } |> printfn "Result for map lookup: %A"

结果如下

  1. Result for map lookup: Some "Alice"

可以看到,第一个查询结果为None,它被忽略掉,故整个语句结果为第二次查询结果值

从上面的讨论可见,这个技术在解析或者计算一系列操作(可能不成功)时非常方便。

为带有顺序步骤的工作流实现“combine”

如果工作流的操作步骤是顺序的,那整体的结果就是最后一步的值,而前面步骤的计算仅是为了获得边界效应(副作用,如改变某些变量的值)。

通常在F#中,顺序步骤可能会写成这样

  1. do some expression
  2. do some other expression
  3. final expression

或者使用分号语法,即

  1. some expression; some other expression; final expression

在普通的F#语句中,最后一个表达式除外的每个表达式的计算结果值均为unit。

Computation expression的等效顺序操作是将每个表达式(最后一个表达式除外)看成一个unit的包装类型值,然后将这个值传入下一个表达式,如此类推,直到最后一个表达式。

这就跟bind所做的事情差不多,所以最简单的实现就是再次利用Bind方法。当然,这里Zero就是unit的包装值

  1. type TraceBuilder() =
  2. // other members as before
  3.  
  4. member this.Zero() =
  5. printfn "Zero"
  6. this.Return () // unit not None
  7.  
  8. member this.Combine (a,b) =
  9. printfn "Combining %A with %A" a b
  10. this.Bind( a, fun ()-> b )
  11.  
  12. // make a new instance
  13. let trace = new TraceBuilder()

与普通的bind不同的是,这个continuation有一个unit类型的输入,然后计算b。这反过来要求a是WrapperType<unit>类型,或者更具体地,如我们这里例子中的unit option

以下是一个顺序过程的例子,实现了Combine

  1. trace {
  2. if true then printfn "hello......."
  3. if false then printfn ".......world"
  4. return
  5. } |> printfn "Result for sequential combine: %A"

输出结果为

  1. hello.......
  2. Zero
  3. Returning a unwrapped <null> as an option
  4. Zero
  5. Returning a unwrapped <null> as an option
  6. Returning a unwrapped as an option
  7. Combining Some null with Some
  8. Combining Some null with Some
  9. 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。

以下是一个最简单的实现

  1. type TraceBuilder() =
  2.  
  3. member this.ReturnFrom(x) = x
  4.  
  5. member this.Zero() = Some ()
  6.  
  7. member this.Combine (a,b) =
  8. a |> Option.bind (fun ()-> b )
  9.  
  10. member this.Delay(f) = f()
  11.  
  12. // make an instance of the workflow
  13. let trace = new TraceBuilder()

使用方法如下

  1. trace {
  2. if true then printfn "hello......."
  3. if false then printfn ".......world"
  4. return! Some
  5. } |> printfn "Result for minimal combine: %A"

类似地,如果你有一个面向数据结构的工作流,可以只实现Combine和其他一些帮助方法。例如,以下为一个list builder类的简单实现

  1. type ListBuilder() =
  2.  
  3. member this.Yield(x) = [x]
  4.  
  5. member this.For(m,f) =
  6. m |> List.collect f
  7.  
  8. member this.Combine (a,b) =
  9. List.concat [a;b]
  10.  
  11. member this.Delay(f) = f()
  12.  
  13. // make an instance of the workflow
  14. let listbuilder = new ListBuilder()

尽管这是最简单的实现,我们依然可以如下写使用代码

  1. listbuilder {
  2. yield
  3. yield
  4. } |> printfn "Result: %A"
  5.  
  6. listbuilder {
  7. for i in [..] do yield i +
  8. yield
  9. } |> printfn "Result: %A"

独立的Combine函数

在上一篇中,我们看到“bind”函数通常被当成一个独立函数来使用,并用操作符 >>= 来表示。

Combine函数亦是如此,常被当成一个独立函数来使用。跟bind不同的是,Combine没有一个标准符号——它可以变化,取决于combine函数的用途。

一个符号化的combination操作通常写成 ++ 或者 <+>。我们之前对options使用的“左倾”的combination(即,如果第一个表达式失败,则只执行第二个表达式)有时候写成 <++。

以下是一个关于options的独立的左倾combination,跟上面那个查询字典的例子类似。

  1. module StandaloneCombine =
  2.  
  3. let combine a b =
  4. match a with
  5. | Some _ -> a // a succeeds -- use it
  6. | None -> b // a fails -- use b instead
  7.  
  8. // create an infix version
  9. let ( <++ ) = combine
  10.  
  11. let map1 = [ ("","One"); ("","Two") ] |> Map.ofList
  12. let map2 = [ ("A","Alice"); ("B","Bob") ] |> Map.ofList
  13.  
  14. let result =
  15. (map1.TryFind "A")
  16. <++ (map1.TryFind "B")
  17. <++ (map2.TryFind "A")
  18. <++ (map2.TryFind "B")
  19. |> 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的更多相关文章

  1. Implementing a builder: Zero and Yield

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

  2. ASP.NET Core 认证与授权[5]:初识授权

    经过前面几章的姗姗学步,我们了解了在 ASP.NET Core 中是如何认证的,终于来到了授权阶段.在认证阶段我们通过用户令牌获取到用户的Claims,而授权便是对这些的Claims的验证,如:是否拥 ...

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

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

  4. Asp.Net Core AuthorizeAttribute 和AuthorizeFilter 跟进及源码解读

    一.前言 IdentityServer4已经分享了一些应用实战的文章,从架构到授权中心的落地应用,也伴随着对IdentityServer4掌握了一些使用规则,但是很多原理性东西还是一知半解,故我这里持 ...

  5. 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 ...

  6. 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, ...

  7. 基于用Path.Combine的优化

    Path.Combine: 什么时候会用到Path.Combine呢?,当然是连接路径字符串的时候! 所以下面的代码可以完美的工作: public static void Main() { strin ...

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

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

  9. builder pattern

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

随机推荐

  1. TCP的滑动窗口机制【转】

    原文链接:http://www.cnblogs.com/luoquan/p/4886345.html      TCP这个协议是网络中使用的比较广泛,他是一个面向连接的可靠的传输协议.既然是一个可靠的 ...

  2. c# PictureBox 的图像上使用鼠标画矩形框

    C# 中在图像上画框,通过鼠标来实现主要有四个消息响应函数MouseDown, MouseMove, MouseUp, Paint重绘函数实现.当鼠标键按下时开始画框,鼠标键抬起时画框结束. Poin ...

  3. asp.net如何把一个tostring类型转化为dateTime类型

    Convert.ToDateTime(dr["consult_DealTime"].ToString()).ToString("yyyy-MM-dd"); co ...

  4. Zabbix概念、安装以及快速入门

    Zabbix is an enterprise-class open source distributed monitoring solution.[1] Zabbix是一个企业级的.开源的.分布式的 ...

  5. CentOS 6.5 安装MySQL5.7 RPM

    一.新特性 MySQL 5.7可谓是一个令人激动的里程碑,在默认了InnoDB引擎的基础上,新增了ssl.json.虚拟列等新特性.相对于postgreSQL和MariaDB而言,MySQL5.7做了 ...

  6. "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. ...

  7. Unity3DGUI:GUILayout

    显示效果,注意GUILayout控件默认垂直布局,且在水平布局模块里控件大小默认按控件内容来显示,因此对于水平滑块HorizontalSlider来说需要自定义大小避免变形

  8. XTU 1250 Super Fast Fourier Transform

    $2016$长城信息杯中国大学生程序设计竞赛中南邀请赛$H$题 排序,二分. 对$a$数组,$b$数组从小到大进行排序. 统计每一个$a[i]$作为较大值的时候与$b[i]$对答案的贡献.反过来再统计 ...

  9. Fiddler的安装设置

    一.安装设置Fiddler2  下载完成后安装,安装完成后打开 如下图设置Fiddler 代理: 二.设置手机代理 快捷键win+r打开运行窗口à输入:cmdà确定 在界面上输入:ipconfig,查 ...

  10. Chapter 2 Open Book——32

    I paused for a long moment, and then made the mistake of meeting his gaze. 我停顿了很长时间,然后错误的去对视了他的凝视 我停 ...