zoukankan      html  css  js  c++  java
  • 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方法的关系。

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

  • 相关阅读:
    Apache 配置 HTTPS访问
    Symfony——如何使用Assetic实现资源管理
    跟我一起学wpf(1)-布局
    wpf图片定点缩放
    Chapter 3 Shared Assemblies and Strongly Named Assemblies
    [JavaScript]父子窗口间参数传递
    [HASH]MOD运算用户哈希函数
    [Linux]返回被阻塞的信号集
    [Linux]信号集和sigprocmask信号屏蔽函数
    [Linux]不可重入函数
  • 原文地址:https://www.cnblogs.com/sjjsxl/p/4985557.html
Copyright © 2011-2022 走看看