4.4.2.3 用函数绘图
DrawStep 函数的第一个参数是两个绘图函数中的一个,我们暂时先给这个绘图函数的类型命名为 DrawingFunc,以后再定义。在讨论其余的参数之前,我们先看一下这个函数的签名:
drawStep : (DrawingFunc * Graphics * float* (string * int) list) -> unit
我们再次使用元组语法来指定参数,因此,函数的参数为一个大元组;第二个参数是用来绘图的Graphics 对象,它会传递给绘图函数;接下来的两个参数描述绘图用的数据集,一个 float 值是所有数值的和,这样,就可以计算出饼图每个部分的角度,另一个 (string * int) list 类型的值,是我们熟悉的数据集,来自控制台应用程序,保存了绘图用的每一项的标签和值。
我们看一下 DrawingFunc 类型,它和清单 4.8 中的 drawPieSegment 函数有相同的签名;第二个绘图函数是 drawLabel,马上就会发现,它也有完全相同的签名。我们可以看看这个签名,声明的 DrawingFunc 类型,与这个两函数的类型完全相同:
drawPieSegment : (Graphics * string * int *int) -> unit
drawLabel : (Graphics * string * int * int)-> unit
type DrawingFunc = (Graphics * string * int* int) -> unit
最后一行是类型声明,声明了一个类型别名(type alias),这样,我们就为复杂类型指定一个名字,可以换一种方式来写。我们只在这个解释中使用 DrawingFunc 的名字,但实际可以在类型批注中使用,比如,我们想要引导类型推断,或都使代码更具可读性。
正如我们前面所说的,不需要在代码中写出这些类型,但是,定出来能帮助我们理解代码的功能。最重要的是,我们已知道 drawStep 函数第一个参数为绘图函数。清单 4.9 是 drawStep 函数的代码。
清单 4.9 使用指定的绘图函数绘制所有项 (F#)
let drawStep(drawingFunc, gr:Graphics, sum,data) =
letrec drawStepUtil(data, angleSoFar) = [1]
matchdata with
|[] –> () [2]
|[title, value] –> [3]
letangle = 360 – angleSoFar <-- 计算角度到 360 度
drawingFunc(gr,title, angleSoFar, angle)
|(title, value)::tail –> [4]
letangle = int(float(value) / sum * 360.0)
drawingFunc(gr,title, angleSoFar, angle)
drawStepUtil(tail,angleSoFar + angle) <-- 递归绘制其余部分
drawStepUtil(data,0) <-- 运行工具函数
为使代码更具可读性,我们把这个函数实现为嵌套函数[1],它遍历应该画在图表上的所有项。这些项保存在标准的 F# 列表中,因此,代码很像我们熟悉的列表处理模式。有一个显著的区别,因为这个列表匹配三种模式,而不是通常的两种情况的匹配,一个空列表和一个 cons cell。
模式匹配的第一个分支[2],匹配空列表,且不执行任何动作。我们已经知道,在 F# 中“什么也不做”用unit 值表示,因此,返回 unit 的代码,写作 ()。这是因为 F# 把每个构造都看作表达式,且表达式必须有返回值。如果空列表的分支为空,就不是有效的表达式。
第二个分支[3]的列表处理代码不同平常。可以看到,这个分支中所使用的模式是 [title, value]。这是一个由两个模式组成的嵌套模式,一个模式匹配只包含一个项目 [it] 的列表,另一个模式匹配包含两个元素(title, value) 的元组项,简写的语法为 [(title, value)],但它们意思相同。第一个模式使用通常的语法创建列表,因此,如果要写匹配有三项列表的模式,可以写成 [a; b; c]。我们包括了这种特殊情况,因为我们想要纠正舍入误差:即,处理列表最后一项时,要确保总角度正好是 360 度。在此分支中,我们只计算角度,然后调用 drawingFunc 函数时,把它作为参数值。
最后一个分支处理的列表不匹配前面的两种模式。在这里,模式的顺序非常重要,因为任何匹配第二种模式[3]的列表也能匹配最后一种模式[4],只是列表的尾为空而已。在代码中模式的顺序保证最后一项不会调用最后的分支。
最后分支的代码计算角度,并使用专门的绘图函数绘制饼图的一部分。这是递归处理列表的唯一分支,因为它一直使用到列表中只有最后一个元素为止,所以,代码的最后一行是递归调用。在递归过程中唯一改变的参数值,是列表中要绘制的剩余元素,和 angleSoFar,这是所有已处理的饼图部分占的角度。由于使用了局部函数,我们不需要传递不会改变的其它参数值。drawStep 函数本身只做一件事是:调用工具函数,用所有数据,把参数 angleSoFar 设置为 0。
绘制整个图表