3.3.2 介绍函数式列表

来源:互联网 发布:淘宝横向布置 编辑:程序博客网 时间:2024/05/20 20:14

3.3.2 介绍函数式列表

 

    现在,使用递归的一般原则更舒适,那我们就详细地看看函数式列表。刚才我们提到过,列表既可以是空的,也可以由一个元素与另一个列表组成。这意味着,我们需要一个特殊的值来表示空的列表,还有一种构建列表的方法,通过取一个现有列表,并在它的前面加上一个元素。第一个选项(空列表)有时称为 nil,第二个选项产生 cons cell(是构造的列表单元格缩写,constructed list cell)。你可以在图 3.1 中看到示例列建表,由一个空列表和 cons cell 构成。

 

3-1

图 3.1 一个函数式包含列表 6、 2、 7 和 3。矩形表示 cons cell,它包含一个值和对列表其余部分的引用。最后的cons cell 引用一个特殊值,表示一个空列表。

 

    正如你在图 3.1 中所看到的,每个 cons cell 存储这个列表单个值(称为头),和对列表其余部分(称为尾)的引用,它既可以是另一个 cons cell,也可以是一个空列表(nil)。让我们现在看看 F# 提供的创建列表的几种方法:

 

> let ls1 = []val ls1 : 'a list = []> let ls2 = 6::2::7::3::[]val ls2 : int list = [6; 2; 7; 3]> let ls3 = [6; 2; 7; 3]val ls3 : int list = [6; 2; 7; 3]> let ls4 = [1 .. 5]val ls4 : int list = [1; 2; 3; 4; 5]> let ls5 = 0::ls4val ls5 : int list = [0; 1; 2; 3; 4; 5]

    首先,创建了一个空列表,在 F# 中写作 []。如果你看看结果,可以看到 F# 创建一个不包含元素的值。列表的类型是有点不清楚,因为,现在还不知道列表中包含的值的类型,F# 推断该列表的类型是"某些东西",这称为泛型值,我们将在第 5 章讨论它。

    第二个示例更加有趣。你可以看到如何根据内容创建列表:我们取一个空列表,并使用一种创建 cons cell 的语法 ::,不像(一般的)运算符,例如 +,:: 结构是右关联的,这意味着,它从右至左组合值。如果你按这个方向读这个表达式,可以看到我们用 3 和一个空列表,构建了一个列表单元;然后,用 7 和这个结果,构建另一个单元,等等。输入这个表达式以后,F# Interactive 报告,创建了一个 int list 类型的列表。这表示 ls2 值的类型是一个包含整数列表。这又是通过泛型类型完成的,可能从 C# 中得知(将详细讨论如何在 F# 中使用它们,在稍后的时间)。

    在接下来的两个示例中,我们使用了一块 F# 提供的创建列表的语法糖。第一个使用方括号,列表元素以分号分隔;第二个使用点-点来创建包含一个数字序列的列表。

    最后一个例子显示了如何使用 cons cells 创建一个列表,通过在另一个列表的开头追加值。可以看到 ls5 包含 0 开头,后面的所有元素都来自 ls4 列表中。

    有关函数式列表的一个重要事实就是它的们不可变。这意味着,我们可以构建一个列表(如在前面的示例中一样),但不能取得现有的列表并修改;不能添加或删除元素。需要添加新元素或删除现有元素的函数,总是返回一个新的列表,而不修改原始列表,因为,修改列表其实是不可能的。我们将在第 5、 6 和 10 章中看到更多有关这些函数的示例,但现在,让我们看看如何处理现有的列表中的元素。

    在函数语言使用列表,处理列表的典型代码包含两个分支:

    ■ 当给定的列表是空的,执行的操作

    ■ 当参数值是 cons cell,执行的操作

    后一个分支通常执行一个计算,使用头,并用这个列表的尾进行递归处理。在这一章的后面,我们将会看到所有这些通用模式,但首先,让我们研究一下如何编写代码,使用模式匹配在这两个分支之间进行选择。

 

用模式匹配分解列表

 

    在 3.2.4 节中,我们在讨论有关元组的匹配模式时,看到两和不同的方法。一种方法是直接在 let 绑定中写模式,即可以是把表达式的结果赋给一个值或,也可以是在函数参数的声明中;另一种方法是使用 match 关键字。两者的重要区别在于,使用 match,我们可以指定多个模式,有多个分支。对于列表,我们需要使用第二个方法,因为,在写列表处理代码时,每次需要指定两个不同的分支,(一个用于空列表,另一个用于创建使用 cons cell)。

    下面的代码演示了对列表的模式匹配,打印一条消息,第一个元素的值,或者该列表为空时,则“Empty list”:

 

match list with| [] -> printfn "Empty list"| head::tail -> printfn "Starting with %d" head
 

    可以看到,在第二行的模式匹配空列表,在第三行的模式,提取头(第一个元素的值)和尾(头后面的列表)。编写这两种模式的语法与较早时用于创建列表的语法完全相同。一个空列表用 [] 来匹配,cons cell 使用:: 模式分解。第二种情况更有趣,因为,它把一个值赋给两个新的符号,head 和 tail。将包含一个数字,通过分解第一个 cons cell 获得的列表中的其余部分。空列表不含任何值,因此,第一个模式不绑定值到任何符号,它只会通知我们原始列表是空的。

    如果参考图 3.1,就会发现,第一个模式对应于 nil 的椭圆,不包含任何值。第二个模式匹配 cons cell 矩形,带出两个部分的内容。

    正如在元组的示例中,模式列表是完整的,这意味着,它必须选择任意给定的列表中的一个分支。如果我们尝试使用不完整的模式,看看会发生什么。

 

Listing 3.13 An incomplete pattern matching on lists (F# Interactive)

 

> let squareFirst list =match list with| head::_ -> head * head;;Warning FS0025: Incomplete pattern matches on thisexpression. The value '[]' will not be matched.val sqareFirst : int list –> int
> squareFirst [4; 5; 6];;val it : int = 16
> squareFirst []Exception of type 'Microsoft.FSharp.Core.MatchFailureException' was thrown.(...)

 

    我们首先声明一个函数 squareFirst,包含一个模式匹配,只匹配 cons cell,并返回列表中的第一个元素的平方。然而,当列表为空时,此模式不会处理这种情况。我们可以看到,F# 编译器是很聪明的,当我们写的模式匹配可能会失败时,能检测到这种情况,甚至给我们这个匹配将会失败的示例。不要忽略此警告,除非你绝对肯定这种情况永远不会发生。喟然,这个函数对于空列表来说,没有任何合理的意义,但是,最好还是添加一个针对剩余情况的处理程序(这种模式可以使用下划线字,表示匹配任意值),既可以触发有额外信息的异常,也可什么都不做。当然,如果这个函数的返回类型是除 unit 以外的其他任何类型,都必须给出一个合适的值返回,即使你什么也不做。 如果函数真的不应该用空列表调用,那么,抛出异常通常是一个更好的主意。

    尽管有一个警告,F# Interactive 还是愿意咬牙处理这个函数的,因此,我们可以尝试调用它。首先,尝试一和可以工作的情况,我们会看到,它行为与预期相同。如果用空列表作为参数值调用函数,match 构造不包含任何匹配的模式,所以,它将触发异常。这是一个通常的 .NET 异常,在 F# 中可以使用 try 结构造抓取。

    你应该想到,我们会从函数式列表中期望得到什么,因此,在下一节中,我们会把注意力转到 C# ,并使用它来详细解释列表。我们还会写出第一个列表处理代码。

原创粉丝点击