12.5.3 在 F# 中实现计算生成器
在计算表达式块前面的的标识符,是一个类的实例,它把所需的操作实现成为一个实例成员。许多操作都是可用的:我们不必要支持所有的。用 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 成员。这是因为,作为最后一个参数值给这个成员的函数是连续,意味着,它表示这个计算的其余部分。示例的最后,是调用 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,在转换中使用。
Listing 12.19 Implementing computation builder for values (F#)
type ValueWrapperBuilder() =
member x.Bind(Value(v), f) = f(v)
member x.Return(v) = Value(v)
let value = new ValueWrapperBuilder()
Bind 成员首先要从 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> 类型添加查询运算符。