More on wrapper types

时间:2023-02-24 17:05:42

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

上一篇中,我们说明了包装类型的概念以及与computation expression的关系。在这一篇中,我们将介绍什么类型是合适的包装类型。

什么样的类型可以是包装类型?

每个computation expression必须要有相应的包装类型,那么什么样的类型可以作为包装类型呢?对包装类型是否有特殊的限制?

有一个通用的原则为:

  • 任何带有泛型参数的类型均可以用作包装类型

例如,你可以使用Option<T>, DbResult<T>等作为包装类型。也可以使用限制了类型参数的包装类型,如Vector<int>

但是对于其他泛型类型如List<T>或者IEnumerable<T>如何呢?事实上,它们也可以被用作包装类型,我们一会就可以看到。

非泛型包装类型是否可行?

是否可以使用一个不带泛型参数的包装类型?

例如,在以前的例子中我们见过一个string上的加法,如"1" + "2"。我们不能聪明地将string看成一个int的包装类型吗?这很酷,是吧

我们来试一试,可是借助Bind和Return的签名帮助我们来实现。

  • Bind函数输入为一个元组。元组的第一部分是一个包装类型(这个例子中是string),第二部分是一个函数,这个函数以一个非包装类型作为输入,并将输入转变为一个包装类型(T -> M<U>)。这个例子中,函数签名是int -> string。
  • Return以一个非包装类型作为输入(这个例子中为int)并将输入转变为一个包装类型,这个例子中,Return签名为int -> string。

以上函数签名如何指导实现过程?

“包装”函数的实现,int -> string,是很简单的,就是int类型的“toString”方法。

Bind函数必须去包装一个string为一个int,然后将这个int传入continuation函数f ,我们可以使用int.Parse函数实现这个去包装操作。

如果Bind函数无法对一个string去包装,因为这个string不是一个有效数字,此时如何处理?这种情况下,绑定函数必须仍然返回一个包装类型(这里是string),所以我们可以只返回一个string如“error”。

builder类的实现如下

type StringIntBuilder() =

    member this.Bind(m, f) =
let b,i = System.Int32.TryParse(m)
match b,i with
| false,_ -> "error"
| true,i -> f i member this.Return(x) =
sprintf "%i" x let stringint = new StringIntBuilder()

现在我们可以尝试使用

let good =
stringint {
let! i = ""
let! j = ""
return i+j
}
printfn "good=%s" good

如果有一个string无效,那么会发生什么

let bad =
stringint {
let! i = ""
let! j = "xxx"
return i+j
}
printfn "bad=%s" bad

看起来不错——在我们的工作流中,将strings看成ints。

但是等下,有问题。

我们给这个工作流一个输入,对输入进行去包装(使用let!),然后立即复包装它(使用return),这其中没有做其他任何事情。会发生什么情况?

let g1 = ""
let g2 = stringint {
let! i = g1
return i
}
printfn "g1=%s g2=%s" g1 g2

以上这段代码没有问题。输入g1和输出g2是相同的值,如我们所期望一样。

但是如果是字符串转换为int时发生错误的情况呢?

let b1 = "xxx"
let b2 = stringint {
let! i = b1
return i
}
printfn "b1=%s b2=%s" b1 b2

这种情况下,我们得到一个跟期望不同的行为。输入b1和输出b2不是相同的值。我们引入了不一致问题。

这在实际中会是一个问题吗?我不清楚,但是我将避免它,使用一个不同的方法,如options,在所有情况都是一致的。

工作流使用包装类型的原则

有个问题,如下代码所示,这两段代码有什么不同,它们的行为是否不同?

// fragment before refactoring
myworkflow {
let wrapped = // some wrapped value
let! unwrapped = wrapped
return unwrapped
} // refactored fragment
myworkflow {
let wrapped = // some wrapped value
return! wrapped
}

答案是否定的,即它们的行为不应该不同。唯一的不同是在第二个例子中,unwrapped值已经被重构了,直接返回wrapped值。

但是正如我们在前一小节中所见,如果不小心则会引入不一致问题。故,任何一种实现都应该遵循一些标准原则,总结如下:

原则1:如果以一个非包装类型值开始,然后包装它(使用return),然后去包装它(使用bind),那么总是可以回到初始的非包装类型值

这个原则以及下一个原则的关注点为:当包装和去包装值的时候不会丢失信息。

用代码表示这个原则如下

myworkflow {
let originalUnwrapped = something // wrap it
let wrapped = myworkflow { return originalUnwrapped } // unwrap it
let! newUnwrapped = wrapped // assert they are the same
assertEqual newUnwrapped originalUnwrapped
}

原则2: 如果以一个包装类型值开始,然后去包装这个值(使用bind),然后包装它(使用return),则总是可以回到初始的包装类型值。

这个原则跟上面的stringInt工作流一致。

用代码表示则如下

myworkflow {
let originalWrapped = something let newWrapped = myworkflow { // unwrap it
let! unwrapped = originalWrapped // wrap it
return unwrapped
} // assert they are the same
assertEqual newWrapped originalWrapped
}

 原则3:如果创建一个子工作流,那它必须产生与主工作流相同的结果,就好像是将逻辑嵌入到主工作流中。

这个原则要求正确组合。

用代码演示则如下

// inlined
let result1 = myworkflow {
let! x = originalWrapped
let! y = f x // some function on x
return! g y // some function on y
} // using a child workflow ("extraction" refactoring)
let result2 = myworkflow {
let! y = myworkflow {
let! x = originalWrapped
return! f x // some function on x
}
return! g y // some function on y
} // rule
assertEqual result1 result2

将“列表”作为包装类型

之前提过List<T>或者IEnumerable<T>可以作为包装类型,但是怎么实现呢?在包装类型和非包装类型之间没有一对一的对应关系。

这正是“包装类型”类比有一点点误导的地方。我们回想一下bind,bind是一种将一个表达式的输出与另一个表达式的输入联系起来的方法。

我们已经看到,bind函数去包装一个类型,然后将continuation函数f 应用到这个去包装后的值上。但是没有任何规定说只能有一个未包装值。没有理由说我们不能依次应用continuation函数到一个list的每一项上。

也就是说,我们能够写一个bind,这个bind的输入参数为由一个列表以及一个continuation函数f 组成的元组,且continuation函数f 每次处理这个列表中的一个元素,如下

bind( [;;], fun elem -> // expression using a single element )

有了这个概念后,我们可以将一些bind链接起来如下

let add =
bind( [;;], fun elem1 ->
bind( [;;], fun elem2 ->
elem1 + elem2
))

但是我们忽略了一些重要的东西。传入bind的continuation函数f 必须要符合某种函数签名,即有一个未包装类型作为输入参数,并产生一个包装类型的输出。

换句话说,continuation函数f 产生的结果必须总是一个新列表(因为类型包装M必须相同,而这里用列表来包装类型)

bind( [;;], fun elem -> // expression using a single element, returning a list )

这样,我们则必须将上面那个链接起来的代码写成如下形式,其中elem1+elem2的结果被放入一个列表中

let add =
bind( [;;], fun elem1 ->
bind( [;;], fun elem2 ->
[elem1 + elem2] // a list!
))

所以我们bind方法的逻辑类似这样

let bind(list,f) =
// 1) for each element in list, apply f
// 2) f will return a list (as required by its signature)
// 3) the result is a list of lists

现在又已经导致另一个问题了。因为continuation函数f 必须返回一个列表类型,而对作为输入参数的列表的每个元素应用函数f,则产生一个“列表的列表”,“列表的列表”不好,我们需要将它们转成简单的一阶列表。

不过这已经很简单了,因为已经有一个模块函数能做到,即concat

故将以上相关代码放到一起,我们有

let bind(list,f) =
list
|> List.map f
|> List.concat let added =
bind( [;;], fun elem1 ->
bind( [;;], fun elem2 ->
// elem1 + elem2 // error.
[elem1 + elem2] // correctly returns a list.
))

现在我们知道了bind工作机制,就能够自己创建一个“列表工作流”

  • Bind对传入的列表的每一个元素应用continuation函数f,然后将“列表的列表”展平,得到一个一阶列表。List.collect就是一个能做到如此的库函数。
  • Return将未包装类型转为包装类型。这意味着将返回值包装成列表。
type ListWorkflowBuilder() =

    member this.Bind(list, f) =
list |> List.collect f member this.Return(x) =
[x] let listWorkflow = new ListWorkflowBuilder()
let added =
listWorkflow {
let! i = [;;]
let! j = [;;]
return i+j
}
printfn "added=%A" added let multiplied =
listWorkflow {
let! i = [;;]
let! j = [;;]
return i*j
}
printfn "multiplied=%A" multiplied

结果显示第一个集合中的每个元素,其中第一个集合由第二个集合中的每个元素组成。

val added : int list = [; ; ; ; ; ; ; ; ]
val multiplied : int list = [; ; ; ; ; ; ; ; ]

非常奇妙,我们完全隐藏了列表枚举的逻辑,只暴露了工作流本身。

“for”语法糖

如果将列表和序列特别对待,我们可以增加一个语法糖:用一个更自然的东西代替let!

用for..in..do表达式代替let!

// let version
let! i = [;;] in [some expression] // for..in..do version
for i in [;;] do [some expression]

为了让F#编译器能做到这点,我们需要增加一个For方法到我们到build类。For方法与一般的Bind方法的实现相同,但是要求接收一个序列类型(Bind函数对包装类型则没有限制为序列类型)

type ListWorkflowBuilder() =

    member this.Bind(list, f) =
list |> List.collect f member this.Return(x) =
[x] member this.For(list, f) =
this.Bind(list, f) let listWorkflow = new ListWorkflowBuilder()

以下是使用方法

let multiplied =
listWorkflow {
for i in [;;] do
for j in [;;] do
return i*j
}
printfn "multiplied=%A" multiplied

LINQ和“list工作流”

这个 for element in collection do 看起熟悉吗?它非常接近于LINQ的from element in collection...语法。事实上,LINQ使用基本相同的方法在后台实现将一个查询表达式如from element in collection... 转为实际的调用方法。

F#中,bind使用 形如 List.collect函数。LINQ中与List.collect等价的是 SelectMany扩展方法。如果知道SelectMany的工作原理,就可以实现相同的查询。参见Jon Skeet的博客 a helpful blog post

“包装类型”本质

本篇我们已经见过很多包装类型了,并且已经说明每个computation expression必须有相对应的包装类型。但是,还记得一开始的那个logging例子吗?那个例子中没有包装类型,有let!在后台执行的逻辑,但是输入类型与输出类型相同,类型没有被改变。

简单来说,可以将任意类型看作是自身的包装类型,但是,也可以从一个 更深的层次理解这一点。

让我们回过头去考虑一下包装类型如List<T>到底是什么。

如果有一个类型如List<T>,实际上这个类型不是一个真正的类型。List<int>是真正的类型,List<string>也是真正的类型,但是List<T>本身是不完整的,它缺少一个能变成真正类型的参数。

一种方法是将List<T>看成一个函数,而不是一个类型,它是类型的抽象世界的一个函数,而不是值的具体世界的一个函数,但是正如那些将一个值映射到另一个值的函数一样,List<T>,其输入为类型(如int或者string),输出为其他类型(如List<int>或List<string>)。List<T>跟其他函数一样,它有一个参数,即“类型参数”,.net开发者所谓的泛型在计算机科学就是“参数多态”。

一旦我们掌握了函数的概念,即,从一个类型产生另一个类型(称为”类型构造器“),就可以明白当说一个包装类型时,我们指的是一个类型构造器。

但是,如果包装类型仅仅是一个函数,它将一个类型映射到另一个类型,那么,可以确定将一个类型映射到同样的类型的函数也符合吗?嗯,没错。“identity”函数符合我们的定义,可以被用作computation expression的包装类型。

回到代码中来,我们可以定义一个“identity”工作流,它非常简单

type IdentityBuilder() =
member this.Bind(m, f) = f m
member this.Return(x) = x
member this.ReturnFrom(x) = x let identity = new IdentityBuilder() let result = identity {
let! x =
let! y =
return x + y
}

有了这些概念,我们可以知道先前讨论的logging的例子就是一个添加了打印log信息的“identity”工作流。

总结

本篇涵盖了很多主题,希望能对包装类型有个更清楚的认识。我们了解到如何在实际中使用包装类型。

总结一下本篇中的几个关键点:

  • computation expression的主要作用是去包装一个类型以及复包装。
  • 可以很容易组合computation expression,因为Return的输出匹配Bind的输入,都是包装类型
  • 每个computation expression必须有一个相关的包装类型
  • 任何带有一个泛型参数的类型都可以被用作包装类型,即使是列表也是如此。
  • 当创建工作流时,需要确保工作流的实现满足三个有关包装、去包装以及组合的原则。

More on wrapper types的更多相关文章

  1. Computation expressions and wrapper types

    原文地址:http://fsharpforfunandprofit.com/posts/computation-expressions-wrapper-types/ 在上一篇中,我们介绍了“maybe ...

  2. 编写高质量代码&colon;改善Java程序的151个建议&lpar;第2章&colon;基本类型&lowbar;&lowbar;&lowbar;建议26~30&rpar;

    建议26:提防包装类型的null值 我们知道Java引入包装类型(Wrapper Types)是为了解决基本类型的实例化问题,以便让一个基本类型也能参与到面向对象的编程世界中.而在Java5中泛型更是 ...

  3. jackson官方快速入门文档

    官方地址: http://jackson.codehaus.org/ http://wiki.fasterxml.com/JacksonInFiveMinutes http://wiki.faster ...

  4. TIJ——Chapter Eleven&colon;Holding Your Objects

    Java Provides a number of ways to hold objects: An array associates numerical indexes to objects. It ...

  5. Java Programming Language Enhancements

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

  6. JPA 教程

    Entities An entity is a lightweight persistence domain object. Typically an entity represents a tabl ...

  7. JAVA笔记 之 JDK新特性

    JDK1.5新特性1.泛型(Generics) 为集合(collections)提供编译时类型安全,无需每刻从Collections取得一个对象就进行强制转换(cast) 2.增强的for循环(for ...

  8. JavaScript简易教程(转)

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

  9. (spring-第13回【IoC基础篇】)PropertyEditor&lpar;属性编辑器&rpar;--实例化Bean的第五大利器

    上一篇讲到JavaBeans的属性编辑器,编写自己的属性编辑器,需要继承PropertyEditorSupport,编写自己的BeanInfo,需要继承SimpleBeanInfo,然后在BeanIn ...

随机推荐

  1. 解决Win7旗舰版开机后无线网络识别非常慢的问题

    最近电脑开机后WIFI识别和连接非常慢,不知何故.查看百度安全卫士的优化记录,发现其禁用了 Network List Service,将该服务设为自动启动,重启服务后,问题解决.PS:如此优化太可恶!

  2. java 静态变量生命周期(类生命周期)

    Static: 加载:java虚拟机在加载类的过程中为静态变量分配内存. 类变量:static变量在内存中只有一个,存放在方法区,属于类变量,被所有实例所共享 销毁:类被卸载时,静态变量被销毁,并释放 ...

  3. discuz论坛很给力

    11.10:老彭在搭建好论坛. 11.17:  主网站导航中加入“论坛” 11.20: 使用“T客在线”的版本将论坛全新改版. 新版论坛非常大气,让网站增色不少.

  4. sql 减去分钟

    SQL SERVER:SELECT DATEADD( minute,-10,GETDATE()) ORACLE:SELECT to_char(sysdate -interval '10' minute ...

  5. 【HDU2224】The shortest path(双调欧几里得dp)

    算法导论上一道dp,挺有趣的.于是就研究了一阵. dp(i, j)代表从左边第一个点到第i个点与从从左边最后一个点(即为第一个点)到j点的最优距离和.于是找到了子状态. 决策过程 dp[i][j] = ...

  6. Python数据类型——字符串

    概论 字符串顾名思义就是一串字符,由于Python中没有“字符”这种数据类型,所以单个的字符也依然是字符串类型的.字符串可以包含一切数据,无论是能从键盘上找到的,还是你根本都不认识的.与数一样,字符串 ...

  7. Arch Linux下韩文重叠显示

    解决方法 sudo pacman -S wqy-microhei-kr-patched

  8. 2008nian元旦

    我要做---->> 控制财务状况---1记账 2 炒股或基金 3预算 锻炼---每次要达到锻炼的效果 学习计划-- 1报班架构 2 项目管理 3读书 爱好---做饭,收拾整理 职业规划. ...

  9. SQL 公用表达式CTE

    一 基本用法 with mywith as(select * from Student ) select * from mywith 二 递归调用 with mywith as( select ID, ...

  10. Xshell配置ssh免密码登录-密钥公钥&lpar;Public key&rpar;与私钥&lpar;Private Key&rpar;登录【已成功实例】

    本文转自https://blog.csdn.net/qjc_501165091/article/details/51278696 ssh登录提供两种认证方式:口令(密码)认证方式和密钥认证方式.其中口 ...