Computation expressions and wrapper types

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

在上一篇中,我们介绍了“maybe”工作流,让我们隐藏了写链接和可选类型的繁杂代码。

典型的“maybe”工作流大概类似

let result = 
    maybe 
        {
        let! anInt = expression of Option<int>
        let! anInt2 = expression of Option<int>
        return anInt + anInt2 
        }

这里有几个点奇怪的行为:

  • let!行,等号后边的表达式是一个int option,但是等号左边的却是一个intlet!在将option绑定到左边的值之前已经对option去包装(unwrapped
  • return行,则进行相反的动作。被返回的表达式是一个int,但是整个“maybe”工作流的值是一个int option。也就是说,return 将原始的int值包装(wrapped)成一个option

在这一篇中,我们将继续这样的观察,并且将看到computation expression的一个主要用途:隐式的去包装(unwrapped)和复包装(rewrapped)一个值,这个值存储在某种包装类型中。

另一个例子

我们访问一个数据库,并想将结果放到一个Success/Error的联合类型中,如下

type DbResult<'a> = 
    | Success of 'a
    | Error of string

然后在访问数据库的方法中运用这个类型。以下是一些简单的例子演示如何使用DbResult类型

let getCustomerId name =
    if (name = "") 
    then Error "getCustomerId failed"
    else Success "Cust42"

let getLastOrderForCustomer custId =
    if (custId = "") 
    then Error "getLastOrderForCustomer failed"
    else Success "Order123"

let getLastProductForOrder orderId =
    if (orderId  = "") 
    then Error "getLastProductForOrder failed"
    else Success "Product456"

现在我们想将这些方法调用链接起来。

显式的方法如下,可以看到,每一步都需要进行模式匹配

let product = 
    let r1 = getCustomerId "Alice"
    match r1 with 
    | Error _ -> r1
    | Success custId ->
        let r2 = getLastOrderForCustomer custId 
        match r2 with 
        | Error _ -> r2
        | Success orderId ->
            let r3 = getLastProductForOrder orderId 
            match r3 with 
            | Error _ -> r3
            | Success productId ->
                printfn "Product is %s" productId
                r3

非常丑陋的代码。使用computation expression则可以拯救我们。

type DbResultBuilder() =

    member this.Bind(m, f) = 
        match m with
        | Error _ -> m
        | Success a -> 
            printfn "	Successful: %s" a
            f a

    member this.Return(x) = 
        Success x

let dbresult = new DbResultBuilder()

有了这个类型的帮助,我们可以专注于整体结构而不用考虑一些细节,从而让代码简洁

let product' = 
    dbresult {
        let! custId = getCustomerId "Alice"
        let! orderId = getLastOrderForCustomer custId
        let! productId = getLastProductForOrder orderId 
        printfn "Product is %s" productId
        return productId
        }
printfn "%A" product'

如果出现错误,这个工作流会漂亮地捕获错误,并告诉我们错误发生的地方,例如

let product'' = 
    dbresult {
        let! custId = getCustomerId "Alice"
        let! orderId = getLastOrderForCustomer "" // error!
        let! productId = getLastProductForOrder orderId 
        printfn "Product is %s" productId
        return productId
        }
printfn "%A" product''

工作流中包装类型的角色

现在我们已经看到两个工作流了(maybe工作流和dbresult工作流),每个工作流都有自己的包装类型(Option<T>DbResult<T>)。

这两个工作流并非有什么特别不同的。事实上,每个computation expression必须有相应的包装类型,而这个包装类型的设计通常与我们想要管理的工作流相关。

上面的例子中DbResult类型不仅仅是一个为了能返回值的简单类型,而是在工作流中扮演着关键的角色:存储工作流的当前状态(错误信息或成功时的结果信息)。通过利用这个DbResult类型的不同caseSuccess或者是Error),dbresult工作流可以为我们做控制管理,并可以在后台执行一些信息(如打印信息)从而让我们专注于大局。

绑定和返回包装类型

再次复习一下BindReturn方法的定义。

Return的签名as documented on MSDN如下,可以看到,对某种类型TReturn方法仅仅包装这个类型。

member Return : 'T -> M<'T>

说明:在签名中,包装类型常被称为M,故M<int>是应用到int的包装类型,M<string>是应用到string的包装类型,以此类推。

我们已经见过两个使用Return方法的例子了。maybe工作流返回一个Some,它是一个option类型,dbresult工作流返回一个Sucess,它是DbResult类型。

// return for the maybe workflow
member this.Return(x) = 
    Some x

// return for the dbresult workflow
member this.Return(x) = 
    Success x

来看Bind的签名

member Bind : M<'T> * ('T -> M<'U>) -> M<'U>

Bind的输入参数为一个元组M<'T>*('T -> M<'U>),返回M<'U>,即应用到类型U的包装类型。

其中元组有两部分

  • M<'T>是类型T的包装类型
  • ('T -> M<'U>)是一个函数,以一个未包装的类型T作为输入参数,输出类型为应用到类型U上的包装类型

或者说,Bind函数做的事情为:

  • 将一个包装类型参数作为输入
  • 将输入参数(M<'T>)去包装化为一个值(类型为T),并对这个值做一些后台逻辑(自定义代码)。
  • 应用函数到这个未包装的值(T)上,并产生一个新的包装类型值(M<'U>
  • 即使没有应用这个函数,Bind也必须返回一个类型U的包装类型(M<'U>)(参考前面安全除法中的除法出错的情况,此时没有应用continuation函数,返回的是None

基于以上的理解,我们给出Bind的方法代码

// return for the maybe workflow
member this.Bind(m,f) = 
   match m with
   | None -> None
   | Some x -> f x

// return for the dbresult workflow
member this.Bind(m, f) = 
    match m with
    | Error _ -> m
    | Success x -> 
        printfn "	Successful: %s" x
        f x

在此,确保你已经懂得了Bind方法所做的事情。

最后,给出一张图来帮助理解

diagram of bind

  • Bind方法来说,从一个包装类型值开始(图中m),将它去包装为一个类型T的原始值,然后(可能)应用函数到这个值上,并获得一个类型U的包装类型
  • Return方法来说,从一个值(图中x)开始,简单的包装它并返回之。

类型包装器是泛型

注意到所有函数使用泛型类型(TU)而不是包装类型,并且自始至终都如此。例如,不能阻止maybe的Bind函数(中的f 函数)以一个int作为输入并返回一个Option<string>,或者以一个string为输入而返回一个Option<bool>,唯一的要求是总是返回一个可选类型Option<something>

为了更好的理解,我们再看上面的例子,但比起到处使用string,我们将为客户id,订单id和产品id创建专有类型,这意味着每一步将使用不同的类型。

先给出类型定义

type DbResult<'a> = 
    | Success of 'a
    | Error of string

type CustomerId =  CustomerId of string
type OrderId =  OrderId of int
type ProductId =  ProductId of string

代码几乎相同,除了Success行改用了新类型。

let getCustomerId name =
    if (name = "") 
    then Error "getCustomerId failed"
    else Success (CustomerId "Cust42")

let getLastOrderForCustomer (CustomerId custId) =
    if (custId = "") 
    then Error "getLastOrderForCustomer failed"
    else Success (OrderId 123)

let getLastProductForOrder (OrderId orderId) =
    if (orderId  = 0) 
    then Error "getLastProductForOrder failed"
    else Success (ProductId "Product456")

应用以上函数,则代码变为

let product = 
    let r1 = getCustomerId "Alice"
    match r1 with 
    | Error e -> Error e
    | Success custId ->
        let r2 = getLastOrderForCustomer custId 
        match r2 with 
        | Error e -> Error e
        | Success orderId ->
            let r3 = getLastProductForOrder orderId 
            match r3 with 
            | Error e -> Error e
            | Success productId ->
                printfn "Product is %A" productId
                r3

从以上代码可以看出,我们可以预见即将写出来的Bind函数中的第一个continuation函数f 的输入参数类型为string(即“Alice”),输出类型为CustomerId option,而第二个continuation函数f 的输入参数类型为CustomerId,与前一个f 函数的输出类型匹配。故可以知道,Bind函数的输入参数类型为T,输出类型为M<U>,只要continuation中下一个函数的输入参数类型为U就行。

有几点变化值得讨论一下:

首先,底部的printfn使用"%A"格式化器而不是"%s"。这是因为ProductId类型是联合类型。

更为细致地,错误行的代码看起来似乎是不必要的。为啥要写| Error e -> Error e?原因是 -> 左边的错误类型与类型DbResult<CustomerId>或者DbResult<OrderId>匹配,但是右边的错误类型必须为DbResult<ProductId>。故即使两个Error看起来一样,但其实它们是不同的类型

下一步,是builder类型,

type DbResultBuilder() =

    member this.Bind(m, f) = 
        match m with
        | Error e -> Error e
        | Success a -> 
            printfn "	Successful: %A" a
            f a

    member this.Return(x) = 
        Success x

let dbresult = new DbResultBuilder()

最后我们使用工作流

let product' = 
    dbresult {
        let! custId = getCustomerId "Alice"
        let! orderId = getLastOrderForCustomer custId
        let! productId = getLastProductForOrder orderId 
        printfn "Product is %A" productId
        return productId
        }
printfn "%A" product'

这一次,每一行的返回值都不同类型(DbResult<CustomerId>DbResult<OrderId>等),但是因为他们有相同的包装类DbResult,故可以如期望一样正常工作。

最后,给出工作流的一个出错的情况的示例

let product'' = 
    dbresult {
        let! custId = getCustomerId "Alice"
        let! orderId = getLastOrderForCustomer (CustomerId "") //error
        let! productId = getLastProductForOrder orderId 
        printfn "Product is %A" productId
        return productId
        }
printfn "%A" product''

组合computation expression

我们已经知道每个computation expression都必须要有相应的包装类型。这个包装类型用在BindReturn中,可以有一个好处:

  • Return的输出可以传送给Bind作为输入

或者说,因为工作流返回一个包装类型,并且let!消费一个包装类型,你可以将一个“子”工作流放到let!表达式的右边。

例如,有一个工作流为myworkflow,然后可以写如下代码

let subworkflow1 = myworkflow { return 42 }
let subworkflow2 = myworkflow { return 43 }

let aWrappedValue = 
    myworkflow {
        let! unwrappedValue1 = subworkflow1
        let! unwrappedValue2 = subworkflow2
        return unwrappedValue1 + unwrappedValue2
        }

或者以行内的形式运用这个工作流

let aWrappedValue = 
    myworkflow {
        let! unwrappedValue1 = myworkflow {
            let! x = myworkflow { return 1 }
            return x
            }
        let! unwrappedValue2 = myworkflow {
            let! y = myworkflow { return 2 }
            return y
            }
        return unwrappedValue1 + unwrappedValue2
        }

如果已经用过async工作流,你可能已经实现过这样的处理,因为async工作流通常包含其他asyncs

let a = 
    async {
        let! x = doAsyncThing  // nested workflow
        let! y = doNextAsyncThing x // nested workflow
        return x + y
    }

介绍“ReturnFrom”

我们已经使用return作为一种包装一个类型并返回这个包装类型的简单方法。

但是,有时候我们的函数已经返回了一个包装类型,我们想直接返回它,return不适合做这个事情,因为它要求一个非包装类型作为输入。

解决方法是采用return!,它采用一个包装类型作为输入并返回这个包装类型。

“builder”类中相应的方法称为ReturnFrom。实现方法通常仅仅是返回这个包装类型(当然,你可以增加额外的代码来实现一些后台逻辑)。

以下是“maybe”工作流的变体,

type MaybeBuilder() =
    member this.Bind(m, f) = Option.bind f m
    member this.Return(x) = 
        printfn "Wrapping a raw value into an option"
        Some x
    member this.ReturnFrom(m) = 
        printfn "Returning an option directly"
        m

let maybe = new MaybeBuilder()

用法如下,同return比较

// return an int
maybe { return 1  }

// return an Option
maybe { return! (Some 2)  }

一个更实际的例子

// using return
maybe 
    {
    let! x = 12 |> divideBy 3
    let! y = x |> divideBy 2
    return y  // return an int
    }    

// using return!    
maybe 
    {
    let! x = 12 |> divideBy 3
    return! x |> divideBy 2  // return an Option
    }

总结

本篇文章介绍了包装类型以及包装类型与BindReturnReturnFrom方法的关系。

下一篇,我们继续讨论包装类型,包括使用列表作为包装类型。

原文地址:https://www.cnblogs.com/sjjsxl/p/4985557.html