原文地址: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寫成中綴操做符的標準方式