12.2.2 无穷序列
在前一章中,我们简单演示了如何使用延迟值实现延迟列表。这种数据结构可以用来创建无穷数据结构,比如,从零开始的整数列表。这是可能的,因为每个元素的计算被推迟了:只当我们访问元素时,才计算它的值,并且,每次只关注一个元素的计算。
使用 seq<'a> 表示序列是相似的。该接口有一个方法 MoveNext,关注下一个计算的元素。序列可能是无窜的,就是说,MoveNext 方法始终能够计算出下一个元素,并永远不会返回 false (表示序列的末尾)。无穷序列听起来可能有点奇怪,但我们将看到,它可以是很有价值的,能有一种好方法分隔算法的不同部分,使代码更具可读性。
在第 4 章中,我们讨论过绘制图表。我们用随机颜色来填充每一个部分,这并不总是有最好的结果。你可以用一个无穷序列来表示图的颜色。在清单 12.5 中,我们首先生成随机颜色的序列,但很快就会看到有其他的选项。
Listing 12.5 Generating an infinite sequence of random colors in C# and F#
// C# version using loops
IEnumerable<Color> RandomColors(){
var rnd = new Random();
while(true) {
int r = rnd.Next(256), g = rnd.Next(256), b = rnd.Next(256);
yield return Color.FromArgb(r, g, b);
}
}
// F# version using recursion
let rnd = new Random()
let rec randomColors = seq {
let r, g, b = rnd.Next(256), rnd.Next(256), rnd.Next(256)
yield Color.FromArgb(r, g, b)
yield! randomColors }
两个实现都包含一个生成颜色的无限循环。在 C# 中,循环使用 while(true) 实现,函数式方法创建无限循环是使用。在无限循环的主体中,我们产生一个随机生成的颜色值。F# 中,我们使用 yield 结构,在 C# 中,我们使用 yield return.
如果编译 F# 版本的代码,你会在递归调用的这一行得到警告,说递归引用将在运行时检查。我们在第 8 章中讨论过这类警告。它通知我们,正在引用的值在其自己的定义之内。这里的代码是正确的,因为递归调用将在序列完全初始化之后进行。
清单 12.5 显示,当 F# 代码括在 seq 块中的时候,会使用一种不同的缩进样式。没有开始新行、缩进整个主体,我们添加了 seq 标识符,并行尾加个大括号。在书中的一些(程序)清单,我们都使用这种方法,使代码更紧凑。在实践中,这两种选项均有效,你可以选择一种,以使更具可读性为准。
现在,我们有一个颜色的无穷序列,我们就用它。清单 12.6 演示了无穷序列如何更好地分隔相邻。这里只有 F# 代码,但 C# (非常类似) 的实现在本书的网站可以找到。
Listing 12.6 Drawing a chart using sequence of colors (F#)
open System.Drawing
open System.Windows.Forms
let dataSource = [ 490; 485; 450; 425; 365; 340; 290; 230; 130; 90; 70; ]
let coloredSequence = Seq.zip dataSource randomColors
let coloredData = coloredSequence |> List.ofSeq
let frm = new Form(ClientSize = Size(500, 350))
frm.Paint.Add(fun e –>
e.Graphics.FillRectangle(Brushes.White, 0, 0, 500, 350)
coloredData |> Seq.iteri(fun i (num, clr) –>
use br = new SolidBrush(clr)
e.Graphics.FillRectangle(br, 0, i * 32, num, 28) )
)
frm.Show()
若要提供一个简短而完整的示例,我们只手工定义了几个数字数据。我们使用 Seq.zip 函数,把随机生成的颜色组合起来。这个函数取两个序列,返回一个元组序列:每个元组的第一个元素来自第一个序列,第二个元素来自第二个序列。在我们的示例中,这意味着,每个元组包含的数字分别来自数据源和随机生成的颜色。所返回序列的长度是两个给定序列中短序列的长度,所以,它将为每个数值生成一个随机颜色,然后停止。就是说,我们只需要有限数量的颜色。即,我们可以生成一百种颜色,但是,如果有人给我们 101 种呢?无穷序列给我们以优雅的方式解决了这个问题,而无需担心长度。
使用序列之前, 我们需要把它转换为列表。这样做是因为,随机颜色的序列不纯,当每次重新计算它时,返回不同的颜色。就是说,如果我们在使用之前,没有把它转换成列表,在每次重绘窗体期间,会得到不同的颜色。一旦我们有了颜色的数据列表时,需要遍历它的元素,并绘制。我们使用 Seq.iteri 函数,为每个元素调用指定的函数,把序列中元素的索引和元素本身传递给它。我们使用元组模式立即把元素分解成数值 (栏的宽度) ,并生成颜色。
本示例之所以有趣,是我们可以方便地使用另一种方式生成颜色。如果我们实现它,颜色可以在绘图函数中计算,这可能会使更改已经使用的颜色相对困难。但是,清单 12.6 中的解决方案完全把颜色生成代码从绘图代码中分离出来,所以,我们可以更改图表绘制的方式,只需要提供不同的颜色序列。清单 12.7 显示另一种配色方案。
Listing 12.7 Generating a sequence with color gradients (C# and F#)
// C# version using loops
IEnumerable<Color> GreenBlackColors() {
while(true) {
for(int g = 0; g < 255; g += 25)
yield return Color.FromArgb(g / 2, g, g / 3);
}
}
// F# version using loop and recursion
let rec greenBlackColors = seq {
for g in 0 .. 25 .. 255 do
yield Color.FromArgb(g / 2, g, g / 3)
yield! greenBlackColors }
在 12.7 中的代码包含无限循环,实现使用了 while 循环和递归。在循环体中,我们生成一个渐变颜色,包含 10 种不同颜色。我们使用 for 循环生成的绿色组件,同样计算出蓝色和红色的组件。本示例还显示了,用于生成具有指定的步长的数字序列的 F# 语法。g 的值从 0 开始,每次迭代增加 25,直到值大于 250。图 12.1 显示了最终结果。
图 12.1 使用由颜色序列生成的渐变颜色绘制的图表
正如所看到的,无穷序列可以用于实际编程,因为它给我们提供了一种方法,可以很容易地把可能要在以后更改的代码部分,从中分解出来。从理论角度来看,无穷序列也很有意思。在 Haskell 中,它经常用来表示数值计算。
到此,我们已经讨论了创建序列的大部分。现在,我们要看一下处理序列的技术。
Haskell 中的无穷列表和 F# 中缓存的序列
正如我们在第 11 章中提到的,Haskell 完全使用延迟计算。我们已经看过 F# 中的 Lazy<'a> 可以在我们需要时模拟延迟计算值,序列使我们能够以同样的方式,模拟一些其他的 Haskell 结构。让我们看一个稍有晦涩的例子,只是为了找到一种能做什么的感觉。在 Haskell 中,我们可以写下面的代码:
let nums = 1 : [ n + 1 | n <- nums ]
只要我们把它译成 F#,你就能理解了。标准的 Haskell 中的函数式列表因为是延迟的(因为完全是),: 运算符对应于 F# 的 :: 运算符。方括号中的表达式返回列表中的所有数字,每次增加 1。在 F# 中,我们可以写相同的代码,使用序列表达式:
let rec nums =
seq { yield 1
for n in nums do yield n + 1 };;
代码构造了一个序列,从 1 开始,并以递归方式从序列取所有数字,每次递增 1。这意味着,返回的序列将包含数字 1,2,3,等等。F# 版本效率太差,因为在每次递归调用中,从第一元素开始构建一个新的序列。要计算出长度为 3 的序列,就会创建 nums 的实例,一个长度为 3,一个长度 2,另一个长度为 1。
在 F# 中,代码的惯用版本可能看起来不一样。就像在清单 12.4 中生成阶乘一样,我们可以实现一个工具函数,从给定数字生成序列。然后,我们以递归方式调用它,使用优化的 yield! 原操作。在 Haskell 中,情况就不同,因为计算的值被缓存了。这意味着,它不必从头开始重新计算序列。我们可以得到类似的行为,在 F# 中使用 Seq.cache 函数:
let rec nums =
seq { yield 1
for n in nums do yield n + 1 } |> Seq.cache;;
Seq.cache 函数将返回的序列,缓存了已经计算过的值,所以,此版本的代码执行会更为聪明。访问第 1000 元素,有缓存的版本要比原始的速度快大约 100 倍。组合缓存和序列表达式,给我们一些与面向数学的 Haskell 相同的表现力。然而,寻找 F# 惯用的解决方案,通常是更好的主意,比如,这里使用的 yield!。
在前一章中,我们简单演示了如何使用延迟值实现延迟列表。这种数据结构可以用来创建无穷数据结构,比如,从零开始的整数列表。这是可能的,因为每个元素的计算被推迟了:只当我们访问元素时,才计算它的值,并且,每次只关注一个元素的计算。
使用 seq<'a> 表示序列是相似的。该接口有一个方法 MoveNext,关注下一个计算的元素。序列可能是无窜的,就是说,MoveNext 方法始终能够计算出下一个元素,并永远不会返回 false (表示序列的末尾)。无穷序列听起来可能有点奇怪,但我们将看到,它可以是很有价值的,能有一种好方法分隔算法的不同部分,使代码更具可读性。
在第 4 章中,我们讨论过绘制图表。我们用随机颜色来填充每一个部分,这并不总是有最好的结果。你可以用一个无穷序列来表示图的颜色。在清单 12.5 中,我们首先生成随机颜色的序列,但很快就会看到有其他的选项。
Listing 12.5 Generating an infinite sequence of random colors in C# and F#
// C# version using loops
IEnumerable<Color> RandomColors(){
var rnd = new Random();
while(true) {
int r = rnd.Next(256), g = rnd.Next(256), b = rnd.Next(256);
yield return Color.FromArgb(r, g, b);
}
}
// F# version using recursion
let rnd = new Random()
let rec randomColors = seq {
let r, g, b = rnd.Next(256), rnd.Next(256), rnd.Next(256)
yield Color.FromArgb(r, g, b)
yield! randomColors }
两个实现都包含一个生成颜色的无限循环。在 C# 中,循环使用 while(true) 实现,函数式方法创建无限循环是使用。在无限循环的主体中,我们产生一个随机生成的颜色值。F# 中,我们使用 yield 结构,在 C# 中,我们使用 yield return.
如果编译 F# 版本的代码,你会在递归调用的这一行得到警告,说递归引用将在运行时检查。我们在第 8 章中讨论过这类警告。它通知我们,正在引用的值在其自己的定义之内。这里的代码是正确的,因为递归调用将在序列完全初始化之后进行。
清单 12.5 显示,当 F# 代码括在 seq 块中的时候,会使用一种不同的缩进样式。没有开始新行、缩进整个主体,我们添加了 seq 标识符,并行尾加个大括号。在书中的一些(程序)清单,我们都使用这种方法,使代码更紧凑。在实践中,这两种选项均有效,你可以选择一种,以使更具可读性为准。
现在,我们有一个颜色的无穷序列,我们就用它。清单 12.6 演示了无穷序列如何更好地分隔相邻。这里只有 F# 代码,但 C# (非常类似) 的实现在本书的网站可以找到。
Listing 12.6 Drawing a chart using sequence of colors (F#)
open System.Drawing
open System.Windows.Forms
let dataSource = [ 490; 485; 450; 425; 365; 340; 290; 230; 130; 90; 70; ]
let coloredSequence = Seq.zip dataSource randomColors
let coloredData = coloredSequence |> List.ofSeq
let frm = new Form(ClientSize = Size(500, 350))
frm.Paint.Add(fun e –>
e.Graphics.FillRectangle(Brushes.White, 0, 0, 500, 350)
coloredData |> Seq.iteri(fun i (num, clr) –>
use br = new SolidBrush(clr)
e.Graphics.FillRectangle(br, 0, i * 32, num, 28) )
)
frm.Show()
若要提供一个简短而完整的示例,我们只手工定义了几个数字数据。我们使用 Seq.zip 函数,把随机生成的颜色组合起来。这个函数取两个序列,返回一个元组序列:每个元组的第一个元素来自第一个序列,第二个元素来自第二个序列。在我们的示例中,这意味着,每个元组包含的数字分别来自数据源和随机生成的颜色。所返回序列的长度是两个给定序列中短序列的长度,所以,它将为每个数值生成一个随机颜色,然后停止。就是说,我们只需要有限数量的颜色。即,我们可以生成一百种颜色,但是,如果有人给我们 101 种呢?无穷序列给我们以优雅的方式解决了这个问题,而无需担心长度。
使用序列之前, 我们需要把它转换为列表。这样做是因为,随机颜色的序列不纯,当每次重新计算它时,返回不同的颜色。就是说,如果我们在使用之前,没有把它转换成列表,在每次重绘窗体期间,会得到不同的颜色。一旦我们有了颜色的数据列表时,需要遍历它的元素,并绘制。我们使用 Seq.iteri 函数,为每个元素调用指定的函数,把序列中元素的索引和元素本身传递给它。我们使用元组模式立即把元素分解成数值 (栏的宽度) ,并生成颜色。
本示例之所以有趣,是我们可以方便地使用另一种方式生成颜色。如果我们实现它,颜色可以在绘图函数中计算,这可能会使更改已经使用的颜色相对困难。但是,清单 12.6 中的解决方案完全把颜色生成代码从绘图代码中分离出来,所以,我们可以更改图表绘制的方式,只需要提供不同的颜色序列。清单 12.7 显示另一种配色方案。
Listing 12.7 Generating a sequence with color gradients (C# and F#)
// C# version using loops
IEnumerable<Color> GreenBlackColors() {
while(true) {
for(int g = 0; g < 255; g += 25)
yield return Color.FromArgb(g / 2, g, g / 3);
}
}
// F# version using loop and recursion
let rec greenBlackColors = seq {
for g in 0 .. 25 .. 255 do
yield Color.FromArgb(g / 2, g, g / 3)
yield! greenBlackColors }
在 12.7 中的代码包含无限循环,实现使用了 while 循环和递归。在循环体中,我们生成一个渐变颜色,包含 10 种不同颜色。我们使用 for 循环生成的绿色组件,同样计算出蓝色和红色的组件。本示例还显示了,用于生成具有指定的步长的数字序列的 F# 语法。g 的值从 0 开始,每次迭代增加 25,直到值大于 250。图 12.1 显示了最终结果。
![](http://hi.csdn.net/attachment/201111/13/0_13211735925r4r.gif)
图 12.1 使用由颜色序列生成的渐变颜色绘制的图表
正如所看到的,无穷序列可以用于实际编程,因为它给我们提供了一种方法,可以很容易地把可能要在以后更改的代码部分,从中分解出来。从理论角度来看,无穷序列也很有意思。在 Haskell 中,它经常用来表示数值计算。
到此,我们已经讨论了创建序列的大部分。现在,我们要看一下处理序列的技术。
Haskell 中的无穷列表和 F# 中缓存的序列
正如我们在第 11 章中提到的,Haskell 完全使用延迟计算。我们已经看过 F# 中的 Lazy<'a> 可以在我们需要时模拟延迟计算值,序列使我们能够以同样的方式,模拟一些其他的 Haskell 结构。让我们看一个稍有晦涩的例子,只是为了找到一种能做什么的感觉。在 Haskell 中,我们可以写下面的代码:
let nums = 1 : [ n + 1 | n <- nums ]
只要我们把它译成 F#,你就能理解了。标准的 Haskell 中的函数式列表因为是延迟的(因为完全是),: 运算符对应于 F# 的 :: 运算符。方括号中的表达式返回列表中的所有数字,每次增加 1。在 F# 中,我们可以写相同的代码,使用序列表达式:
let rec nums =
seq { yield 1
for n in nums do yield n + 1 };;
代码构造了一个序列,从 1 开始,并以递归方式从序列取所有数字,每次递增 1。这意味着,返回的序列将包含数字 1,2,3,等等。F# 版本效率太差,因为在每次递归调用中,从第一元素开始构建一个新的序列。要计算出长度为 3 的序列,就会创建 nums 的实例,一个长度为 3,一个长度 2,另一个长度为 1。
在 F# 中,代码的惯用版本可能看起来不一样。就像在清单 12.4 中生成阶乘一样,我们可以实现一个工具函数,从给定数字生成序列。然后,我们以递归方式调用它,使用优化的 yield! 原操作。在 Haskell 中,情况就不同,因为计算的值被缓存了。这意味着,它不必从头开始重新计算序列。我们可以得到类似的行为,在 F# 中使用 Seq.cache 函数:
let rec nums =
seq { yield 1
for n in nums do yield n + 1 } |> Seq.cache;;
Seq.cache 函数将返回的序列,缓存了已经计算过的值,所以,此版本的代码执行会更为聪明。访问第 1000 元素,有缓存的版本要比原始的速度快大约 100 倍。组合缓存和序列表达式,给我们一些与面向数学的 Haskell 相同的表现力。然而,寻找 F# 惯用的解决方案,通常是更好的主意,比如,这里使用的 yield!。