在前一章,我们简单演示过使用延迟值,实现延迟列表。这种数据结构可以用来创建无穷数据结构,比如,从零开始的整数列表。这之所以可能,是因为每个元素的计算被推迟了:只在访问元素时,才计算它的值,并且,每次只关注一个元素的计算。
使用seq<‘a> 表示序列是相似的。该接口有一个方法MoveNext,计算出下一个的元素。序列可能是无穷的,即,MoveNext 方法始终能够计算出下一个元素,并永远不会返回false (表示序列结束)。无穷序列听起来可能有点奇怪,但我们将看到,它可能很有价值,把算法划分成不同部分,使代码更具可读性。
在第四章,我们讨论过图表的绘制,使用随机颜色填充每一个部分,但这并不总是有最好的结果。我们可以用无穷序列来表示图的颜色。在清单12.5 中,我们首先生成随机颜色的序列,很快就会看到有其他的方法。
清单12.5 在C# 和F# 中生成随机颜色的无穷序列
// C# version using loops
IEnumerable<Color> RandomColors(){
varrnd = new Random();
while(true){ [1]
intr = rnd.Next(256), g = rnd.Next(256), b = rnd.Next(256);
yieldreturn Color.FromArgb(r, g, b); [2]
}
}
// F# version using recursion
let rnd = new Random()
let rec randomColors = seq { [3]
letr, g, b = rnd.Next(256), rnd.Next(256), rnd.Next(256)
yieldColor.FromArgb(r, g, b) [4]
yield!randomColors } [5]
两个实现都包含生成颜色的无限循环。在C# 中,循环使用while(true) 实现[1];函数式方法创建无限循环是使用递归[5]。在无限循环的主体中,我们产生一个随机生成的颜色值。在F# 中,我们使用yield 结构[4],在C# 中,我们使用yield return[2]。
如果编译F# 版本的代码,在递归调用的这一行会有警告[5],说递归引用将在运行时检查。我们在第八章讨论过这种警告,是通知我们,正在引用的值在自己的定义之内。在这里,代码是正确的,因为递归调用将在序列完全初始化之后进行。
清单12.5 在把F# 代码括在seq 块中的时候,使用了不同的缩进样式[3]。没有从新行开始,缩进整个主体,而是把seq 标识符,和左大括号放在行尾。在本书中的一些(程序)清单中,我们会使用这种方法,使代码更紧凑。在实践中,这两种方法均正确,可以自由选择一种,以使更具可读性为准。
现在,我们有了颜色的无穷序列,就可以使用了。清单12.6 表明,无穷序列能够更好地分隔相信部分。这里只有F# 代码,C# 版本(非常类似)在本书的网站可以找到。
清单12.6 使用颜色序列绘制图表(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 [1]
let coloredData = coloredSequence |> List.ofSeq [2]
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) –> [3]
usebr = new SolidBrush(clr)
e.Graphics.FillRectangle(br,0, i * 32, num, 28) ) <-- 用索引确定色块的位置
)
frm.Show()
为了提供简短而完整的示例,我们只手工定义了几个数字数据,使用Seq.zip 函数,把随机生成的颜色组合起来[1]。这个函数取的参数为两个序列,返回一个元组序列:每个元组的第一个元素来自第一个序列,第二个元素来自第二个序列。这样,在我们的示例中,每个元组包含的数字分别来自数据源和随机生成的颜色。所返回序列的长度,是两个给定序列中较短序列的长度,所以,它将为每个数值生成一个随机颜色,然后停止。就是说,我们只用到有限数量的颜色。那么,如果我们可以生成了一百种颜色,但是,有人给了我们101 个数字,怎么办呢?无穷序列优雅地解决了这个问题,而无需担心长度。
在使用序列之前, 我们要把它转换为列表[2]。这样做是因为,随机颜色序列不纯(pure。这是什么意思?),每次重新计算时,返回不同颜色。因此,如果我们在使用之前,没有把它转换成列表,在每次重绘窗体期间,会得到不同的颜色。如果我们有了颜色数据的列表,只需要遍历其中的元素,并绘制色块,就可以了。我们将使用Seq.iteri 函数[3],它为每个元素调用指定的函数,把元素在序列中的索引和元素本身传递给函数。我们使用元组模式,立即把元素分解成数字值(色块的宽度),并生成颜色。
本示例之所以有意义,是因为我们可以很方便地使用另一种方式生成颜色。如果我们na?vely (又出现这个字了。是正常的意思吗?)实现它,颜色在绘图函数中计算[3],这可能会使改变使用的颜色,相对困难。但是,清单12.6 中的解决方案把颜色生成代码,与绘图代码中完全分离开来,这样,只需要提供不同的颜色序列,我们就能改变图表绘制的方式了。清单12.7 显示另一种配色方案。
清单12.7 生成渐变颜色序列(C# and F#)
// C# version using loops
IEnumerable<Color> GreenBlackColors(){
while(true){ [1]
for(intg = 0; g < 255; g += 25)
yield return Color.FromArgb(g / 2, g, g / 3);
}
}
// F# version using loop and recursion
let rec greenBlackColors = seq {
forg in 0 .. 25 .. 255 do [2]
yieldColor.FromArgb(g / 2, g, g / 3)
yield!greenBlackColors } [3]
在12.7 中的代码再次包含了无限循环,使用while 循环[1]和递归[3]实现。在循环体中,我们生成了渐变颜色,包含10 种不同颜色。我们使用for 循环生成的绿色部分,并计算出蓝色和红色部分(三原色吧)。本示例还展示了,生成具有指定步长的、数字序列的F# 语法[2]。g 的值从0 开始,每次迭代增加25,直到值大于250 为止。图12.1 显示了最终结果。
图12.1 使用渐变颜色作为生成的颜色序列,绘制图表
可以发现,无穷序列在实际编程中非常有用,因为它能很容易地把可能要在以后更改的代码部分,从中分解出来。从理论角度来看,无穷序列也很有意义;在Haskell 中,它经常用来表示数值计算。
到此,我们已经讨论了创建序列的绝大部分内容,现在,要看一下如何处理序列了。
Haskell 中的无穷列表和 F# 的缓存序列
我们在第十一章提到,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!。