[TOC]
## 各就各位,預備!

好的,出發!如果你就是那種從不看說明書的不良人士,我推薦你還是回頭看一下簡介的最后一節。那里面講了這個教程中你需要用到的工具及基本用法。我們首先要做的就是進入ghc的交互模式,接著就可以調幾個函數小體驗一把haskell了。打開控制臺,輸入ghci,你會看到如下歡迎信息
~~~
GHCi,?version?6.8.2:?http://www.haskell.org/ghc/???
:??for?help??Loading?package?base?...?linking?...?done.???
Prelude>?
~~~
恭喜,您已經進入了ghci!目前它的命令行提示是`prelude>`,不過它在你裝載什么東西后會變的比較長。免得礙眼,我們輸入個:`set prompt "ghci> "`把它改成`ghci>`。
如下是一些簡單的運算
~~~
ghci>?2?+?15??17???
ghci>?49?*?100??4900???
ghci>?1892?-?1472??420???
ghci>?5?/?2??2.5???
ghci>?
~~~
很簡單。也可以在一行中使用多個運算符,按照運算符優先級執行計算,使用括號可以更改優先級次序。
~~~
ghci>?(50?*?100)?-?4999???
1???
ghci>?50?*?100?-?4999???
1???
ghci>?50?*?(100?-?4999)???
-244950?
~~~
很酷么?嗯,我承認不。處理負數時會有個小陷阱:執行`5 * -3`會使ghci報錯。所以說,使用負數時最好將其置于括號之中,像`5*(-3)`就不會有問題。
邏輯運算也同樣直白,你也許知道,&&指邏輯與,||指邏輯或,not指邏輯否。
~~~
ghci>?True?&&?False???
False???
ghci>?True?&&?True???
True???
ghci>?False?||?True???
True????
ghci>?not?False???
True???
ghci>?not?(True?&&?True)???
False?
~~~
相等性可以這樣判定
~~~
ghci>?5?==?5???
True???
ghci>?1?==?0???
False???
ghci>?5?/=?5???
False???
ghci>?5?/=?4???
True???
ghci>?"hello"?==?"hello"???
True??
~~~
執行5+"llama"或者5==True會怎樣?好的,一個大大的報錯等著你。
~~~
No?instance?for?(Num?[Char])???
arising?from?a?use?of?`+'?at?:1:0-9???
Possible?fix:?add?an?instance?declaration?for?(Num?[Char])???
In?the?expression:?5?+?"llama"???
In?the?definition?of?`it':?it?=?5?+?"llama"??
~~~
Yikes!ghci 提示說"llama"并不是數值類型,所以它不知道該怎樣才能給它加上5。即便是“four”甚至是“4”也不可以,haskel不拿它當數值。執行True==5, ghci就會提示類型不匹配。+運算符要求兩端都是數值,而==運算符僅對兩個可比較的值可用。這就要求他們的類型都必須一致,蘋果和橙子就無法做比較。我們會在后面深入地理解類型的概念。Note:`5+4.0`是可以執行的,5既可以做被看做整數也可以被看做浮點數,但4.0則不能被看做整數。

也許你并未察覺,不過從始至終我們一直都在使用函數。*就是一個將兩個數相乘的函數,就像三明治一樣,用兩個參數將它夾在中央,這被稱作中綴函數。而其他大多數不能與數夾在一起的函數則被稱作前綴函數。絕大部分函數都是前綴函數,在接下來我們就不多做甄別。大多數命令式編程語言中的函數調用形式通常就是函數名,括號,由逗號分隔的參數表。而在haskell中,函數調用的形式是函數名,空格,空格分隔的參數表。簡單據個例子,我們調用haskell中最無聊的函數:
~~~
ghci>?succ?8???
9??
~~~
succ函數返回一個數的后繼(successor, 在這里就是8后面那個數,也就是9。譯者注)。如你所見,通過空格將函數與參數分隔。調用多個參數的函數也是同樣容易,min和max接受兩個可比較大小的參數,并返回較大或者較小的那個數。
~~~
ghci>?min?9?10???
9???
ghci>?min?3.4?3.2???
3.2???
ghci>?max?100?101???
101??
~~~
函數調用擁有最高的優先級,如下兩句是等效的
~~~
ghci>?succ?9?+?max?5?4?+?1???
16???
ghci>?(succ?9)?+?(max?5?4)?+?1???
16?
~~~
若要取9乘10的后繼,`succ 9*10`是不行的,程序會先取9的后繼,然后再乘以10得100。正確的寫法應該是`succ(9*10)`,得91。如果某函數有兩個參數,也可以用?\`?符號將它括起,以中綴函數的形式調用它。例如取兩個整數相除所得商的div函數,div 92 10可得9,但這種形式不容易理解:究竟是哪個數是除數,哪個數被除?使用中綴函數的形式?92 \`div\` 10 就更清晰了。從命令式編程走過來的人們往往會覺得函數調用與括號密不可分,在C中,調用函數必加括號,就像foo(),bar(1),或者`baz(3,"haha")`。而在haskell中,函數的調用必使用空格,例如`bar (bar 3)`,它并不表示以bar和3兩個參數去調用bar,而是以bar 3所得的結果作為參數去調用bar。在C中,就相當于`bar(bar(3))`。
## 啟蒙:你的第一個函數
在前一節中我們簡單介紹了函數的調用,現在讓我們編寫我們自己的函數!打開你最喜歡的編輯器,輸入如下代碼,它的功能就是將一個數字乘以2.
~~~
doubleMe?x?=?x?+?x?
~~~
函數的聲明與它的調用形式大體相同,都是先函數名,后跟由空格分隔的參數表。但在聲明中一定要在 = 后面定義函數的行為。
保存為baby.hs或任意名稱,然后轉至保存的位置,打開ghci,執行:l baby.hs。這樣我們的函數就裝載成功,可以調用了。
~~~
ghci>?:l?baby???
[1?of?1]?Compiling?Main?????????????(?baby.hs,?interpreted?)???
Ok,?modules?loaded:?Main.???
ghci>?doubleMe?9???
18???
ghci>?doubleMe?8.3???
16.6??
~~~
+運算符對整數和浮點都可用(實際上所有有數字特征的值都可以),所以我們的函數可以處理一切數值。聲明一個包含兩個參數的函數如下:
~~~
doubleUs?x?y?=?x*2?+?y*2??
~~~
很簡單。將其寫成doubleUs x y = x + x + y + y也可以。測試一下(記住要保存為baby.hs并到ghci下邊執行:l baby.hs)
~~~
ghci>?doubleUs?4?9??26???
ghci>?doubleUs?2.3?34.2??73.0???
ghci>?doubleUs?28?88?+?doubleMe?123???
478
~~~
你可以在其他函數中調用你編寫的函數,如此一來我們可以將doubleMe函數改為:
~~~
doubleUs?x?y?=?doubleMe?x?+?doubleMe?y??
~~~

這種情形在haskell下邊十分常見:編寫一些簡單的函數,然后將其組合,形成一個較為復雜的函數,這樣可以減少重復工作。設想若是哪天有個數學家驗證說2應該是3,我們只需要將doubleMe改為x+x+x即可,由于doubleUs調用到doubleMe,于是整個程序便進入了2即是3的古怪世界。
haskell中的函數并沒有順序,所以先聲明doubleUs還是先聲明doubleMe都是同樣的。如下,我們編寫一個函數,它將小于100的數都乘以2,因為大于100的數都已經足夠大了!
~~~
doubleSmallNumber?x?=?if?x?>?100???????????????????????????
?????????????????????????????????????then?x???????????????????????????
?????????????????????????????????????else??x*2??
~~~
接下來介紹haskell的if語句。你也許會覺得和其他語言很像,不過存在一些不同。haskell中if語句的else部分是不可省略。在命令式語言中,你可以通過if語句來跳過一段代碼,而在haskell中,每個函數和表達式都要返回一個結果。對于這點我覺得將if語句置于一行之中會更易理解。haskell 中的if語句的另一個特點就是它其實是個表達式,表達式就是返回一個值的一段代碼:5是個表達式,它返回5;4+8是個表達式;x+y也是個表達式,它返 回x+y的結果。正由于else是強制的,if語句一定會返回某個值,所以說if語句也是個表達式。如果要給剛剛定義的函數的結果都加上1,可以如此修改:
~~~
doubleSmallNumber'?x?=?(if?x?>?100?then?x?else?x*2)?+?1?
~~~
若是去掉括號,那就會只在小于100的時候加1。注意函數名最后的那個單引號,它沒有任何特殊含義,只是一個函數名的合法字符罷了。通常,我們使用單引號來區分一個稍經修改但差別不大的函數。定義這樣的函數也是可以的:
~~~
conanO'Brien?=?"It's?a-me,?Conan?O'Brien!"??
~~~
在這里有兩點需要注意。首先就是我們沒有大寫conan的首字母,因為首字母大寫的函數是不允許的,稍后我們將討論其原因;另外就是這個函數并沒有任何參數。沒有參數的函數通常被稱作“定義”(或者“名字”),一旦定義,conanO'Brien就與字符串"It's a-me, Conan O'Brien!"完全等價,且它的值不可以修改。
## List入門

在Haskell中,List就像現實世界中的購物單一樣重要。它是最常用的數據結構,并且十分強大,靈活地使用它可以解決很多問題。本節我們將對List,字符串和list comprehension有個初步了解。 在Haskell中,List是一種單類型的數據結構,可以用來存儲多個類型相同的元素。我們可以在里面裝一組數字或者一組字符,但不能把字符和數字裝在一起。
> **Note**:在ghci下,我們可以使用let關鍵字來定義一個常量。在ghci下執行`let a =1`與在腳本中編寫a=1是等價的。
~~~
ghci>?let?lostNumbers?=?[4,8,15,16,23,48]???
ghci>?lostNumbers????
[4,8,15,16,23,48]?
~~~
如你所見,一個List由方括號括起,其中的元素用逗號分隔開來。若試圖寫`[1,2,'a',3,'b','c',4]`這樣的List,Haskell就會報出這幾個字符不是數字的錯誤。字符串實際上就是一組字符的List,"Hello"只是`['h','e','l','l','o']`的語法糖而已。所以我們可以使用處理List的函數來對字符串進行操作。 將兩個List合并是很常見的操作,這可以通過++運算符實現。
~~~
ghci>?[1,2,3,4]?++?[9,10,11,12]????
[1,2,3,4,9,10,11,12]???
ghci>?"hello"?++?"?"?++?"world"???
"hello?world"???
ghci>?['w','o']?++?['o','t']???
"woot"
~~~
在使用++運算符處理長字符串時要格外小心(對長List也是同樣),Haskell會遍歷整個的List(++符號左邊的那個)。在處理較短的字符串時問題還不大,但要是在一個5000萬長度的List上追加元素,那可得執行好一會兒了。所以說,用:運算符往一個List前端插入元素會是更好的選擇。
~~~
ghci>?'A':"?SMALL?CAT"???
"A?SMALL?CAT"???
ghci>?5:[1,2,3,4,5]??
[5,1,2,3,4,5]?
~~~
:運算符可以連接一個元素到一個List或者字符串之中,而++運算符則是連接兩個List。若要使用++運算符連接單個元素到一個List之中,就用方括號把它括起使之成為單個元素的List。`[1,2,3]`實際上是`1:2:3:[]`的語法糖。`[]`表示一個空List,若要從前端插入3,它就成了`[3]`,再插入2,它就成了`[2,3]`,以此類推。
> **Note**:`[],[[]],[[],[],[]]`是不同的。第一個是一個空的List,第二個是含有一個空List的List,第三個是含有三個空List的List。
若是要按照索引取得List中的元素,可以使用!!運算符,索引的下標為0。
~~~
ghci>?"Steve?Buscemi"?!!?6???
'B'???
ghci>?[9.4,33.2,96.2,11.2,23.25]?!!?1???
33.2?
~~~
但你若是試圖在一個只含有4個元素的List中取它的第6個元素,就會報錯。要小心!
List同樣也可以用來裝List,甚至是List的List的List:
~~~
ghci>?let?b?=?[[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]???
ghci>?b???
[[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]???
ghci>?b?++?[[1,1,1,1]]???
[[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3],[1,1,1,1]]???
ghci>?[6,6,6]:b???
[[6,6,6],[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]???
ghci>?b?!!?2???
[1,2,2,3,4]??
~~~
List中的List可以是不同長度,但必須得是相同的類型。如不可以在List中混合放置字符和數組相同,混合放置數值和字符的List也是同樣不可以的。當List內裝有可比較的元素時,使用 > 和 >=可以比較List的大小。它會先比較第一個元素,若它們的值相等,則比較下一個,以此類推。
~~~
ghci>?[3,2,1]?>?[2,1,0]???
True???
ghci>?[3,2,1]?>?[2,10,100]???
True???
ghci>?[3,4,2]?>?[3,4]???
True???
ghci>?[3,4,2]?>?[2,4]???
True???
ghci>?[3,4,2]?==?[3,4,2]???
True?
~~~
還可以對List做啥?如下是幾個常用的函數:
head返回一個List的頭部,也就是List的首個元素。
~~~
ghci>?head?[5,4,3,2,1]??
5
~~~
tail返回一個LIst的尾部,也就是List除去頭部之后的部分。
~~~
ghci>?tail?[5,4,3,2,1]???
[4,3,2,1]??
~~~
last返回一個LIst的最后一個元素。
~~~
ghci>?last?[5,4,3,2,1]???
1??
~~~
init返回一個LIst出去最后一個元素的部分。
~~~
ghci>?init?[5,4,3,2,1]???
[5,4,3,2]??
~~~
如果我們把List想象為一頭怪獸,那這就是它的樣子:

試一下,若是取一個空List的head又會怎樣?
~~~
ghci>?head?[]???
***?Exception:?Prelude.head:?empty?list?
~~~
omg,它翻臉了!怪獸壓根就不存在,head又從何而來?在使用head,tail,last和init時要小心別用到空的List上,這個錯誤不會在編譯時被捕獲。所以說做些工作以防止從空List中取值會是個好的做法。
length返回一個List的長度。
~~~
ghci>?length?[5,4,3,2,1]???
5?
~~~
null檢查一個List是否為空。如果是,則返回True,否則返回False。應當避免使用xs==[]之類的語句來判斷List是否為空,使用null會更好。
~~~
ghci>?null?[1,2,3]???
False???
ghci>?null?[]???
True?
~~~
reverse將一個List反轉
~~~
ghci>?reverse?[5,4,3,2,1]???
[1,2,3,4,5]?
~~~
take返回一個List的前幾個元素,看:
~~~
ghci>?take?3?[5,4,3,2,1]???
[5,4,3]???
ghci>?take?1?[3,9,3]???
[3]???
ghci>?take?5?[1,2]???
[1,2]???
ghci>?take?0?[6,6,6]??
[]?
~~~
如上,若是圖取超過List長度的元素個數,只能得到原List。若take 0個元素,則會得到一個空List!drop與take的用法大體相同,它會刪除一個List中的前幾個元素。
~~~
ghci>?drop?3?[8,4,2,1,5,6]???
[1,5,6]???
ghci>?drop?0?[1,2,3,4]???
[1,2,3,4]???
ghci>?drop?100?[1,2,3,4]???
[]??
~~~
maximum返回一個List中最大的那個元素。miniimun返回最小的。
~~~
ghci>?minimum?[8,4,2,1,5,6]???
1???
ghci>?maximum?[1,9,2,3,4]???
9??
~~~
sum返回一個List中所有元素的和。product返回一個List中所有元素的積。
~~~
ghci>?sum?[5,2,1,6,3,2,5,7]???
31???
ghci>?product?[6,2,1,2]???
24???
ghci>?product?[1,2,5,6,7,9,2,0]???
0??
~~~
elem判斷一個元素是否在包含于一個List,通常以中綴函數的形式調用它。
~~~
ghci>?4?`elem`?[3,4,5,6]???
True???
ghci>?10?`elem`?[3,4,5,6]???
False?
~~~
這就是幾個基本的List操作函數,我們會在往后的一節中了解更多的函數。
## 德州區間

該怎樣得到一個包含1到20之間所有數的List呢?我們完全可以用手把它全打出來,但顯而易見,這并不是完美人士的方案,他們都用區間(Range)。Range是構造List方法之一,而其中的值必須是可枚舉的,像1、2、3、4...字符同樣也可以枚舉,字母表就是A..Z所有字符的枚舉。而名字就不可以枚舉了,"john"后面是誰?我不知道。
要得到包含1到20中所有自然數的List,只要`[1..20]`即可,這與用手寫`[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]`是完全等價的。其實用手寫一兩個還不是什么大事,但若是手寫一個非常長的List那就一定是笨得可以了。
~~~
ghci>?[1..20]???
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]???
ghci>?['a'..'z']???
"abcdefghijklmnopqrstuvwxyz"???
ghci>?['K'..'Z']???
"KLMNOPQRSTUVWXYZ"
~~~
Range很cool,允許你申明一個步長。要得到1到20間所有的偶數或者3的倍數該怎樣?
~~~
ghci>?[2,4..20]???
[2,4,6,8,10,12,14,16,18,20]???
ghci>?[3,6..20]???
[3,6,9,12,15,18]??
~~~
僅需用逗號將前兩個元素隔開,再標上上限即可。盡管Range很聰明,但它恐怕還滿足不了一些人對它的期許。你就不能通過`[1,2,4..100]`這樣的語句來獲得所有2的冪。一方面是因為步長只能標明一次,另一方面就是僅憑前幾項,數組的后項是不能確定的。要得到20到1的List,`[20..1]`是不可以的。必須得`[20,19..1]`。在Range中使用浮點數要格外小心!出于定義的原因,浮點數并不精確。若是使用浮點數的話,你就會得到如下的糟糕結果
~~~
ghci>?[0.1,?0.3?..?1]???
[0.1,0.3,0.5,0.7,0.8999999999999999,1.0999999999999999]?
~~~
我的建議就是避免在Range中使用浮點數。
你也可以不標明Range的上限,從而得到一個無限長度的List。在后面我們會講解關于無限List的更多細節。取前24個13的倍數該怎樣?恩,你完全可以`[13,26..24*13]`,但有更好的方法:`take 24 [13,26..]`。
由于Haskell是惰性的,它不會對無限長度的List求值,否則會沒完沒了的。它會等著,看你會從它那兒取多少。在這里它見你只要24個元素,便欣然交差。如下是幾個生成無限List的函數cycle接受一個List做參數并返回一個無限List。如果你只是想看一下它的運算結果而已,它會運行個沒完的。所以應該在某處劃好范圍。
~~~
ghci>?take?10?(cycle?[1,2,3])???
[1,2,3,1,2,3,1,2,3,1]???
ghci>?take?12?(cycle?"LOL?")???
"LOL?LOL?LOL?"??
~~~
repeat接受一個值作參數,并返回一個僅包含該值的無限List。這與用cycle處理單元素List差不多。
~~~
ghci>?take?10?(repeat?5)???
[5,5,5,5,5,5,5,5,5,5]?
~~~
其實,你若只是想得到包含相同元素的List,使用replicate會更簡單,如`replicate 3 10`,得`[10,10,10]`。
## 我是List Comprehension

學過數學的你對集合的comprehension(Set Comprehension)概念一定不會陌生。通過它,可以從既有的集合中按照規則產生一個新集合。前十個偶數的set comprehension可以表示為,豎線左端的部分是輸出函數,x是變量,N是輸入集合。在haskell下,我們可以通過類似`take 10 [2,4..]`的代碼來實現。但若是把簡單的乘2改成更復雜的函數操作該怎么辦呢?用list comprehension,它與set comprehension十分的相似,用它取前十個偶數輕而易舉。這個list comprehension可以表示為:
~~~
ghci>?[x*2?|?x??[1..10]]???
[2,4,6,8,10,12,14,16,18,20]
~~~
如你所見,結果正確。給這個comprehension再添個限制條件(predicate),它與前面的條件由一個逗號分隔。在這里,我們要求只取乘以2后大于等于12的元素。
~~~
ghci>?[x*2?|?x??[1..10],?x*2?>=?12]???
[12,14,16,18,20]?
~~~
cool,靈了。若是取50到100間所有除7的余數為3的元素該怎么辦?簡單:
~~~
ghci>?[?x?|?x??[50..100],?x?`mod`?7?==?3]???
[52,59,66,73,80,87,94]??
~~~
成功!從一個List中篩選出符合特定限制條件的操作也可以稱為過濾(flitering)。即取一組數并且按照一定的限制條件過濾它們。再舉個例子 吧,假如我們想要一個comprehension,它能夠使list中所有大于10的奇數變為“BANG”,小于10的奇數變為“BOOM”,其他則統統 扔掉。方便重用起見,我們將這個comprehension置于一個函數之中。
~~~
boomBangs?xs?=?[?if?x?10?then?"BOOM!"?else?"BANG!"?|?x??xs,?odd?x]??
~~~
這個comprehension的最后部分就是限制條件,使用odd函數判斷是否為奇數:返回True,就是奇數,該List中的元素才被包含。
~~~
ghci>?boomBangs?[7..13]???
["BOOM!","BOOM!","BANG!","BANG!"]??
~~~
也可以加多個限制條件。若要達到10到20間所有不等于13,15或19的數,可以這樣:
~~~
ghci>?[?x?|?x??[10..20],?x?/=?13,?x?/=?15,?x?/=?19]???
[10,11,12,14,16,17,18,20]?
~~~
除了多個限制條件之外,從多個List中取元素也是可以的。這樣的話comprehension會把所有的元素組合交付給我們的輸出函數。在不過濾的前提 下,取自兩個長度為4的集合的comprehension會產生一個長度為16的List。假設有兩個List,`[2,5,10]`和`[8,10,11]`, 要取它們所有組合的積,可以這樣:
~~~
ghci>?[?x*y?|?x??[2,5,10],?y??[8,10,11]]???
[16,20,22,40,50,55,80,100,110]??
~~~
意料之中,得到的新List長度為9。若只取乘積為50的結果該如何?
~~~
ghci>?[?x*y?|?x?[2,5,10],?y??[8,10,11],?x*y?>?50]???
[55,80,100,110]??
~~~
取個包含一組名詞和形容詞的List comprehension吧,寫詩的話也許用得著。
~~~
ghci>?let?nouns?=?["hobo","frog","pope"]???
ghci>?let?adjectives?=?["lazy","grouchy","scheming"]???
ghci>?[adjective?++?"?"?++?noun?|?adjective??adjectives,?noun??nouns]???
["lazy?hobo","lazy?frog","lazy?pope","grouchy?hobo","grouchy?frog",?"grouchy?pope","scheming?hobo",?
"scheming?frog","scheming?pope"]?
~~~
明白!讓我們編寫自己的length函數吧!就叫做length'!
~~~
length'?xs?=?sum?[1?|?_??xs]??
~~~
_表示我們并不關心從List中取什么值,與其弄個永遠不用的變量,不如直接一個_。這個函數將一個List中所有元素置換為1,并且使其相加求和。得到的結果便是我們的List長度。友情提示:字符串也是List,完全可以使用list comprehension來處理字符串。如下是個除去字符串中所有非大寫字母的函數:
~~~
removeNonUppercase?st?=?[?c?|?c??st,?c?`elem`?['A'..'Z']]??
~~~
測試一下:
~~~
ghci>?removeNonUppercase?"Hahaha!?Ahahaha!"???
"HA"???
ghci>?removeNonUppercase?"IdontLIKEFROGS"???
"ILIKEFROGS"??
~~~
在這里,限制條件做了所有的工作。它說:只有在`['A'..'Z']`之間的字符才可以被包含。
若操作含有List的List,使用嵌套的List comprehension也是可以的。假設有個包含許多數值的List的List,讓我們在不拆開它的前提下除去其中的所有奇數:
~~~
ghci>?let?xxs?=?[[1,3,5,2,3,1,2,4,5],[1,2,3,4,5,6,7,8,9],[1,2,4,2,1,6,3,1,3,2,3,6]]???
ghci>?[?[?x?|?x??xs,?even?x?]?|?xs??xxs]???
[[2,2,4],[2,4,6,8],[2,4,2,6,2,6]]?
~~~
將List Comprehension分成多行也是可以的。若非在GHCI之下,還是將List Comprehension分成多行好,尤其是需要嵌套的時候。
## Tuple

從某種意義上講,Tuple(元組)很像List--都是將多個值存入一個個體的容器。但它們卻有著本質的不同,一組數字的List就是一組數字,它們的類型相 同,且不關心其中包含元素的數量。而Tuple則要求你對需要組合的數據的數目非常的明確,它的類型取決于其中項的數目與其各自的類型。Tuple中的項 由括號括起,并由逗號隔開。
另外的不同之處就是Tuple中的項不必為同一類型,在Tuple里可以存入多類型項的組合。
動腦筋,在haskell中表示二維向量該如何?使用List是一種方法,它倒也工作良好。若要將一組向量置于一個List中來表示平面圖形又該怎樣?我們可以寫類似`[[1,2],[8,11],[4,5]]`的代碼來實現。但問題在于,`[[1,2],[8,11,5],[4,5]]`也是同樣合法的,因為其中元素的類型都相同。盡管這樣并不靠譜,但編譯時并不會報錯。然而一個長度為2的Tuple(也可以稱作序對,Pair),是一個獨立的類 型,這便意味著一個包含一組序對的List不能再加入一個三元組,所以說把原先的方括號改為圓括號使用Tuple會 更好:`[(1,2),(8,11),(4,5)]`。若試圖表示這樣的圖形:`[(1,2),(8,11,5),(4,5)]`,就會報出以下的錯誤:
~~~
Couldn't?match?expected?type?`(t,?t1)'???
against?inferred?type?`(t2,?t3,?t4)'???
In?the?expression:?(8,?11,?5)???
In?the?expression:?[(1,?2),?(8,?11,?5),?(4,?5)]???
In?the?definition?of?`it':?it?=?[(1,?2),?(8,?11,?5),?(4,?5)]
~~~
這告訴我們說程序在試圖將序對和三元組置于同一List中,而這是不允許的。同樣`[(1,2),("one",2)]`這樣的List也不行,因為 其中的第一個Tuple是一對數字,而第二個Tuple卻成了一個字符串和一個數字。Tuple可以用來儲存多個數據,如,我們要表示一個人的名字與年 齡,可以使用這樣的Tuple:`("Christopher", "Walken", 55)`。從這個例子里也可以看出,Tuple中也可以存儲List。
使用Tuple前應當事先明確一條數據中應該由多少個項。每個不同長度的Tuple都是獨立的類型,所以你就不可以寫個函數來給它追加元素。而唯一能做的,就是通過函數來給一個List追加序對,三元組或是四元組等內容。
可以有單元素的List,但Tuple不行。想想看,單元素的Tuple本身就只有一個值,對我們又有啥意義?不靠譜。
同List相同,只要其中的項是可比較的,Tuple也可以比較大小,只是你不可以像比較不同長度的List那樣比較不同長度的Tuple。如下是兩個有用的序對操作函數:
fst返回一個序對的首項。
~~~
ghci>?fst?(8,11)???
8???
ghci>?fst?("Wow",?False)???
"Wow"
~~~
snd返回序對的尾項。
~~~
ghci>?snd?(8,11)???
11???
ghci>?snd?("Wow",?False)???
False
~~~
> **Note**:這兩個函數僅對序對有效,而不能應用于三元組,四元組和五元組之上。稍后,我們將過一遍從Tuple中取數據的所有方式。
有個函數很cool,它就是zip。它可以用來生成一組序對(Pair)的List。它取兩個List,然后將它們交叉配對,形成一組序對的List。它很簡單,卻很實用,尤其是你需要組合或是遍歷兩個List時。如下是個例子:
~~~
ghci>?zip?[1,2,3,4,5]?[5,5,5,5,5]???
[(1,5),(2,5),(3,5),(4,5),(5,5)]???
ghci>?zip?[1?..?5]?["one",?"two",?"three",?"four",?"five"]???
[(1,"one"),(2,"two"),(3,"three"),(4,"four"),(5,"five")]
~~~
它把元素配對并返回一個新的List。第一個元素配第一個,第二個元素配第二個..以此類推。注意,由于序對中可以含有不同的類型,zip函數可能會將不同類型的序對組合在一起。若是兩個不同長度的List會怎么樣?
~~~
ghci>?zip?[5,3,2,6,2,7,2,5,4,6,6]?["im","a","turtle"]???
[(5,"im"),(3,"a"),(2,"turtle")]
~~~
較長的那個會在中間斷開,去匹配較短的那個。由于haskell是惰性的,使用zip同時處理有限和無限的List也是可以的:
~~~
ghci>?zip?[1..]?["apple",?"orange",?"cherry",?"mango"]???
[(1,"apple"),(2,"orange"),(3,"cherry"),(4,"mango")]
~~~
接下來考慮一個同時應用到List和Tuple的問題:如何取得所有三邊長度皆為整數且小于等于10,周長為24的直角三角形?首先,把所有三遍長度小于等于10的三角形都列出來:
~~~
ghci>?let?triangles?=?[?(a,b,c)?|?c??[1..10],?b??[1..10],?a??[1..10]?]
~~~
剛才我們是從三個List中取值,并且通過輸出函數將其組合為一個三元組。只要在ghci下邊調用triangle,你就會得到所有三邊都小于等于 10的三角形。我們接下來給它添加一個限制條件,令其必須為直角三角形。同時也考慮上b邊要短于斜邊,a邊要短于b邊情況:
~~~
ghci>?let?rightTriangles?=?[?(a,b,c)?|?c??[1..10],?b??[1..c],?a??[1..b],?a^2?+?b^2?==?c^2]
~~~
已經差不多了。最后修改函數,告訴它只要周長為24的三角形。
~~~
ghci>?let?rightTriangles'?=?[?(a,b,c)?|?c??[1..10],?b??[1..c],?a??[1..b],?a^2?+?b^2?==?c^2,?a+b+c?==?24]???
ghci>?rightTriangles'???
[(6,8,10)]
~~~
得到正確結果!這便是函數式編程的一般思路:先取一個初始的集合并將其變形,執行過濾條件,最終取得正確的結果。