• Introducing 'bind'


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

    上一篇讨论了如何理解let作为一个能实现continuations功能的语法,并介绍了pipeInto函数能让我们增加钩子(处理逻辑)到continuation管道。

    现在可以来一探究竟第一个builder方法——Bind,它是computation expression的核心。

    介绍“Bind

    MSDN的computation expression说明了let!表达式是Bind方法的语法糖。请看let!文档说明以及一个示例

    // documentation
    {| let! pattern = expr in cexpr |}
    
    // real example
    let! x = 43 in some expression

    Bind方法文档说明及示例

    // documentation
    builder.Bind(expr, (fun pattern -> {| cexpr |}))
    
    // real example
    builder.Bind(43, (fun x -> some expression))

    Bind有两个参数,一个表达式(43)和一个lambda

    lambda的参数x绑定到Bind的第一个参数

    Bind的参数顺序与let!的参数顺序相反

    let!表达式链接起来

    let! x = 1
    let! y = 2
    let! z = x + y

    编译器会转换成调用Bind,就像这样

    Bind(1, fun x ->
    Bind(2, fun y ->
    Bind(x + y, fun z ->
    etc

    我想你会发现有似曾相识的感觉。没错,pipeInto函数跟Bind函数完全相同。

    实际上,computation expressions 仅是一种创建语法糖的方式,以让我们自己来实现想做的事情。

    "bind" 函数

    bind”是一个标准的函数模式,不依赖于computation expression

    首先,为何称之为“bind”?嗯,正如我们所见,“bind”函数可被想象成将一个输入值传入到一个函数,即,绑定一个值到一个函数的参数上。可见“bind”类似于管道或组合。

    事实上,可以将它转成一个中缀操作

    let (>>=) m f = pipeInto(m,f)

    顺便一提,“>>=”是将bind写成中缀操作符的标准方式。如果你曾在F#代码中见过这个符号,这个符号的意思就是这里绑定的含义。

    再看之前“安全除法”的例子,现在可以将代码写成如下

    let divideByWorkflow x y w z = 
        x |> divideBy y >>= divideBy w >>= divideBy z

    这种方式与普通的管道或者组合的区别不是很明显,但是有两点值得一提

    • 首先,bind函数在每个场景下有额外的自定义行为(如前面文章中logging和安全除法的例子,分别有额外的打印log和对除数是否为0的判断),它不像管道或者组合,它不是一个泛型函数
    • 其次,输入参数类型(如上面的m)不一定与函数参数(如上面的f)的输出类型相同,因此这个bind所做的事情就是优雅的处理这个不匹配以让函数可以被链接起来

    我们可以在下一篇看到,bind是与某种“包装(wrapper)类型”配合使用,它的值参数可能是WrapperType<TypeA>bind函数的函数参数的签名总是TypeA -> WrapperType<TypeB>

    在“安全除法”的例子中,包装类型是Option。值参数(对应上面的m)的类型是Option<int>,函数参数(对应上面的f)的签名是int -> Option<int>

    再看一个例子,其中使用了中缀绑定函数

    let (>>=) m f = 
        printfn "expression is %A" m
        f m
    
    let loggingWorkflow = 
        1 >>= (+) 2 >>= (*) 42 >>= id

    这个例子中,没有包装类型。所有的都是int类型。但即使如此,bind也有幕后打印logging的这种行为。

    Option.bind 和回顾"maybe" 工作流

    F#库中,你可以在很多地方看到Bind函数。现在你知道它们是什么。

    一个特别有用的例子是Option.bind,这跟我们上面写的代码的功能相同,即

    如果输入参数为None,那不再调用continuation函数。

    如果输入参数为Some,那调用continuation函数,并将Some的内容作为参数传入到函数中。

    下面是我们自己写的函数

    let pipeInto (m,f) =
       match m with
       | None -> 
           None
       | Some x -> 
           x |> f

    然后是Option.bind的实现

    module Option = 
        let bind f m =
           match m with
           | None -> 
               None
           | Some x -> 
               x |> f 

    Option.bind重写“maybe”工作流

    type MaybeBuilder() =
        member this.Bind(m, f) = Option.bind f m
        member this.Return(x) = Some x

    复习不同的实现方法

    迄今已经用四种不同的方法实现“安全除法”。将它们放在一起,并作比较

    首先是最原始的版本,它使用了一个显式的工作流

    module DivideByExplicit = 
    
        let divideBy bottom top =
            if bottom = 0
            then None
            else Some(top/bottom)
    
        let divideByWorkflow x y w z = 
            let a = x |> divideBy y 
            match a with
            | None -> None  // give up
            | Some a' ->    // keep going
                let b = a' |> divideBy w
                match b with
                | None -> None  // give up
                | Some b' ->    // keep going
                    let c = b' |> divideBy z
                    match c with
                    | None -> None  // give up
                    | Some c' ->    // keep going
                        //return 
                        Some c'
        // test
        let good = divideByWorkflow 12 3 2 1
        let bad = divideByWorkflow 12 3 0 1

    其次,使用我们自己定义的函数的版本(也就是“pipeInto”)

    module DivideByWithBindFunction = 
    
        let divideBy bottom top =
            if bottom = 0
            then None
            else Some(top/bottom)
    
        let bind (m,f) =
            Option.bind f m
    
        let return' x = Some x
           
        let divideByWorkflow x y w z = 
            bind (x |> divideBy y, fun a ->
            bind (a |> divideBy w, fun b ->
            bind (b |> divideBy z, fun c ->
            return' c 
            )))
    
        // test
        let good = divideByWorkflow 12 3 2 1
        let bad = divideByWorkflow 12 3 0 1

    然后,使用computation expression的版本

    module DivideByWithCompExpr = 
    
        let divideBy bottom top =
            if bottom = 0
            then None
            else Some(top/bottom)
    
        type MaybeBuilder() =
            member this.Bind(m, f) = Option.bind f m
            member this.Return(x) = Some x
    
        let maybe = new MaybeBuilder()
    
        let divideByWorkflow x y w z = 
            maybe 
                {
                let! a = x |> divideBy y 
                let! b = a |> divideBy w
                let! c = b |> divideBy z
                return c
                }    
    
        // test
        let good = divideByWorkflow 12 3 2 1
        let bad = divideByWorkflow 12 3 0 1

    最后,用中缀操作来实现绑定

    module DivideByWithBindOperator = 
    
        let divideBy bottom top =
            if bottom = 0
            then None
            else Some(top/bottom)
    
        let (>>=) m f = Option.bind f m
    
        let divideByWorkflow x y w z = 
            x |> divideBy y 
            >>= divideBy w 
            >>= divideBy z 
    
        // test
        let good = divideByWorkflow 12 3 2 1
        let bad = divideByWorkflow 12 3 0 1

    bind函数是非常强大的。下一篇我们将会看到结合bind和包装类型来创造一种优雅的方式,并用这种方式传递额外的信息。

    练习:你能理解多少?

    在进入下一篇之前,何不来测试下自己是否已经理解之前的内容?

    第一部分——创建一个工作流

    首先,创建一个函数,将字符串解析成int型:

    let strToInt str = ???

    然后,创建一个computation expression builder类,用在工作流,如下所示

    let stringAddWorkflow x y z = 
        yourWorkflow 
            {
            let! a = strToInt x
            let! b = strToInt y
            let! c = strToInt z
            return a + b + c
            }    
    
    // test code
    let good = stringAddWorkflow "12" "3" "2"
    let bad = stringAddWorkflow "12" "xyz" "2"

    解析:

    strToInt函数类似上面的divide函数,故可以写出函数定义如下

    open System
    
    let strToInt (str: string) =
        try
            let res = Convert.ToInt32 str
            Some res
        with
        | _ -> None

    这里也可以换一种转换为int类型的方法,如下

    let strToInt str =
        let b, i = Int32.TryParse str
        match b, i with
        | false, _ -> None
        | true, _ -> Some i

    表示工作流的builder类写成如下

    type YourWorkflowBuilder() =
    
        member this.Bind(x, f) =
        match x with
        | None -> None
           | Some a -> f a
        member this.Return(x) = 
                Some x    

    最后实例化这个类

    let yourWorkflow = new YourWorkflowBuilder()

    运行测试代码,结果为

    val good : int option = Some 17
    val bad : int option = None

    第二部分——创建一个bind函数

     通过第一部分后,增加两个函数

    let strAdd str i = ???
    let (>>=) m f = ???

    然后用上面的两个函数,可以将代码写成如下形式

    let good = strToInt "1" >>= strAdd "2" >>= strAdd "3"
    let bad = strToInt "1" >>= strAdd "xyz" >>= strAdd "3"

    解析:

    首先很容易写出>>=运算符的定义

    let (>>=) m f = Option.bind f m

    然后,由第一部分可知,strToInt函数返回结果类型为int option,通过>>=运算符传给strAdd str函数(柯里化),而>>=运算符内部的bind函数会对这个int option去包装化为int类型,然后将这个int类型参数传给strAdd str函数(参见Option.bind函数的定义),故可知strAdd函数签名为

    strAdd: str:string -> i: int -> int option

    尝试写出strAdd的函数定义

    let strAdd str i =
        match strToInt str with
        | None -> None
        | Some a -> Some (a + i)

    最后运行上面的测试代码,结果为

    val good : int option = Some 6
    val bad : int option = None

    总结

    Computation expressionscontinuation passing(后继传递)的语法糖,隐藏了链接逻辑。

    bind是关键函数,用来连接前一步的输出到下一步的输入。

    符号>>=是将bind写成中缀操作符的标准方式

  • 相关阅读:
    在VS2010下,用C语言编写pthread多线程程序的配置
    java帮助文档系列JDK1.5 JDK1.6 JDK1.7官方中英完整版下载
    瑜伽练习day02----适合练习瑜伽时听的歌曲
    瑜伽练习day01----瑜伽练习的好处,坏处
    抛出错误Debug Assertion Failed!
    stringstream的基本用法
    AWS中S3的Bucket桶访问策略
    AWS中SQS的几项使用注意
    AWS在.Net中连接数据库时发生认证错误的解决办法
    .Net捕捉配置或程序错误
  • 原文地址:https://www.cnblogs.com/sjjsxl/p/4983076.html
Copyright © 2020-2023  润新知