在计算表达式块前面的标识符,是类的实例,把所需的操作实现成为实例成员。许多操作都已经有了,我们根本不必要提供所有的,最基本的操作用 Bind 和 Return 成员实现。当 F# 编译器看到计算表达式,比如清单 12.18 时,会把它转换为使用这些成员的F# 代码。F# 示例转换成这样:
value.Bind(ReadInt(), fun n –>
value.Bind(ReadInt(), fun m–>
let add = n + m
let sub = n – m
value.Return(n * m) ))
每当我们在计算表达式中使用 let! 基本操作时,都会转换为对 Bind 成员的调用。这是因为 readInt 函数返回 ValueWrapper<int> 类型的值,但是,当我们使用自定义的值绑定,为它分配符号 n 时,值的类型将是 int;Bind 成员的目的是要从计算类型中提取值,并用这个值作为参数值,调用表示计算的其余部分函数。
可以把let! 基本操作和标准的值绑定let 的行为进行比较。在清单 12.18 中,如果写成 let n = readInt(),那么,n 的类型就是 ValueWrapper<int>,要得到整数,我们就必须自己提取。这时,我们可以使用 Value 属性,但是,对于值是隐藏的计算,唯一能访问的方式就是通过 Bind 成员。
计算的其余部分转换成函数,为计算提供了很大的灵活性。Bind 成员可能立即调用函数,或者不调用函数,就返回结果。例如,我们处理选项值时,如果Bind 成员的第一个参数是 None,那么,道最后结果就是 None,而不管这个函数的结果如何。这样,bind 操作不会调用给定的函数,因为作为参数使用的选项值是不包含实际值;在其他情况下,bind 操作应该有效地记住这个函数(通过把它作为结果的一部分保存起来),在以后执行。我们会在下一章讨论这样的例子。
我们的示例还表明,多个 let! 结构会转换成嵌套调用 Bind 成员。这是因为,作为给这个成员的最后一个参数值的函数,是连续(continuation),即,表示计算的其余部分。示例最后调用 Return 成员,是使用 return 结构创建的。
理解 bind 和 return的类型签名
各种计算表达式都需要实现的这两个操作的类型,结构总是相同;唯一的不同,是下面的签名中泛型类型 M 的变化:
Bind : M<‘T>* (‘T -> M<‘R>) -> M<‘R>
Return : ‘T -> M<‘T>
在前面的示例中,类型 M<‘T> 是 ValueWrapper <‘T> 类型。通常,bind 操作为了调用指定的函数,需要知道如何从计算类型中获得值。当计算类型携带额外的信息时,bind 操作也需要把由第一个参数值(类型为 M <‘T>)所携带的额外信息,和从函数调用的结果(类型为 M<‘R>)提取的信息组合起来,把它们作为整体结果的一部分返回。return 操作要简单得多,因为它从基本操作值构造了一元类型的实例。
在前面的示例中,我们使用标识符 value 来构造计算,这个标识符就是普通的 F# 值,它是有特定成员对象的实例。在 F# 中,这个对象称为计算生成器(computation builder)。清单 12.19 实现了一个简单的生成器,有两个必须的成员,我们还需要创建实例 value,在转换过程中使用。
清单 12.19 为值实现计算生成器 (F#)
type ValueWrapperBuilder() =
member x.Bind(Value(v), f) =f(v) [1]
member x.Return(v) = Value(v) [2]
let value = new ValueWrapperBuilder() <-- 创建生成器的实例
Bind 成员[1]首先要从ValueWrapper <‘T> 类型中提取实际值,这是在成员的参数列表中完成的,使用差别联合的 Value 识别器作为一个模式,实际值将分配给符号 v。有了这个值以后,我们就可以调用计算 f 的其余部分。这个计算类型不携带任何额外的信息,因此,我们能够返回此调用的结果,作为整个计算的结果。Return 成员就简单了,它只把值打包到计算类型中。
在这个清单中,使用 value 声明,我们现在可以运行清单 12.18 中的计算表达式。F# 还可以使用计算表达式实现 readInt 函数,我们需要把结果打包到 ValueWrapper <int> 类型的实例中,这可以使用 return 基本操作实现:
> let readInt() = value {
let n =Int32.Parse(Console.ReadLine())
return n };;
val readInt : unit ->ValueWrapper<int>
这个函数不需要 bind 操作,因为它不使用任何 ValueWrapper <‘T> 类型的值。整个函数括在计算表达式块中,这样,函数的返回类型是 ValueWrapper<int>,而不是 int。如果我们对ValueWrapper <‘T> 类型一无所知,那么,使用这个函数的唯一方法,是从另一个计算表达式中,使用 let! 基本操作来调用函数。重要的是,计算表达式提供了一种方法,通过组合,可以从简单的值生成更复杂的值。单子值有点像可以组合的黑盒子,而如果我们想看看里面,就需要一些关于单子类型的专门知识。而有了
ValueWrapper <‘T>,我们需只要知道差别联合的结构。
在 C# 中,用查询语法写出类似于 readInt 的函数,是不可能的,因为查询需要有一些输入,来初始化 from 子句。在查询语法中的 let 子句,大致相当于计算表达式中 let 绑定,但是,查询不能从这里开始。不过,如我们在清单 12.15 所见的,使用查询可以写很多有用的东西,因此,我们可以为 ValueWrapper <‘T> 类型添加查询运算符。