原文地址: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 "\tSuccessful: %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 "\tSuccessful: %s" x
f x

在此,确保你已经懂得了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 ) let getLastProductForOrder (OrderId orderId) =
if (orderId = )
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 "\tSuccessful: %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  }
let subworkflow2 = myworkflow { return } let aWrappedValue =
myworkflow {
let! unwrappedValue1 = subworkflow1
let! unwrappedValue2 = subworkflow2
return unwrappedValue1 + unwrappedValue2
}

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

let aWrappedValue =
myworkflow {
let! unwrappedValue1 = myworkflow {
let! x = myworkflow { return }
return x
}
let! unwrappedValue2 = myworkflow {
let! y = myworkflow { return }
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 } // return an Option
maybe { return! (Some ) }

一个更实际的例子

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

总结

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

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

Computation expressions and wrapper types的更多相关文章

  1. More on wrapper types

    原文地址:http://fsharpforfunandprofit.com/posts/computation-expressions-wrapper-types-part2/ 上一篇中,我们说明了包 ...

  2. Computation expressions: Introduction

    本文仅为对原文的翻译,主要是记录以方便以后随时查看.原文地址为http://fsharpforfunandprofit.com/posts/computation-expressions-intro/ ...

  3. 表达式,数据类型和变量(Expressions,Data Types & Variables)

    (一)表达式: 1)4+4就是表达式,它是程序中最基本的编程指令:表达式包含一个值(4)和操作符号(+),然后就会计算出一个单独的值; 2)一个单独的值没有包含操作符号也可以叫表达式,尽管它只计算它本 ...

  4. Java Programming Language Enhancements

    引用:Java Programming Language Enhancements Java Programming Language Enhancements Enhancements in Jav ...

  5. JavaScript简易教程(转)

    原文:http://www.cnblogs.com/yanhaijing/p/3685304.html 这是我所知道的最完整最简洁的JavaScript基础教程. 这篇文章带你尽快走进JavaScri ...

  6. Lambdas in Java 8--reference

    Part 1 reference:http://jaxenter.com/lambdas-in-java-8-part-1-49700.html Get to know lambda expressi ...

  7. Introducing 'bind'

    原文地址:http://fsharpforfunandprofit.com/posts/computation-expressions-bind/ 上一篇讨论了如何理解let作为一个能实现contin ...

  8. Understanding continuations

    原文地址http://fsharpforfunandprofit.com/posts/computation-expressions-continuations/ 上一篇中我们看到复杂代码是如何通过使 ...

  9. Hibernate Validator 6.0.9.Final - JSR 380 Reference Implementation: Reference Guide

    Preface Validating data is a common task that occurs throughout all application layers, from the pre ...

随机推荐

  1. 【LeetCode】31. Next Permutation

    Implement next permutation, which rearranges numbers into the lexicographically next greater permuta ...

  2. Eclipse/MyEclipse 安裝後應該更改的設置

    基本上都通過 Window -> Preferences 進行設置: Java 保存自動格式化: Java:Java -> Editor -> Save Actions,選中 Per ...

  3. 用R语言 做回归分析

    使用R做回归分析整体上是比较常规的一类数据分析内容,下面我们具体的了解用R语言做回归分析的过程. 首先,我们先构造一个分析的数据集 x<-data.frame(y=c(102,115,124,1 ...

  4. 最近用到mysql和mybatis结合常用的知识点坐下整理

    1.当用到集合in(x,x...)参数可以单个或者多个 ,当为单个时: findbyIds(List<Long> ids),或者findByids(Long [] ids)  <se ...

  5. CSS3学习之——【特殊属性】

    一.CSS3的一些特殊属性 1.1 text-shadow text-shadow曾经在css2中就出现过,但在css2.1版本中又被抛弃了,现在css3.0版本又重新捡回来了.这说明text-sha ...

  6. mysql给表添加外键并查询

    CREATE TABLE `heart` ( `heart_ID` ) NOT NULL AUTO_INCREMENT, `heart_name` ) CHARACTER SET utf8 NOT N ...

  7. 【Python】0/1背包、动态规划

    0/1背包问题:在能承受一定重量的背包中,放入重量不同,价值不同的几件物品,怎样放能让背包中物品的价值最大? 比如,有三件物品重量w,价值v分别是 w=[5,3,2] v=[9,7,8] 包的容量是5 ...

  8. hdu 5901 Count primes 素数计数模板

    转自:http://blog.csdn.net/chaiwenjun000/article/details/52589457 计从1到n的素数个数 两个模板 时间复杂度O(n^(3/4)) #incl ...

  9. es6语法

    let定义变量,特性: 1,不允许重复定义 2,不存在预解析 3,变量存在于会块级作用域 即{}内部 const : 定义常量,常量的值不能修改,若常量是对象 对象下的属性可修改. 解构赋值语法: 数 ...

  10. ccf cv讲座记录