11.4.1 无穷列表
这一节的标题可能听起来有点奇怪(或疯了),所以,我们会提供一个字面解释。我们用得很多的数据结构之一是函数式列表。我们也可能想要表示逻辑上无穷列表,例如,所有质数的列表。事实上,我们不会使用所有的数字,但可能会处理这样的数据结构,而不考虑长度。如果列表是无穷的,我们就能够访问我们需要尽可能多的数字。
除了数学上的挑战,同样的概念在许多主流编程中也是有用的。当我们在第 4 章画饼图时,用的是随机颜色,但是,如果使用以无限列表方式生成颜色,使得图表外观清晰。在下一章中,我们将会看到所有这些例子,但现在,我们来向你展示如何将这种想法表示成使用延迟值。
在内存中存储无穷数字列表,看起来是一个棘手的问题。很明显,我们不能完整地存储这种数据结构,所以,我们需要存储它的一部分,其余部分表示为一个延迟计算。如我们已经看到的,延迟值是一个好方法,表示延迟计算的部分。
我们可以表示简单的无穷列表,其方式类似于普通列表。它是一个单元格,它包含一个值和列表中的其余部分。唯一不同的是,列表的其余部分将延迟计算,当我们执行它的时候,它给出另一个单元格。在 F# 中,我们可以使用差别联合来表示这种列表,如清单 11.19 所示。
Listing 11.19 Infinite list of integers (F#)
type InfiniteInts =
| LazyCell of int *
Lazy<InfiniteInts>
这个差别联合只有一个识别器,这意味着,它是类似于记录。我们可以用记录来写这段代码,但对于这个示例,用差别联合更方便,因为,我们可以用优美的模式匹配的语法,来提取其携带的值。这个唯一的识别器被称为延迟单元(LazyCell),它在单元格存储当前值,以及对"尾"的引用。尾是一个延迟值,所以,它将在需求时进行计算。用这种方式,当我们计算单元格时,可以逐个列表单元格进行,结果会被缓存。
F# 和 Haskell 中的延迟列表
如前所述,Haskell 到处使用延迟计算。这意味着,在 Haskell 中,标准的列表类型自动就是延迟的。尾直到这个值在代码中访问时才计算。
在 F# 中,延迟列表的使用并不非常频繁。在下一章,我们将会看到一种更优雅的写无穷集合,用 F#,也用 C# 2.0。F# 提供了延迟列表的一种实现,相似于我们在这一节实现的。你可以在 LazyList FSharp.PowerPack.dll 库中找到 LazyList<'a>。
现在,我们已经有了自己的类型,让我们用它来创建一个简单的无穷列表,存储整数0、 1、 2、 3 … …。清单 11.20 也演示如何从这个列表中访问值。
Listing 11.20 Creating a list containing 0, 1, 2, 3, 4, … (F# Interactive)
> let rec numbers(num) =
LazyCell(num, lazy numbers(num + 1));;
val numbers : int –> InfiniteInts
> numbers(0);;
val nums : InfiniteInts = LazyCell(0, Value is not created.)
> let next(LazyCell(hd, tl)) =
tl.Value;;
val next : InfiniteInts –> InfiniteInts
> numbers(0) |> next |> next |> next |> next |> next;;
val nums : InfiniteInts = LazyCell(5, Value is not created.)
我们首先写一个递归函数 numbers,它返回整数的无穷列表,从作为参数值给定的数字开始,直到无穷。返回的单元格包含第一个值和一个尾。这个尾是延迟值,在以递归方式调用 numbers 时,得到下一个单元格。
如果我们以 0 作为参数值,调用这个函数,得到从 0 开始的无穷列表,F# Interactive 的输出并不具特别可读性,但你可以发现,第一个值是 0,尾是 <InfiniteInts> 类型的延迟值。随后的命令声明函数 next,给出列表中下一个单元格。我们用声明中的模式匹配来分解唯一的参数值。这看起来有点怪,因为你通常不使用只有一个识别器的差别联合,但其原理与分解元组的组件是相同。在函数体中,我们读取 Value 属性,计算下一个单元格。最后一行使用 next 函数几次,从列表中读第六次值。
我们可以用延迟列表做更多的事情,但是,在这里我们不会更深入进去,在下一章,我们会看到更多 F# 常用的技术。有些情况下,LazyList<'T> 类型是很有用的。虽然我们没有直接使用 F# 类型库,使用它也不会有问题,现在你理解了原理。
在这一节介绍无穷数据结构中,我们一直专注于更多的函数风格,而没有展示 C# 示例。现在,它有可能在 C# 中写相同的类型,我们知道如何在 C# 中写延迟值,但在下一章中,我们将会看到更自然的方式在 C# 中表示无穷结构,或者流的值。
在此示例中,延迟列表有一个非常有趣的地方。一旦我们计算列表到一些点,计算的值在内存中仍可用,我们不必要每次重新计算。正如我们在下一节中将要看到,延迟值的这方面可以用于很简单而优雅的缓存机制。
写处理无穷列表的函数
当使用标准的列表类型时,我们可以使用的函数,如 List.map 和 List.filter。我们也可以为无穷列表实现相同的函数,但是,当然,并非所有的都可以。例如,List.fold 和 List.sort 需要读所有元素,对于延迟列表,这是不可能的。这里的一个例子,说明什么是可能的,它实现了 map 函数:
let rec map f (LazyCell(hd, tl)) =
LazyCell(f(hd), lazy map f tl.Value)
其结构类似于正常的 map 函数。它把给定的函数应用到单元格中的第一个值,然后,以递归方式处理列表中的其余部分。尾的处理由于使用 lazy 关键字而延迟。其他常见的列表处理函数也类似。
这一节的标题可能听起来有点奇怪(或疯了),所以,我们会提供一个字面解释。我们用得很多的数据结构之一是函数式列表。我们也可能想要表示逻辑上无穷列表,例如,所有质数的列表。事实上,我们不会使用所有的数字,但可能会处理这样的数据结构,而不考虑长度。如果列表是无穷的,我们就能够访问我们需要尽可能多的数字。
除了数学上的挑战,同样的概念在许多主流编程中也是有用的。当我们在第 4 章画饼图时,用的是随机颜色,但是,如果使用以无限列表方式生成颜色,使得图表外观清晰。在下一章中,我们将会看到所有这些例子,但现在,我们来向你展示如何将这种想法表示成使用延迟值。
在内存中存储无穷数字列表,看起来是一个棘手的问题。很明显,我们不能完整地存储这种数据结构,所以,我们需要存储它的一部分,其余部分表示为一个延迟计算。如我们已经看到的,延迟值是一个好方法,表示延迟计算的部分。
我们可以表示简单的无穷列表,其方式类似于普通列表。它是一个单元格,它包含一个值和列表中的其余部分。唯一不同的是,列表的其余部分将延迟计算,当我们执行它的时候,它给出另一个单元格。在 F# 中,我们可以使用差别联合来表示这种列表,如清单 11.19 所示。
Listing 11.19 Infinite list of integers (F#)
type InfiniteInts =
| LazyCell of int *
Lazy<InfiniteInts>
这个差别联合只有一个识别器,这意味着,它是类似于记录。我们可以用记录来写这段代码,但对于这个示例,用差别联合更方便,因为,我们可以用优美的模式匹配的语法,来提取其携带的值。这个唯一的识别器被称为延迟单元(LazyCell),它在单元格存储当前值,以及对"尾"的引用。尾是一个延迟值,所以,它将在需求时进行计算。用这种方式,当我们计算单元格时,可以逐个列表单元格进行,结果会被缓存。
F# 和 Haskell 中的延迟列表
如前所述,Haskell 到处使用延迟计算。这意味着,在 Haskell 中,标准的列表类型自动就是延迟的。尾直到这个值在代码中访问时才计算。
在 F# 中,延迟列表的使用并不非常频繁。在下一章,我们将会看到一种更优雅的写无穷集合,用 F#,也用 C# 2.0。F# 提供了延迟列表的一种实现,相似于我们在这一节实现的。你可以在 LazyList FSharp.PowerPack.dll 库中找到 LazyList<'a>。
现在,我们已经有了自己的类型,让我们用它来创建一个简单的无穷列表,存储整数0、 1、 2、 3 … …。清单 11.20 也演示如何从这个列表中访问值。
Listing 11.20 Creating a list containing 0, 1, 2, 3, 4, … (F# Interactive)
> let rec numbers(num) =
LazyCell(num, lazy numbers(num + 1));;
val numbers : int –> InfiniteInts
> numbers(0);;
val nums : InfiniteInts = LazyCell(0, Value is not created.)
> let next(LazyCell(hd, tl)) =
tl.Value;;
val next : InfiniteInts –> InfiniteInts
> numbers(0) |> next |> next |> next |> next |> next;;
val nums : InfiniteInts = LazyCell(5, Value is not created.)
我们首先写一个递归函数 numbers,它返回整数的无穷列表,从作为参数值给定的数字开始,直到无穷。返回的单元格包含第一个值和一个尾。这个尾是延迟值,在以递归方式调用 numbers 时,得到下一个单元格。
如果我们以 0 作为参数值,调用这个函数,得到从 0 开始的无穷列表,F# Interactive 的输出并不具特别可读性,但你可以发现,第一个值是 0,尾是 <InfiniteInts> 类型的延迟值。随后的命令声明函数 next,给出列表中下一个单元格。我们用声明中的模式匹配来分解唯一的参数值。这看起来有点怪,因为你通常不使用只有一个识别器的差别联合,但其原理与分解元组的组件是相同。在函数体中,我们读取 Value 属性,计算下一个单元格。最后一行使用 next 函数几次,从列表中读第六次值。
我们可以用延迟列表做更多的事情,但是,在这里我们不会更深入进去,在下一章,我们会看到更多 F# 常用的技术。有些情况下,LazyList<'T> 类型是很有用的。虽然我们没有直接使用 F# 类型库,使用它也不会有问题,现在你理解了原理。
在这一节介绍无穷数据结构中,我们一直专注于更多的函数风格,而没有展示 C# 示例。现在,它有可能在 C# 中写相同的类型,我们知道如何在 C# 中写延迟值,但在下一章中,我们将会看到更自然的方式在 C# 中表示无穷结构,或者流的值。
在此示例中,延迟列表有一个非常有趣的地方。一旦我们计算列表到一些点,计算的值在内存中仍可用,我们不必要每次重新计算。正如我们在下一节中将要看到,延迟值的这方面可以用于很简单而优雅的缓存机制。
写处理无穷列表的函数
当使用标准的列表类型时,我们可以使用的函数,如 List.map 和 List.filter。我们也可以为无穷列表实现相同的函数,但是,当然,并非所有的都可以。例如,List.fold 和 List.sort 需要读所有元素,对于延迟列表,这是不可能的。这里的一个例子,说明什么是可能的,它实现了 map 函数:
let rec map f (LazyCell(hd, tl)) =
LazyCell(f(hd), lazy map f tl.Value)
其结构类似于正常的 map 函数。它把给定的函数应用到单元格中的第一个值,然后,以递归方式处理列表中的其余部分。尾的处理由于使用 lazy 关键字而延迟。其他常见的列表处理函数也类似。