zoukankan      html  css  js  c++  java
  • 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 1
        yield 2
        } |> 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 1
        return 2
        } |> printfn "Result for return then return: %A" 

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

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

    我们会获得同样的错误。

    理解这个问题

    那这里该怎么办呢?

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

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

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

    Bind(1,fun x -> 
       Bind(2,fun y -> 
         Bind(x + y,fun z -> 
            Yield(z)  
    // start a new expression        
    Bind(3,fun w -> 
       Bind(4,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 1
        yield 2
        } |> 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 1
        yield 2
        } |> printfn "Result for yield then yield: %A" 

    最后我们获得结果如下

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

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

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

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

    使用三个yield

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

    结果如期望,为Some 6

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

    trace { 
        yield 1
        return 2
        } |> printfn "Result for yield then return: %A" 
    
    trace { 
        return 1
        return 2
        } |> 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 1
        yield 2
        } |> printfn "Result for yield then yield: %A" 
    
    listbuilder { 
        yield 1
        yield! [2;3]
        } |> 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 1
        yield 2
        yield 3
        yield 4
        } |> printfn "Result for yield x 4: %A" 

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

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

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

    Combine

    无序的Combine

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

    trace { 
        if true then printfn "hello"  //expression 1
        return 1                      //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 "42"  // fails
        return! parseInt "42"
        } |> printfn "Result for parsing: %A"

    结果如下

    Some (I 42)

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

    例子:查字典

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

    let map1 = [ ("1","One"); ("2","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 1
        } |> 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 1 as an option
    Combining Some null with Some 1
    Combining Some null with Some 1
    Result for sequential combine: Some 1

    注意整个语句的结果是最后一个表达式的值。

    为创建数据结构的工作流实现“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 1
        } |> 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 1
        yield 2
        } |> printfn "Result: %A" 
    
    listbuilder { 
        for i in [1..5] do yield i + 2
        yield 42
        } |> 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 = [ ("1","One"); ("2","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可以被当成一个独立的函数暴露出来

    下一篇中,当计算内部的表达式时,我们增加一些逻辑控制,并引入正确的短路和延迟计算。

  • 相关阅读:
    基于接口的动态代理和基于子类的动态代理
    JDBC连接数据库
    关于使用Binlog和canal来对MySQL的数据写入进行监控
    使用VMware12在CentOS7上部署docker实例
    VMWare12pro安装Centos 6.9教程
    读《Java并发编程的艺术》学习笔记(十)
    读《Java并发编程的艺术》学习笔记(九)
    读《Java并发编程的艺术》学习笔记(八)
    读《Java并发编程的艺术》学习笔记(七)
    读《Java并发编程的艺术》学习笔记(六)
  • 原文地址:https://www.cnblogs.com/sjjsxl/p/5011520.html
Copyright © 2011-2022 走看看