Introducing 'bind'

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

上一篇討論瞭如何理解let做爲一個能實現continuations功能的語法,並介紹了pipeInto函數能讓咱們增長鉤子(處理邏輯)到continuation管道。安全

如今能夠來一探究竟第一個builder方法——Bind,它是computation expression的核心。app

介紹「Bind

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" 函數

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

相關文章
相關標籤/搜索