跳至內容

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])到一個列表上,從而創建另一個列表。

Example
Example

例子: 把數據附加(Cons)到列表

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


當你附加數據到列表(something:someList)時,你得到的是另一個列表。那麼,毫無懸念,這種構建方式是可以複合的。

Example
Example

例子: 附加(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的錯誤,你會得到如下錯誤。

Example
Example

例子: 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,但仍然錯誤

Example
Example

例子: 更簡單但仍然錯誤

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 =

列表組成的列表

[編輯]

列表可以包含任意數據,只要它們都屬於同一種類型。那麼,接下來思考這樣一個問題:列表也是數據,所以,列表可以包含……是的的確,其它列表!在解釋器上嘗試以下語句:

Example
Example

例子: 列表可以包含列表

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),所以在這個例子中可以使用元組來儲存平面坐標。或者,我們要寫一個電話本程序,我們可以把某人的名字,電話號碼以及地址儲存到一個元組中。在第二個例子中,我們同樣知道我們需要儲存三個值為一組。儘管三個值的類型不盡相同,但元組對其中值的類型沒有同一性的要求。

讓我們看一下這些元組樣本。

Example
Example

例子: 一些元組

(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))。讓我們來看一些例子:

Example
Example

例子: 使用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的函數:

Example
Example

例子: first的定義

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


這就是說如果輸入(x, y)first (x, y)會返回x

元組組成的元組(以及其它組合)

[編輯]

我們可以像在列表中儲存列表一樣來操作元組。元組也是數據,所以你可以在元組中儲存元組(嵌套在元組中直到任意複雜的級別)。同樣,你也可以創建元組組成的列表,列表組成的元組,以下例子的每一行分別表達了一中不同的組合方法。

Example
Example

例子: 嵌入元組和列表

((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性能


庫參考 >> 普通實務 >> 特殊任務


提示

[編輯]