首页 > 代码库 > 7.3.1 转换表示

7.3.1 转换表示

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。

7.3.1 转换表示