在 C# 中,当我们实现返回 IEnumerable<T>、IEnumerator<T>,或对应的非泛型方法时,可以自动使用迭代器。F# 序列表达式使用 seq 标识符显式标记,而且不必要使用方法体或函数体。正如其名字所暗示的,序列表达式是表达式的不同类型,我们可以在代码中的任意位置使用。清单 12.2 演示了使用此语法,创建简单的序列。
清单 12.2 介绍序列表达式的语法 (F# Interactive)
> let nums =
seq { let n = 10 [1]
yield n + 1 [2]
printfn "second.." [3]
yield n + 2 };;
val nums : seq<int> [4]
写序列表达式时,我们把生成序列的整个 F# 表达式,括在一个 seq 块中[1]。块使用大括号,在开头使用 seq 标识符 1,表示编译器应该把块的主体解释为序列表达式。在后面我们将会看到,还有其他可能的标识符,指定其他选择性工作流。使用 seq 块,把整个表达式转换为延迟生成的序列,这从推断出的值类型中可以看到[4]。
序列表达式的主体可以包含具有专门含义的语句。与 C# 相类似,有从序列中返回元素的语句;在 F# 中,使用 yield 关键字[2]。主体也可以包含其他标准的 F# 结构,比如值绑定,甚至是执行有副作用的调用[3]。
类似于 C#,序列表达式的主体是延迟执行的。创建序列值(在前面示例中的值 nums)时,序列表达式的主体并不执行;只有访问序列中的元素时,才发生,每次访问一个元素时,序列表达式代码才执行,直到下一个 yield 语句。在 C# 中,访问迭代器中元素,最常用的是 foreach 循环。下面的 F# 示例,我们将使用 List.ofSeq 函数,把序列转换为不可变的 F# 列表:
> nums |> List.ofSeq;;
second..
val it : int list = [11; 12]
返回的列表包含序列生成两个的元素。这就是说,必须计算整个表达式,包括中途的执行 printfn 调用,因此,输出中包含来自序列表达式的打印行。如果我们只从序列中取一个元素,那么,序列表达式只计算到第一个 yield 调用,字符串不会打印出来:
> nums |> Seq.take 1 |> List.ofSeq;;
val it : int list = [11]
我们将使用Seq 模块中的一个序列处理函数,它只从序列中取一个元素。take 函数返回一个新的序列,参数指定元素的数量(示例中是 1),然后终止。我们可以将它转换为 F# 列表,就得到只包含一个元素的列表,但不调用 printfn 函数。
当我们实现序列表达式时,可能会遇到表达式主体太长的情况。在这种情况下,最自然的做法,就是将它拆分成几个生成序列部件的函数。如果序列使用多个数据源,我们可能要有在单独的函数中,读数据的代码。到目前为止,一切顺利,但是,还有一个问题,把从不同函数返回的序列组合起来。
------------------------------
1 你可能会感到奇怪,我们把 seq 称为标识符,而不关键字;后面会看到,它就是标识符(我们甚至可以自己定义),而不是 F# 语言中内置的、专门关键字。seq 标识符也不是由 seq <‘a> 类型自动定义的。名字虽然一样,但是在这里的seq 标识符是不同于由 F# 库定义的符号。