原文地址:http://fsharpforfunandprofit.com/posts/computation-expressions-bind/express
上一篇讨论了如何理解let做为一个能实现continuations功能的语法,并介绍了pipeInto函数能让咱们增长钩子(处理逻辑)到continuation管道。安全
如今能够来一探究竟第一个builder方法——Bind,它是computation expression的核心。app
MSDN的computation expression说明了let!表达式是Bind方法的语法糖。请看let!文档说明以及一个示例ide
// 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)和一个lambdapost
lambda的参数x绑定到Bind的第一个参数测试
Bind的参数顺序与let!的参数顺序相反ui
将let!表达式连接起来this
let! x = 1 let! y = 2 let! z = x + y
编译器会转换成调用Bind,就像这样spa
Bind(1, fun x -> Bind(2, fun y -> Bind(x + y, fun z -> etc
我想你会发现有似曾相识的感受。没错,pipeInto函数跟Bind函数彻底相同。
实际上,computation expressions 仅是一种建立语法糖的方式,以让咱们本身来实现想作的事情。
“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是与某种“包装(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的这种行为。
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
经过第一部分后,增长两个函数
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 expressions 是continuation passing(后继传递)的语法糖,隐藏了连接逻辑。
bind是关键函数,用来链接前一步的输出到下一步的输入。
符号>>=是将bind写成中缀操做符的标准方式