本篇将介绍函数式编程中常用的两种基本数据结构,list(列表)和tuple(元组)。 这两种数据结构贯穿了函数式编程的全部内容。 list和tuple是如此强大,以至于Lisp这样的函数式语言,在语言层面没有结构体的情况下,仍然能够基于这两种数据结构,做出全部的编程抽象。
实际上Lisp只有list没有tuple。由于Lisp是弱类型语言,list也承担了tuple的功能。
List (列表)
list是函数式编程中特有的一种数据结构。它在函数式编程中的地位,相当于数组在过程式编程中的地位。
list由一系列顺序排列的节点组成。 list的最后一个节点是空节点,用“nil”或“[]”表示。 其他非空节点由两部分信息组成:一是本节点包含的具体数据;二是除本节点外,后续所有节点构成的list。

上图是一个list的具体例子,它由四个节点组成:前三个节点分别包含42、69、613三个数字作为内容,最后一个节点是空节点。 每个非空节点除了包含数据外,还包含了对尾部list的引用。
我们用冒号表示list的节点的连接关系,那么上述例子中的list可以表示为
42:69:613:nil
或
42:69:613:[]
或者简写表示为
[42, 69, 613]
看到这里你可能会想到Python里的list数据结构。没错,Python中的list就是借鉴自函数编程中的list概念。
有时我们用x和ys分别代表list的首节点和尾部,那么上述list也可以表示为x:ys,
其中x = 42, ys = 69:613:[]
list作为一种数据结构,提供了三个基本操作,分别是head,tail和cons。
head函数接受一个list作为参数,返回该list首节点所包含的数据:
head([42, 69, 613]) = 42
tail函数接受一个list作为参数,返回该list除首节点外其他后续节点所组成的list:
tail([42, 69, 613]) = [69, 613]
cons函数接受一个数据和一个list,将该数据拼接到list的首位后得到一个新list,作为返回值:
cons(42, [69, 613]) = [42, 69, 613]
cons(36, []) = [36]
看到这里,你可能会觉得,list不就是链表(linked-list)吗?
的确,从数据模型上来讲,这两者是基本一致的。 但是它们的具体关注点是不同的。 list的核心思想是“实现了head、tail和cons三个接口的数据结构”。 至于该数据结构实际怎样存储,并不关心。链表、数组甚至二叉树都可以作为list这一数据结构的底层实现。
事实上,在函数式编程的底层模型——λ演算中,根本没有内存这一概念,因此也不存在过程式编程中的所说的各类数据结构。函数式编程对数据结构能做的唯一约束就是其接口。
在有类型的函数式编程语言中,list中的每个元素必须是相同类型的。
例如你可以写出[1, 2, 3]或者["abc", "efg", "xyz"];但[1, "cde"]这样的列表是不合法的。
tuple(元组)
一个N-tuple翻译为N-元组,是指由N个数据按顺序组成的定长数据结构。 例如一个数字3和一个字符串“hello”可以组成一个2-元组,表示为
(3, "hello")
Haskell中可以用fst和snd函数分别获取元组中的第一个或第二个元素。用伪代码可以表示为
fst( (3, "hello") ) = 3
snd( (3, "hello") ) = "hello"
如上例所述,元组中的数据可以是不同类型的。元组长度和对应位置的数据类型都是元组类型的一部分。也就是说(1, 2, 3)和(1, 2)是不同类型的元组;(1, 'c')和('c', 1)也是不同类型的元组。它们的类型分别为
(1, 2, 3) :: (Int, Int, Int)
(1, 2) :: (Int, Int)
(1, 'c') :: (Int, Char)
('c', 1) :: (Char, Int)
从某种意义上讲,元组可以看做是一个匿名的结构体。这个结构体内每个字段也都是匿名的,只通过位置进行索引。
元组类型也广泛被其他非函数式的现代编程语言所使用。 例如Python直接提供了元组类型;Go中的函数返回值也是一种不完整的元组类型实现。