`
tianshibaijia
  • 浏览: 1125372 次
文章分类
社区版块
存档分类
最新评论

12.1.3 使用 F# 序列表达式

 
阅读更多

12.1.3 使用 F# 序列表达式

在 C# 中的迭代器是很舒服,因为它们允许我们在普通的 C# 方法中写复杂的代码 (一种类型,实现 IEnumerable<T>/IEnumerator<T> 接口) 。开发人员写的代码使用标准的 C# 功能,比如循环,唯一的改变只是我们可以使用一种新的语句,来做一些非标准的东西。这个新语句用 yield return 来表示(或者 yield break 表示终止序列),非标准的行为返回一个值,作为序列中的下一个元素。然后,在需要的时候使用 MoveNext 方法访问序列(最后,逐个元素进行) 。在 F# 中的序列表达式是类似的:使用一种等效于 yield return 的结构。

写序列表达式

在 C# 中,我们可以自动使用迭代器,在实现返回 IEnumerable<T>、IEnumerator<T>,或对应的非泛型的方法时。F# 序列表达式使用 seq 标识符显式标记,而且不必要使用方法体或函数体。正如其名字所暗示的,序列表达式是表达式的不同类型,我们可以在代码中的任意位置使用。清单 12.2 演示了如何创建一个简单的序列,使用此语法。

Listing 12.2 Introducing sequence expression syntax (F# Interactive)

> let nums =
seq { let n = 10
yield n + 1
printfn "second.."
yield n + 2 };;
val nums : seq<int>

当写序列表达式时,我们把生成序列的整个 F# 表达式括在一个 seq 块中。这个块使用大括号,用 seq 标识符 [1] 表示,编译器可以把这个块主体解释为序列表达式。还有其他可能的标识符指定其他选择性的工作流,在后面,我们将会看到。在 seq 的情况下,这个块把整个表达式转换为延迟生成的序列,从这个值的类型推断可以看到。

序列表达式的主体可以包含具有特殊意义的语句。类似于 C#,有一个语句,返回这个序列的一个元素。在 F# 中,使用 yield 关键字。主体也可以包含其他标准的 F# 结构,比如值绑定,甚至是有副作用的调用。

类似于 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 函数。

当我们实现一个序列表达式时,可能会遇到这样的情况,表达式的主体太长。在这种情况下,最自然的做法就是,将它拆分成几个生成序列部件的函数。如果序列使用多个数据源,我们可能希望读取数据的代码,在单独的函数中。到目前为止,一切顺利,但是,还有一个问题,把从不同函数返回的序列组合起来。

组合序列表达式

在 C# 中的 yield return 关键字只允许返回一个元素,因此,如果要在 C# 中,从使用迭代器实现的方法产生整个序列的话,就必须使用 foreach 循环,遍历序列中的所有元素,一个一个地产生元素。这可以工作,但效率低下,特别是如果以这种方式生成几个嵌套的序列。在函数式编程中,可组合性是一个更重要的方面,所以,F# 允许我们组合序列,从序列表达式产生整个序列,使用专门的语言表达结构:yield!(通常发音 yield-bang)。清单 12.3 演示了以三种不同的方法生成一个城市的序列。

Listing 12.3 Composing sequences from different sources (F# Interactive)

> let capitals = [ "Paris"; "Prague" ];;
val capitals : string list

> let withNew(name) =
seq { yield name
yield "New " + name };;
val withNew : string -> seq<string>

> let allCities =
seq { yield "Oslo"
yield! capitals
yield! withNew("York") };;
val allCities : seq<string>

> allCities |> List.ofSeq;;
val it : string list = ["Oslo"; "Paris"; "Prague"; "York"; "New York"]

清单 12.3 创建两个不同的数据源。第一个是 F# 列表,包含两个首都,这个值的类型是 list<string>,但是,因为 F# 列表实现了 seq <'a> 接口,因此,可以在后面的代码将它当作一个序列来使用。第二个数据源是一个函数,它生成包含两个元素的序列。下面的一段代码演示如何将这两个数据源联接成一个序列。首先,我们使用 yield 语句返回一个值,接着,使用 yield! 结构返回 F# 列表中的所有元素,最后,调用 withNew 函数(返回一个序列),返回该序列的所有元素。从这里可以看出,可以在一个序列表达式混合使用两种产生元素的方法。

就像 yield 一样,yield! 结构也是延迟返回元素。就是说,当代码到达调用 withNew 函数时,这个函数才调用,而且,只返回表示该序列的一个对象。如果我们在函数中写一些代码,放在 seq 块之前,它会在这一点上执行,但是,seq 块的主体并不会执行。它要到 withNew 函数返回之后才发生,因为,我们需要生成的下一个元素。当执行到达第一个 yield 结构时,它将返回该元素,并将控制权返回到调用者。调用者然后执行其他操作,当调用者请求另一个元素时,序列的执行才恢复。

我们一直专注于序列表达式的语法,但是,可能听起来相当尴尬,直到我们开始使用。当使用序列表达式时,有几个模式是共同的,让我们看两个。

------------------------------

1 你可能会感到奇怪,我们把 seq 称为标识符,而不叫关键字,后面你会看到,这就是一个标识符(我们甚至自己定义),而不是 F# 语言中内置的专门的关键字。seq 标识符也并不是由 seq <'a> 类型自动定义的。名字是一样的,但是在这里,seq 标识符是由 F# 库定义不同的符号。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics