原文地址: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. php 内置函数 ( 随手能写出100个才算高级工程师 )

    string 类型 去掉左右空格, trim()字符串的长度 strlen() float类型数组类型 count() 数组个数 资源类型 fopen() 大写转小写 strtolower()小写转大 ...

  2. 坚持使用GNU/Linux

    在Windows或Mac OS X下,包括手机上的iOS与Android,应用程序开发得越来越复杂.点下一个按钮,光标转半天,程序在背后做了一堆你不清楚的操作.这不仅仅让你花更 长且不确定的时间等待, ...

  3. Tomcat 6 跨域的支持

    1.添加2个jar包 这个我是自己保存在云端的 cors-filter-1.7.jar java-property-utils-1.9.jar tomcat7以后自动支持 2.tomcat 下面的we ...

  4. robotium测试

    作者:贺锐链接:https://www.zhihu.com/question/28466134/answer/40921012来源:知乎著作权归作者所有,转载请联系作者获得授权. 直接用自己的手机上就 ...

  5. 浏览器的云加速可能导致IP统计异常

    前段时间弄个流量统计相关的东西,请求展示图片时根据请求的IP进行 md5 签名生成点击链接的验证参数,结果发现一个莫名其妙的问题 发现点击日志中有一小部分点击的IP居然不一致,如果是开放给别人用可能存 ...

  6. CodeForces 669D Little Artem and Dance

    模拟. 每个奇数走的步长都是一样的,每个偶数走的步长也是一样的. 记$num1$表示奇数走的步数,$num2$表示偶数走的步数.每次操作更新一下$num1$,$num2$.最后输出. #pragma ...

  7. css3-文字旋转

    <meta charset="utf-8"/><style> * {margin: 0; padding: 0;} ul { height: 80px; b ...

  8. android移动开发学习笔记(二)神奇的Web API

    本次分两个大方向去讲解Web Api,1.如何实现Web Api?2.如何Android端如何调用Web Api?对于Web Api是什么?有什么优缺点?为什么用WebApi而不用Webservice ...

  9. dplyr 数据操作 常用函数(5)

    继续来了解dplyr中的其他有用函数 1.sample() 目的是可以从一个数据框中,随机抽取一些行,然后组成新的数据框. sample_n(tbl, size, replace = FALSE, w ...

  10. dplyr 数据操作 常用函数(2)

    继上一节常用函数,继续了解其他函数 1.desc() 这个函数和SQL中的排序用法是一样的,表示对数据进行倒序排序. 接下来我们看些例子. a=sample(20,50,rep=T)a desc(a) ...