打开主菜单

Haskell/列表和元组

< Haskell

列表和元组是把多个值融为单个值的两种方法

列表编辑

函数式编程程序员的下一个好友编辑

上一章我们介绍了Haskell变量和函数的概念。 在Haskell程序中,函数是两个主要的构成块之一。另一个则是通用列表。那么干脆,我们切换到解释器里,并创建一些列表:

例子 - 在解释器中创建列表编辑

Prelude> let numbers = [1,2,3,4]
Prelude> let truths  = [True, False, False]
Prelude> let strings = ["here", "are", "some", "strings"]

方括号指出列表的开始和结束,逗号操作符","分隔列表元素。另外,列表元素必须是同一类型。因此, [42, "life, universe and everything else"] 不是一个正确的列表,因为它包含了两种不同类型的元素,也就分别是整型和字符串型。而 [12, 80] 或者, ["beer", "sandwiches"] 都是合法的列表,因为他们都是单一类型的。

如果定义一个含有多个类型元素的列表,就会发生这样的错误:

Prelude> let mixed = [True, "bonjour"]

<interactive>:1:19:
    Couldn't match `Bool' against `[Char]'
      Expected type: Bool
      Inferred type: [Char]
    In the list element: "bonjour"
    In the definition of `mixed': mixed = [True, "bonjour"]

如果你对列表和类型感到困惑,不用担心。我们还没有太多地讨论到类型,我们相信在以后的章节你会明白。

创建列表编辑

方括号与逗号并不是创建列表的唯一方式。使用(:)cons操作符,你可以逐步地把数据附加(consing[1])到一个列表上,从而创建另一个列表。

例子: 把数据附加(Cons)到列表

Prelude> let numbers = [1,2,3,4]
Prelude> numbers
[1,2,3,4]
Prelude> 0:numbers
[0,1,2,3,4]


当你附加数据到列表(something:someList)时,你得到的是另一个列表。那么,毫无悬念,这种构建方式是可以复合的。

例子: 附加(Cons)

Prelude> 1:0:numbers
[1,0,1,2,3,4]
Prelude> 2:1:0:numbers
[2,1,0,1,2,3,4]
Prelude> 5:4:3:2:1:0:numbers
[5,4,3,2,1,0,1,2,3,4]


事实上所有的列表都是在一个空的列表([])的基础上通过附加数据创建的。逗号与方括号的记法实际上是一种语法糖般的令人愉快的形式。 换句话说,[1,2,3,4,5]精确地等同于1:2:3:4:5:[]

你需要留意一类形如1:2的错误,你会得到如下错误。

例子: Whoops!

Prelude> 1:2

<interactive>:1:2:
    No instance for (Num [a])
      arising from the literal `2' at <interactive>:1:2
    Probable fix: add an instance declaration for (Num [a])
    In the second argument of `(:)', namely `2'
    In the definition of `it': it = 1 : 2


再一个例子True:False,但仍然错误

例子: 更简单但仍然错误

Prelude> True:False

<interactive>:1:5:
    Couldn't match `[Bool]' against `Bool'
      Expected type: [Bool]
      Inferred type: Bool
    In the second argument of `(:)', namely `False'
    In the definition of `it': it = True : False


可以看到(:)cons构建适用于something:someList这种形式,但当我们将其使用于something:somethingElse形式下时它就不适用了。可以看到(:)cons是如何依赖于列表的。 在这里我们开始接触到类型的概念。让我们来总结一下:

  • 列表中的元素必须有相同的类型
  • 只能附加(cons)数据到一个列表上

类型是不是很烦人?确实,但是我们可以从Haskell/类型基础可以看到它也可以为我们节约时间。当你以后使用Haskell编程时遇到了错误,你会习惯想到这很可能是一个类型错误。

练习
  1. 如下Haskell是否正确: 3:[True,False]?为什么?
  2. 写一个函数cons88添加到一个列表中。并进行如下测试:
    1. cons8 []
    2. cons8 [1,2,3]
    3. cons8 [True,False]
    4. let foo = cons8 [1,2,3]
    5. cons8 foo
  3. 写一个有两个参数的函数,一个参数为list,另一个为thing,把thing附加到list。你可以这样开始let myCons list thing =

列表组成的列表编辑

列表可以包含任意数据,只要它们都属于同一种类型。那么,接下来思考这样一个问题:列表也是数据,所以,列表可以包含……是的的确,其它列表!在解释器上尝试以下语句:

例子: 列表可以包含列表

Prelude> let listOfLists = [[1,2],[3,4],[5,6]] 
Prelude> listOfLists
[[1,2],[3,4],[5,6]]


有时列表组成的列表可能相当狡猾,因为一个列表所包含的单个数据并不和这个列表自身同属一种数据类型。让我们用几个练习来说明这个概念:

练习
  1. 下列哪些是正确的Haskell表达式,哪些不是?用cons记法重写。
    1. [1,2,3,[]]
    2. [1,[2,3],4]
    3. [[1,2,3],[]]
  2. 下列哪些是正确的Haskell表达式,哪些不是?用逗号和方括号记法重写。
    1. []:[[1,2,3],[4,5,6]]
    2. []:[]
    3. []:[]:[]
    4. [1]:[]:[]
  3. Haskell中可以创建包含由列表组成的列表的列表吗?为什么能或者为什么不能?
  4. 下面的列表为什么不正确?如果你还不能回答不要太烦恼。
    1. [[1,2],3,[4,5]]

列表组成的列表极其有用,因为它们允许你表达一些非常复杂的、结构化的数据(例如二维矩阵)。它们也是Haskell类型系统中众多真正的闪光点之一。人类程序员,或者至少这本Wikibook的作者,在使用列表组成的列表时,总是变得困惑;同时,有限制的类型经常帮助我们艰难地通过这些潜在的困难。

元组编辑

另一种多值记法编辑

元组是另一种把多个值储存到一个值的方式,但是它们与列表在某些方面有一些精巧的差异。若你事先知道你要储存多少个值为一组元组是十分有用的,其次元组对其中每一个值是分别进行类型限制的。举些例子,我们想用一个类型来表达平面坐标,现在我们知道要储存的值的个数为两个(xy),所以在这个例子中可以使用元组来储存平面坐标。或者,我们要写一个电话本程序,我们可以把某人的名字,电话号码以及地址储存到一个元组中。在第二个例子中,我们同样知道我们需要储存三个值为一组。尽管三个值的类型不尽相同,但元组对其中值的类型没有同一性的要求。

让我们看一下这些元组样本。

例子: 一些元组

(True, 1)
("Hello world", False)
(4, 5, "Six", True, 'b')


第一个例子是一个有两个元素的元组,第一个元素是True第二个是1。第二个例子同样是一个有两个元素的元组,第一个是"Hello world"第二个是False。第三个例子比较复杂,这个元组有五个元素,第一个是数字4,第二个是数字5,第三个是"Six",第四个是True,最后一个是字母'b'。元组的组成就是用逗号吧各个元素分开,两端围上圆括号。

术语:若元组的长度为n则称其为n-tuple。特殊地如果元组的长度为2则称其为'pairs'(对),如果元组的长度为3则称其为'triples'。

元组跟列表有一点是相似的,就是它们都能储存多个值。但是,很重要的一点,不同长度的元组的类型是不一样的。虽然这里再次提及类型这个概念,一个你可能还不清楚的概念,但是可以看到列表跟元组对待自身长度的方式是不一样的。换一种说法,如果对一个由数字组成的列表添加一个新的数字,它依然是一个列表。但是如果你往一个长度为2的元组中添加一个元素,则你得到了一个完全不同的元组[2]

练习
  1. 写一个 3-tuple 第一个元素为 4, 第二个元素为 "hello" , 第三个元素为 True。
  2. 以下哪几个是元组 ?
    1. (4, 4)
    2. (4, "hello")
    3. (True, "Blah", "foo")
  3. 往一个列表堆叠(consing)新的元素: 你可以往一个数字列表添加数字,得到一个数字列表,但是元组是没有这种堆叠方法的。
    1. 你是怎样理解的?
    2. 讨论:如果有一个函数可以对元组进行堆叠,堆叠后得到的是什么?

元组有何用途?编辑

当你想在某个函数中返回多个值的时候,元组很好使。在大多数语言中,返回多个值意味着那些值必须被封装成一种特殊的数据结构,而这种数据结构也许仅仅就在这个函数中使用一次(这种情况下显得有点浪费)。 在Haskell中,仅仅需要返回一个元组就够了。

Haskell记录常常达到同样的目的,但是你将不得不指定元素的名字。我们将在接下来的学习中与记录不期而遇。

你也可将元组视为一种原子数据结构。 这需要对类型有所了解,而到我们还没说到那。

从元组中取出数据编辑

在本节中,我们的注意力将仅仅集中在包含2个元素的元组上。虽然这主要是为了简化,但2个元素的元组确实是被使用最多的一种元组。

好的,我们已经看到,简单地使用(x, y, z)语法,可以把数值放进元组。我们怎样再把它们取出来?例如,元组的一个典型用途是储存一个点的(x, y)坐标:想像你有一个棋盘,而你想要指定一个特定的方格。你可以通过给所有的行打上从1到8的标签来做到,列同理,然后说,(2, 5)代表第2行第5列。我们要定义一个能找出一个给定列中所有点的函数。方法之一是找到所有点的坐标,然后看行部分是否等于我们要找的行。一旦有了一个点的坐标(x, y),这个函数将需要提取x(行部分)。这里有两个函数可以做到,fstsnd,它们分别“投影”出一个对中的第一和第二个元素(用数学语言来说,从结构体中取出数据的函数叫做“投影”(Projection))。让我们来看一些例子:

例子: 使用fstsnd

Prelude> fst (2, 5)
2
Prelude> fst (True, "boo")
True
Prelude> snd (5, "Hello")
"Hello"


以上函数的功能容易明白。但是要注意的是fstsnd只能使用到长度为2的元组中。[3]

练习
  1. 使用fstsnd的组合将4从(("Hello", 4), True)中取出。
  2. 思考列表 [(4, 'a'),(5,'b')] 与 列表 [(4, 1),(5, 2)] 之间的区别。

将元素从元组中取出的通用技巧是模式匹配(是的,由于这是一个函数式编程的出众特征,我们过些时候将深入讨论)。为避免使事情比原本的更复杂,让我们仅仅向你展示我怎么写一个和fst有同样功能、命名为first的函数:

例子: first的定义

 Prelude> let first (x, y) = x
 Prelude> first (3, True) 
 3


这就是说如果输入(x, y)first (x, y)会返回x

元组组成的元组(以及其它组合)编辑

我们可以像在列表中储存列表一样来操作元组。元组也是数据,所以你可以在元组中储存元组(嵌套在元组中直到任意复杂的级别)。同样,你也可以创建元组组成的列表,列表组成的元组,以下例子的每一行分别表达了一中不同的组合方法。

例子: 嵌入元组和列表

((2,3), True)
((2,3), [2,3])
[(1,2), (3,4), (5,6)]


一些相关讨论——你更应该从中看到数据分组的重要思想

无论如何,这里还有一个难点需要当心。决定元组类型的不仅有它的大小,还有它其中所包含的元素的类型。例如,("Hello",32)(47,"World")是完全不同的两个类型。一个是(String,Int)类型的元组,然而另一个是(Int,String)。因此我们在构造包含元组的列表时,只能构建形如[("a",1),("b",9),("c",9)]这样的列表,而[("a",1),(2,"b"),(9,"c")]就是错误的。你能指出其中的区别吗?

练习
  1. 以下哪些是正确的Haskell表达式,为什么?
    • fst [1,2]
    • 1:(2,3)
    • (2,4):(2,3)
    • (2,4):[]
    • [(2,4),(5,5),('a','b')]
    • ([2,4],[2,2])

概要编辑

在这一章我们已经介绍了两种新的记法,列表和元组。总结一下:

  1. 列表用方括号和逗号定义:[1,2,3].
    • 它们可以包含任意数据,只要这个列表的所有元素都属于相同类型
    • 也可以用cons操作符(:)来创建它们,但是你只能附加(cons)数据到列表
  2. 元组用圆括号和逗号定义:("Bob",32)
    • 它们可以包含任意数据,甚至不同类型的数据
    • 它们有一个固定的长度,或者至少它们的长度被它们的类型决定。就是说,两个不同长度的元组有不同的类型。
  3. 列表和元组可以任意方式嵌套:列表组成的列表,元组组成的列表,等等

我们希望在这一刻,你已经能熟练运用Haskell的基本构建(变量,函数,列表),因为我们现在会转移到一些更振奋人心的主题,类型和递归。 类型,虽然我们已经在这一章提及了三次,但是却没有明确说明它是什么。 在解释什么是类型前,我们还是先对GHC作一个介绍,使你能更好地使用GHC解释器。

提示编辑

  1. 你应该反对因为你认为那甚至不是一个正确的单词。好的,它不是。在程序员中,一个由LISP开始的传统是把这种追加元素到列表头部的操作称为"consing"。因为这是一种构造列表的方式,所以这个动词在这里是"cons",也就是"constructor"(构造)的简写。
  2. 这至少涉及到类型,但是我们正试着避免使用这个词 :)
  3. 更专业的说,fstsnd有限制它们配对的类型。你将不能定义通用的以元组为参数的射影函数(projection function),因为它们将不得不接受不同大小的数据,从而使这个函数的类型多样化。



列表和元组
习题解答
Haskell基础

起步  >> 变量和函数  >> 列表和元组  >> 更进一步  >> 类型基础  >> 简单的输入输出  >> 类型声明


Haskell

Haskell基础 >> 初级Haskell >> Haskell进阶 >> Monads
高级Haskell >> 类型的乐趣 >> 理论提升 >> Haskell性能


库参考 >> 普通实务 >> 特殊任务


提示编辑