7.3.1 转换表示
在我们实现数的据类型之间,存在两个关键的不同:
[两个不同,怎么出现三项]
1、在新的表示形式中,文件是一个(递归)值,而在第一种情况下,是元素的列表。
2、第 7.2 节的数据类型显式包含边框,指定内容的位置。
3、第二个数据类型,只表示各部分是如何嵌套的。
因此,当我们进行表示形式的转换时,需要计算每个嵌套部分的位置。
这些差异影响转换函数的签名,在我们实现之前先分析一下:
val documentToScreen : DocumentPart * Rect-> ScreenElement list
这个函数取的第一个参数是转换的文档部分,返回第 7.2 节中ScreenElement 值的列表,这样,输入的参数值和结果都能表示整个文档;函数的第二个参数,指定整个文档的边框,在转换期间,我们将需要用它来计算每个部分的位置。清单 7.10 的实现,使用了(毫不奇怪)递归函数。
清单 7.10 在文档的表示形式之间进行转换 (F#)
let rec documentToScreen(doc, bounds)=
match doc with
| SplitPart(Horizontal, parts)–> [1]
let width = bounds.Width/ (float32(parts.Length)) <-- 计算每一部分的大小
parts
|>List.mapi (fun i part -> | [2]
let left = bounds.Left + float32(i) * width |
let bounds = { bounds with Left = left; Width = width } |
documentToScreen(part, bounds)) |
|>List.concat [3]
| SplitPart(Vertical, parts)–> [4]
let height =bounds.Height / float32(parts.Length)
parts
|>List.mapi (fun i part ->
let top = bounds.Top + float32(i) * height | 计算行边框
let bounds = { bounds with Top = top; Height = height } |
documentToScreen(part, bounds)) <-- 递归处理元素
|>List.concat
| TitledPart(tx, content)-> [5]
let titleBounds = {bounds with Height = 35.0f }
let restBounds = {bounds with Height = bounds.Height - 35.0f;
Top = bounds.Top + 35.0f }
let convertedBody =documentToScreen(content, restBounds) <-- 转换正文,加标题
TextElement(tx,titleBounds)::convertedBody
| TextPart(tx) -> [TextElement(tx, bounds) ] | [6]
| ImagePart(im) -> [ImageElement(im, bounds) ] |
我们先从代码的后面开始看,处理表示内容的部分很容易[6],因为只返回包含一个屏幕元素的列表。我们可以使用提供的矩形作为参数值,表示位置和大小,无需更多的计算。
其余部分是由其他部分组成的。在这种情况下,函数递归地调用自己,处理构成更大部分的所有子部分。这是必须进行布局计算的地方,因为当我们再次调用 documentToScreen 时,用子部分和子部份的边框作为参数。我们不能复制 bounds 参数,否则,所有的子部分会在同一地方终结!相反,我们把矩形划分为更小的矩形,每个子部分一个。
TitledPart [5]包含一个子部分,因此,只要进行一次递归调用。在此之前,我们已经计算了标题的边框(前面 35 个像素),和正文的边框(除了前面 35 个像素之外的一切)。接下来,我们递归地处理正文,添加表示标题的 TextElement 到要返回的屏幕元素列表中。
处理 SplitPart,两个方向使用单独的分支[1][4],计算每行或列的大小,并转换它的所有部件。 我们使用 List.mapi 函数[2],它很像 List.map,但它同时提供了当前正在处理部分的索引,可以使用这个索引来计算更小部分边框相对主矩形左上角的偏移量。这个 Lambda 函数然后递归调用 documentToScreen,返回每个文档部件对应屏幕元素的列表。这样,我们得到List.mapi 映射结果列表的列表,结果的类型是 list<list<ScreenElement>>,不是我们需要返回的平面列表,因此,需要使用标准的 F# 库函数List.concat[3],把结果变成list<ScreenElement> 类型的值。
注意
在文档的不同表示形式之间进行转换,是这一章的难点,因此,可能想要下载源代码,并体验看它是如何运行的。最重要(也是最困难)的部分是每次递归调用计算边框。有必要确保能理解函数返回的列表,以及它如何从每次更深的递归调用建立的。我们会发现,用铅笔和纸张完整地走一遍,跟踪矩形边框和返回屏幕元素,是有用的。
不同的表示形式之间的转换,通常是简化函数式编程的关键,因为它能够为实现其他操作的每个部分,找到最适合这种情况的数据结构。我们知道,第一种表示形式适合绘制文档,而第二种形式使构造更简单,同时操作更容易,我们将在 7.4 节讨论。在此之前,我们要介绍另一种表示:XML。