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,但是等号左边的却是一个int。let!在将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类型的不同case(Success或者是Error),dbresult工作流可以为我们做控制管理,并可以在后台执行一些信息(如打印信息)从而让我们专注于大局。
绑定和返回包装类型
再次复习一下Bind和Return方法的定义。
Return的签名as documented on MSDN如下,可以看到,对某种类型T,Return方法仅仅包装这个类型。
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的原始值,然后(可能)应用函数f 到这个值上,并获得一个类型U的包装类型
- 对Return方法来说,从一个值(图中x)开始,简单的包装它并返回之。
类型包装器是泛型
注意到所有函数使用泛型类型(T和U)而不是包装类型,并且自始至终都如此。例如,不能阻止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都必须要有相应的包装类型。这个包装类型用在Bind和Return中,可以有一个好处:
- 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
}
总结
本篇文章介绍了包装类型以及包装类型与Bind、Return和ReturnFrom方法的关系。
下一篇,我们继续讨论包装类型,包括使用列表作为包装类型。
Computation expressions and wrapper types的更多相关文章
- More on wrapper types
原文地址:http://fsharpforfunandprofit.com/posts/computation-expressions-wrapper-types-part2/ 上一篇中,我们说明了包 ...
- Computation expressions: Introduction
本文仅为对原文的翻译,主要是记录以方便以后随时查看.原文地址为http://fsharpforfunandprofit.com/posts/computation-expressions-intro/ ...
- 表达式,数据类型和变量(Expressions,Data Types & Variables)
(一)表达式: 1)4+4就是表达式,它是程序中最基本的编程指令:表达式包含一个值(4)和操作符号(+),然后就会计算出一个单独的值; 2)一个单独的值没有包含操作符号也可以叫表达式,尽管它只计算它本 ...
- Java Programming Language Enhancements
引用:Java Programming Language Enhancements Java Programming Language Enhancements Enhancements in Jav ...
- JavaScript简易教程(转)
原文:http://www.cnblogs.com/yanhaijing/p/3685304.html 这是我所知道的最完整最简洁的JavaScript基础教程. 这篇文章带你尽快走进JavaScri ...
- Lambdas in Java 8--reference
Part 1 reference:http://jaxenter.com/lambdas-in-java-8-part-1-49700.html Get to know lambda expressi ...
- Introducing 'bind'
原文地址:http://fsharpforfunandprofit.com/posts/computation-expressions-bind/ 上一篇讨论了如何理解let作为一个能实现contin ...
- Understanding continuations
原文地址http://fsharpforfunandprofit.com/posts/computation-expressions-continuations/ 上一篇中我们看到复杂代码是如何通过使 ...
- 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 ...
随机推荐
- php 内置函数 ( 随手能写出100个才算高级工程师 )
string 类型 去掉左右空格, trim()字符串的长度 strlen() float类型数组类型 count() 数组个数 资源类型 fopen() 大写转小写 strtolower()小写转大 ...
- 坚持使用GNU/Linux
在Windows或Mac OS X下,包括手机上的iOS与Android,应用程序开发得越来越复杂.点下一个按钮,光标转半天,程序在背后做了一堆你不清楚的操作.这不仅仅让你花更 长且不确定的时间等待, ...
- Tomcat 6 跨域的支持
1.添加2个jar包 这个我是自己保存在云端的 cors-filter-1.7.jar java-property-utils-1.9.jar tomcat7以后自动支持 2.tomcat 下面的we ...
- robotium测试
作者:贺锐链接:https://www.zhihu.com/question/28466134/answer/40921012来源:知乎著作权归作者所有,转载请联系作者获得授权. 直接用自己的手机上就 ...
- 浏览器的云加速可能导致IP统计异常
前段时间弄个流量统计相关的东西,请求展示图片时根据请求的IP进行 md5 签名生成点击链接的验证参数,结果发现一个莫名其妙的问题 发现点击日志中有一小部分点击的IP居然不一致,如果是开放给别人用可能存 ...
- CodeForces 669D Little Artem and Dance
模拟. 每个奇数走的步长都是一样的,每个偶数走的步长也是一样的. 记$num1$表示奇数走的步数,$num2$表示偶数走的步数.每次操作更新一下$num1$,$num2$.最后输出. #pragma ...
- css3-文字旋转
<meta charset="utf-8"/><style> * {margin: 0; padding: 0;} ul { height: 80px; b ...
- android移动开发学习笔记(二)神奇的Web API
本次分两个大方向去讲解Web Api,1.如何实现Web Api?2.如何Android端如何调用Web Api?对于Web Api是什么?有什么优缺点?为什么用WebApi而不用Webservice ...
- dplyr 数据操作 常用函数(5)
继续来了解dplyr中的其他有用函数 1.sample() 目的是可以从一个数据框中,随机抽取一些行,然后组成新的数据框. sample_n(tbl, size, replace = FALSE, w ...
- dplyr 数据操作 常用函数(2)
继上一节常用函数,继续了解其他函数 1.desc() 这个函数和SQL中的排序用法是一样的,表示对数据进行倒序排序. 接下来我们看些例子. a=sample(20,50,rep=T)a desc(a) ...