在 12.4 节,我们用选项值作为示例,介绍了用 LINQ 查询和 F# 计算表达式创建非标准计算的概念,处理选项值的代码,有自定义的值绑定读取实际值,如同标准值。既然我们已经知道如何转换计算表达式,也就知道我们的 Bind 成员会接收值和 lambda 函数。因为我们处理的是选项类型计算表达式,只有当值是 Some(x) 而不是 None 时,我们才打算执行 lambda 表达式;后一种情况,我们可以立即返回 None。
要运行前面的例子,我们需要在 C# 中实现 LINQ 查询运算符,在 F# 中实现选项计算生成器。同样,我们先看 F# 版本,清单 12.21 是有两个成员的 F# 对象类型。我们已经在第六章实现过 Option.bind 函数,在这里要重新实现,提醒一下典型的 bind 操作做什么。
清单12.21 选项类型的计算生成器 (F#)
type OptionBuilder() =
member x.Bind(opt, f) =
match opt with [1]
| Some(value) ->f(value) [2]
| _ –> None [3]
member x.Return(v) = Some(v) <-- 包装实际值
let option = new OptionBuilder()
Bind 成员首先从第一个参数的选项值中,提取值,这类似于我们前面实现的 ValueWrapper<‘T> 类型的 Bind。同样,我们使用模式匹配[1],但在这里,值可以省略,所以,我们将使用 match 结构。如果定义了这个值,则调用指定的函数[2]。这样,我们就把值绑定到使用 let! 声明的符号上,然后,运行计算的其余部分。如果未定义这个值,就返回 None,作为整个计算表达式的结果[3]。
Return 成员有一个参数值,必须返回计算类型的值。在我们的示例中,计算类型是 option<‘a>,所以,把实际值包装到 Some 识别器内。
为了在 C# 中使用查询语法写对应的代码,就需要到在第五章为定义 Option<T> 类型实现的 Select 和 SelectMany 方法。清单 12.22 实现了两个额外的扩展方法,这样,就可以在查询表达式中使用选项了,这次,我们使用在第六章中写的扩展方法,使代码更加简单。
清单12.22 选项类型的查询操作 (C#)
static class OptionExtensions {
public static Option<R>Select<S, R>
(thisOption<S> source, Func<S, R> selector) {
returnsource.Map(selector); [1]
}
public static Option<R>SelectMany<S, V, R>
(thisOption<S> source,
Func<S, Option<V>> valueSelector,
Func<S, V, R> resultSelector) {
returnsource.Bind(sourceValue => [2]
valueSelector(sourceValue).Map(resultValue => [3]
resultSelector(sourceValue, resultValue)));
}
}
如果给定选项值包含实际值,Select 方法应该把给定的函数应用到所携带的值上,然后,再把结果打包到选项类型中。在 F# 中,函数称为 Option.map,C# 方法也使用类似的名字(Map)。如果我们是先看到 LINQ 的话,那么,首先可能会把方法称为Select,但是,最简单的解决方案是添加新的称为Map 方法[1]。
SelectMany 会更复杂,它类似于 bind 操作,但是,此外需要使用由第三个参数,指定额外的函数,格式化操作的结果。在第六章,我们用C# 写过 bind 操作;在这里,我们可以使用 Bind 扩展方法[2]。要调用格式化函数 resultSelector,需要两个参数:一个是由选项携带原始值,另一个是由绑定函数(命名为 selector)产生的值。在处理的末尾,我们可以添加对 Map 的调用执行此操作,但是,需要把这个调用放在 lambda 函数内部,给 Bind 方法[3],这是因为我们还需要访问来自源的原始值。在
lambda 函数内部,原始值是作用域之内(名为 sourceValue 的变量),因此,我们可以把它和新值,即,分配给变量 resultValue,放在一起使用。
这个实现是有点复杂,但它表明,使用函数式编程,可以把很多已有的功能组合起来。如果我们试图自己实现的话,在这里,可以看到类型是宝贵的助手。可能首先使用 Bind 方法,但是,可能看到类型不匹配。会看到什么类型不兼容,如果看过哪些函数可用,你会发现,为了获得正确的类型,需要添加什么。我们自己重复的风险在于:函数式编程中的类型要重要得多,告诉你更多有关程序的正确性问题。
使用新的扩展方法,我们可以运行 12.3 节中的示例。在 F# 中,我们不需要实现 yield 和 for 基本操作,只使用 return 和 let! 就能运行。这是故意的,因为这些第一组基本操作更适合处理各种形式序列的计算。我们仍然需要实现 TryReadInt 方法(类似于 F# 的函数),这些是简单的,因为只要从控制台读取一行,并尝试解析,然后,当字符串是数字时,返回 Some,其它则返回 None。
肯定和可能单子
我们前面看到的两个例子,在 Haskell 中是众所周知的。第一个示例是肯定单子(identity monad),因为这个单子类型与值的实际类型相同,只打包在命名类型中。第二个示例是可能单(maybe monad),因为Maybe 是 Haskell 的类型名,对应于 F# 中的 option<‘a> 类型。
第一个示例没有什么实际意义,演示了在实现计算时需要做的;而第二个示例在写组合大量操作的代码时,其中每个可能失败,是有用的。我们分析这两个例子时,可以发现,单子类型非常重要;一旦我们理解了这种类型,就会知道,是什么让计算非标准。
到目前为止,例子已经有点抽象了;下一节,会有很多更具体的示例,在代码中添加自动日志。