原文地址:点击这里
本篇我们继续讨论从一个使用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可以被当成一个独立的函数暴露出来
下一篇中,当计算内部的表达式时,我们增加一些逻辑控制,并引入正确的短路和延迟计算。