到目前为止,我们所讨论过的序列表达式,都是用 seq 标识符表示,后面的代码块括在大括号中。然而,F# 还允许我们创建自己的标识符,给代码块以特殊的意义。通常,这个功能称为计算表达式(computation expressions),而序列表达式是它的一个特例,是由 F#核心所实现,并经编译器优化。
我们已经知道,计算表达式可以包含标准的语言结构,比如 for 循环,还有别的结构,像 yield。在代码块之前的标识符,描述构造的意义,其方式与查询运算符(例如,Select 和 Where 扩展方法)描述 LINQ 查询所执行的操作,完全相同。这就是说,我们可以创建自定义的计算表达式,用于处理选项值。我们可以用 for 结构处理选项值,但是,F# 提供了更好的自定义表达式的方法,在清单 12.16 中可以看到这些替代方法。第一个版本使用的语法类似于序列表达式;第二个版本功能相同,但方法更自然。
清单 12.16 处理选项值的计算表达式 (F#)
// 使用自定义的 ‘for‘ 基本操作,进行值绑定
option {
for n in tryReadInt() do
for m in tryReadInt() do
yield n * m
}
// 使用专门的 ‘let!‘ 基本操作,进行值绑定
option {
let! n = tryReadInt()
let! m = tryReadInt()
return n * m
}
在计算表达式内部,所有自定义的基本操作(例如,for、yield 和 let!)所发生的行为,由可选(option)标识符决定,它定义了我们所写的计算表达式的种类。现在,我们可以知道,序列表达式就是由 seq 标识符定义的特殊情况。我们将在 12.5 节学习定义标识符,现在,先看一下清单 12.16 中的两个例子。
第一个版本与清单 12.15 中的 LINQ 查询极为相似,每一个 for 循环至少能够执行一次。当选项值包含值时,将分别绑定到符号 n 或 m,并执行循环体。开发人员只希望循环处理集合,而是不处理选项值,而结构 for 和 yield 通常只能用于处理序列。当我们创建计算表达式,处理其他类型值时,将使用后面的语法。第二个版本使用了不止两个计算表达式基本操作,第一个是 let!,表示自定义的值绑定。
在这两个版本中,值 n 和 m 的类型是整数,自定义的值绑定从类型 option<int> 的值中取出实际值。当从TryReadInt 返回的值为 None时,它可能无法把值分配给符号,这样,整个计算表达式将立即返回 None,不会执行其余的代码。表达式中的第二个非标准基本操作是 return,描述了如何从值构建选项值。在清单 12.16 中,我们给它的是整数值,它构造的结果类型是 option<int>。
我们刚才所了解的概念,可以看作是一种函数式的设计模式,使用 F# 计算表达式,可以不要了解模式的所有细节。如果想学习如何自定义计算表达式,了解概念和术语有关的背景知识,是非常有用的。补充材料“计算表达式和单子”更详细地讨论了这种模式,以及它与 Haskell 单子的关系。
计算表达式和单子
我们在前面提到过,F# 中的计算表达式是一种称为单子(monads,一元运算)思想的实现,在 Haskell 中非常有用。单子是数学术语,但 F# 使用了不同的名字,更好地反映了这种思想在 F# 语言中的使用。
当定义计算表达式(或单子)时,我们总是使用泛型类型,比如 M <‘a>,这通常称为单子类型(monadic type),它描述了计算的含义,这种类型能够给我们所写代码增加了含义。例如,刚才我们看到的 option<‘a>,给代码增加了返回未定义值(None)的可能性。序列也是一种形式的单子,类型 seq <‘a> 给代码增加了处理多个值的能力。
每个计算表达式(或单子)是由两个函数,bind 和 return,实现的。bind 能够创建和组合计算,处理单子类型的值。在清单 12.16 中,每当我们使用 let! 基本操作时,就使用了bind 操作。return 用于构造单子类型的值。
值得注意的是,序列表达式也是单的一个实例。对于序列,绑定操作是 Seq.collect,虽然在序列表达式中,我们没有用 let! 语法,而是使用更舒服的 for 循环语法。清单 12.16 展示了两者密切相关。序列的 return 操作创建了只有一个元素的序列。在序列表达式的内部,可以用更自然的 yield 基本操作来写。
在下一节,我们将学习可能是最简单的自定义计算,用 C# 和 F# 来实现,解释单子类型的含义,以及 bind 和 return 操作。