好的,出發(fā)!如果你就是那種從不看說明書的不良人士,我推薦你還是回頭看一下簡介的最后一節(jié)。那里面講了這個教程中你需要用到的工具及基本用法。我們首先要做的就是進入ghc的交互模式,接著就可以調(diào)幾個函數(shù)小體驗一把haskell了。打開控制臺,輸入ghci,你會看到如下歡迎信息
GHCi, version 6.8.2: http://www.haskell.org/ghc/
:? for help Loading package base ... linking ... done.
Prelude>
恭喜,您已經(jīng)進入了ghci!目前它的命令行提示是prelude>
,不過它在你裝載什么東西后會變的比較長。免得礙眼,我們輸入個:set prompt "ghci> "
把它改成ghci>
。
如下是一些簡單的運算
ghci> 2 + 15 17
ghci> 49 * 100 4900
ghci> 1892 - 1472 420
ghci> 5 / 2 2.5
ghci>
很簡單。也可以在一行中使用多個運算符,按照運算符優(yōu)先級執(zhí)行計算,使用括號可以更改優(yōu)先級次序。
ghci> (50 * 100) - 4999
1
ghci> 50 * 100 - 4999
1
ghci> 50 * (100 - 4999)
-244950
很酷么?嗯,我承認不。處理負數(shù)時會有個小陷阱:執(zhí)行5 * -3
會使ghci報錯。所以說,使用負數(shù)時最好將其置于括號之中,像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
執(zhí)行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"并不是數(shù)值類型,所以它不知道該怎樣才能給它加上5。即便是“four”甚至是“4”也不可以,haskel不拿它當數(shù)值。執(zhí)行True==5, ghci就會提示類型不匹配。+運算符要求兩端都是數(shù)值,而==運算符僅對兩個可比較的值可用。這就要求他們的類型都必須一致,蘋果和橙子就無法做比較。我們會在后面深入地理解類型的概念。Note:5+4.0
是可以執(zhí)行的,5既可以做被看做整數(shù)也可以被看做浮點數(shù),但4.0則不能被看做整數(shù)。
也許你并未察覺,不過從始至終我們一直都在使用函數(shù)。*就是一個將兩個數(shù)相乘的函數(shù),就像三明治一樣,用兩個參數(shù)將它夾在中央,這被稱作中綴函數(shù)。而其他大多數(shù)不能與數(shù)夾在一起的函數(shù)則被稱作前綴函數(shù)。絕大部分函數(shù)都是前綴函數(shù),在接下來我們就不多做甄別。大多數(shù)命令式編程語言中的函數(shù)調(diào)用形式通常就是函數(shù)名,括號,由逗號分隔的參數(shù)表。而在haskell中,函數(shù)調(diào)用的形式是函數(shù)名,空格,空格分隔的參數(shù)表。簡單據(jù)個例子,我們調(diào)用haskell中最無聊的函數(shù):
ghci> succ 8
9
succ函數(shù)返回一個數(shù)的后繼(successor, 在這里就是8后面那個數(shù),也就是9。譯者注)。如你所見,通過空格將函數(shù)與參數(shù)分隔。調(diào)用多個參數(shù)的函數(shù)也是同樣容易,min和max接受兩個可比較大小的參數(shù),并返回較大或者較小的那個數(shù)。
ghci> min 9 10
9
ghci> min 3.4 3.2
3.2
ghci> max 100 101
101
函數(shù)調(diào)用擁有最高的優(yōu)先級,如下兩句是等效的
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。如果某函數(shù)有兩個參數(shù),也可以用 ` 符號將它括起,以中綴函數(shù)的形式調(diào)用它。例如取兩個整數(shù)相除所得商的div函數(shù),div 92 10可得9,但這種形式不容易理解:究竟是哪個數(shù)是除數(shù),哪個數(shù)被除?使用中綴函數(shù)的形式 92 `div` 10 就更清晰了。從命令式編程走過來的人們往往會覺得函數(shù)調(diào)用與括號密不可分,在C中,調(diào)用函數(shù)必加括號,就像foo(),bar(1),或者baz(3,"haha")
。而在haskell中,函數(shù)的調(diào)用必使用空格,例如bar (bar 3)
,它并不表示以bar和3兩個參數(shù)去調(diào)用bar,而是以bar 3所得的結(jié)果作為參數(shù)去調(diào)用bar。在C中,就相當于bar(bar(3))
。
在前一節(jié)中我們簡單介紹了函數(shù)的調(diào)用,現(xiàn)在讓我們編寫我們自己的函數(shù)!打開你最喜歡的編輯器,輸入如下代碼,它的功能就是將一個數(shù)字乘以2.
doubleMe x = x + x
函數(shù)的聲明與它的調(diào)用形式大體相同,都是先函數(shù)名,后跟由空格分隔的參數(shù)表。但在聲明中一定要在 = 后面定義函數(shù)的行為。
保存為baby.hs或任意名稱,然后轉(zhuǎn)至保存的位置,打開ghci,執(zhí)行:l baby.hs。這樣我們的函數(shù)就裝載成功,可以調(diào)用了。
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
+運算符對整數(shù)和浮點都可用(實際上所有有數(shù)字特征的值都可以),所以我們的函數(shù)可以處理一切數(shù)值。聲明一個包含兩個參數(shù)的函數(shù)如下:
doubleUs x y = x*2 + y*2
很簡單。將其寫成doubleUs x y = x + x + y + y也可以。測試一下(記住要保存為baby.hs并到ghci下邊執(zhí)行:l baby.hs)
ghci> doubleUs 4 9 26
ghci> doubleUs 2.3 34.2 73.0
ghci> doubleUs 28 88 + doubleMe 123
478
你可以在其他函數(shù)中調(diào)用你編寫的函數(shù),如此一來我們可以將doubleMe函數(shù)改為:
doubleUs x y = doubleMe x + doubleMe y
這種情形在haskell下邊十分常見:編寫一些簡單的函數(shù),然后將其組合,形成一個較為復雜的函數(shù),這樣可以減少重復工作。設想若是哪天有個數(shù)學家驗證說2應該是3,我們只需要將doubleMe改為x+x+x即可,由于doubleUs調(diào)用到doubleMe,于是整個程序便進入了2即是3的古怪世界。
haskell中的函數(shù)并沒有順序,所以先聲明doubleUs還是先聲明doubleMe都是同樣的。如下,我們編寫一個函數(shù),它將小于100的數(shù)都乘以2,因為大于100的數(shù)都已經(jīng)足夠大了!
doubleSmallNumber x = if x > 100
then x
else x*2
接下來介紹haskell的if語句。你也許會覺得和其他語言很像,不過存在一些不同。haskell中if語句的else部分是不可省略。在命令式語言中,你可以通過if語句來跳過一段代碼,而在haskell中,每個函數(shù)和表達式都要返回一個結(jié)果。對于這點我覺得將if語句置于一行之中會更易理解。haskell 中的if語句的另一個特點就是它其實是個表達式,表達式就是返回一個值的一段代碼:5是個表達式,它返回5;4+8是個表達式;x+y也是個表達式,它返 回x+y的結(jié)果。正由于else是強制的,if語句一定會返回某個值,所以說if語句也是個表達式。如果要給剛剛定義的函數(shù)的結(jié)果都加上1,可以如此修改:
doubleSmallNumber' x = (if x > 100 then x else x*2) + 1
若是去掉括號,那就會只在小于100的時候加1。注意函數(shù)名最后的那個單引號,它沒有任何特殊含義,只是一個函數(shù)名的合法字符罷了。通常,我們使用單引號來區(qū)分一個稍經(jīng)修改但差別不大的函數(shù)。定義這樣的函數(shù)也是可以的:
conanO'Brien = "It's a-me, Conan O'Brien!"
在這里有兩點需要注意。首先就是我們沒有大寫conan的首字母,因為首字母大寫的函數(shù)是不允許的,稍后我們將討論其原因;另外就是這個函數(shù)并沒有任何參數(shù)。沒有參數(shù)的函數(shù)通常被稱作“定義”(或者“名字”),一旦定義,conanO'Brien就與字符串"It's a-me, Conan O'Brien!"完全等價,且它的值不可以修改。
在Haskell中,List就像現(xiàn)實世界中的購物單一樣重要。它是最常用的數(shù)據(jù)結(jié)構,并且十分強大,靈活地使用它可以解決很多問題。本節(jié)我們將對List,字符串和list comprehension有個初步了解。 在Haskell中,List是一種單類型的數(shù)據(jù)結(jié)構,可以用來存儲多個類型相同的元素。我們可以在里面裝一組數(shù)字或者一組字符,但不能把字符和數(shù)字裝在一起。
Note:在ghci下,我們可以使用let關鍵字來定義一個常量。在ghci下執(zhí)行
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就會報出這幾個字符不是數(shù)字的錯誤。字符串實際上就是一組字符的List,"Hello"只是['h','e','l','l','o']
的語法糖而已。所以我們可以使用處理List的函數(shù)來對字符串進行操作。 將兩個List合并是很常見的操作,這可以通過++運算符實現(xiàn)。
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上追加元素,那可得執(zhí)行好一會兒了。所以說,用:運算符往一個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中混合放置字符和數(shù)組相同,混合放置數(shù)值和字符的List也是同樣不可以的。當List內(nèi)裝有可比較的元素時,使用 > 和 >=可以比較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做啥?如下是幾個常用的函數(shù):
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反轉(zhuǎn)
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長度的元素個數(shù),只能得到原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,通常以中綴函數(shù)的形式調(diào)用它。
ghci> 4 `elem` [3,4,5,6]
True
ghci> 10 `elem` [3,4,5,6]
False
這就是幾個基本的List操作函數(shù),我們會在往后的一節(jié)中了解更多的函數(shù)。
該怎樣得到一個包含 1到20 之間所有數(shù)的 List 呢?我們完全可以用手把它全打出來,但顯而易見,這并不是完美人士的方案,他們都用區(qū)間(Range)。Range 是構造 List 方法之一,而其中的值必須是可枚舉的,像 1、2、3、4...字符同樣也可以枚舉,字母表就是 A . . Z 所有字符的枚舉。而名字就不可以枚舉了,"john" 后面是誰?我不知道。
要得到包含 1 到 20 中所有自然數(shù)的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間所有的偶數(shù)或者3的倍數(shù)該怎樣?
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的冪。一方面是因為步長只能標明一次,另一方面就是僅憑前幾項,數(shù)組的后項是不能確定的。要得到20到1的List,[20..1]是不可以的。必須得 [ 20,19 . . 1 ] 。在 Range 中使用浮點數(shù)要格外小心!出于定義的原因,浮點數(shù)并不精確。若是使用浮點數(shù)的話,你就會得到如下的糟糕結(jié)果
ghci> [0.1, 0.3 .. 1]
[0.1,0.3,0.5,0.7,0.8999999999999999,1.0999999999999999]
我的建議就是避免在Range中使用浮點數(shù)。
你也可以不標明Range的上限,從而得到一個無限長度的List。在后面我們會講解關于無限List的更多細節(jié)。取前24個13的倍數(shù)該怎樣?恩,你完全可以 [ 13 , 26 . . 24 * 13 ] ,但有更好的方法:take 24 [ 13 , 26 . . ] 。
由于 Haskell 是惰性的,它不會對無限長度的 List 求值,否則會沒完沒了的。它會等著,看你會從它那兒取多少。在這里它見你只要24個元素,便欣然交差。如下是幾個生成無限 List 的函數(shù) cycle 接受一個 List 做參數(shù)并返回一個無限List。如果你只是想看一下它的運算結(jié)果而已,它會運行個沒完的。所以應該在某處劃好范圍。
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 接受一個值作參數(shù),并返回一個僅包含該值的無限 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 ]。
學過數(shù)學的你對集合的 comprehension(Set Comprehension)概念一定不會陌生。通過它,可以從既有的集合中按照規(guī)則產(chǎn)生一個新集合。前十個偶數(shù)的 set comprehension 可以表示為,豎線左端的部分是輸出函數(shù),x是變量,N是輸入集合。在 haskell 下,我們可以通過類似 take 10 [ 2 , 4 . . ] 的代碼來實現(xiàn)。但若是把簡單的乘2改成更復雜的函數(shù)操作該怎么辦呢?用 list comprehension,它與 set comprehension 十分的相似,用它取前十個偶數(shù)輕而易舉。這個 list comprehension 可以表示為:
ghci> [x*2 | x <- [1..10]]
[2,4,6,8,10,12,14,16,18,20]
如你所見,結(jié)果正確。給這個 comprehension 再添個限制條件(predicate),它與前面的條件由一個逗號分隔。在這里,我們要求只取乘以 2 后大于等于 12 的元素。
ghci> [x*2 | x <- [1..10], x*2 >= 12]
[12,14,16,18,20]
cool,靈了。若是取 50 到 100 間所有除 7 的余數(shù)為 3 的元素該怎么辦?簡單:
ghci> [ x | x <- [50..100], x `mod` 7 == 3]
[52,59,66,73,80,87,94]
成功!從一個 List 中篩選出符合特定限制條件的操作也可以稱為過濾(flitering)。即取一組數(shù)并且按照一定的限制條件過濾它們。再舉個例子 吧,假如我們想要一個 comprehension ,它能夠使 list 中所有大于 10 的奇數(shù)變?yōu)椤癇ANG”,小于10的奇數(shù)變?yōu)椤癇OOM”,其他則統(tǒng)統(tǒng) 扔掉。方便重用起見,我們將這個 comprehension 置于一個函數(shù)之中。
boomBangs xs = [ if x < 10 then "BOOM!" else "BANG!" | x <- xs, odd x]
這個 comprehension 的最后部分就是限制條件,使用 odd 函數(shù)判斷是否為奇數(shù):返回 True ,就是奇數(shù),該 List 中的元素才被包含。
ghci> boomBangs [7..13]
["BOOM!","BOOM!","BANG!","BANG!"]
也可以加多個限制條件。若要達到 10 到 20 間所有不等于 13,15 或 19 的數(shù),可以這樣:
ghci> [ x | x <- [10..20], x /= 13, x /= 15, x /= 19]
[10,11,12,14,16,17,18,20]
除了多個限制條件之外,從多個 List 中取元素也是可以的。這樣的話 comprehension 會把所有的元素組合交付給我們的輸出函數(shù)。在不過濾的前提 下,取自兩個長度為4的集合的comprehension會產(chǎn)生一個長度為 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 的結(jié)果該如何?
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 函數(shù)吧!就叫做 length '!
length' xs = sum [1 | _ <- xs]
表示我們并不關心從 List 中取什么值,與其弄個永遠不用的變量,不如直接一個。這個函數(shù)將一個 List 中所有元素置換為1,并且使其相加求和。得到的結(jié)果便是我們的 List 長度。友情提示:字符串也是 List ,完全可以使用 list comprehension 來處理字符串。如下是個除去字符串中所有非大寫字母的函數(shù):
removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]
測試一下:
ghci> removeNonUppercase "Hahaha! Ahahaha!"
"HA"
ghci> removeNonUppercase "IdontLIKEFROGS"
"ILIKEFROGS"
在這里,限制條件做了所有的工作。它說:只有在 [ ' A ' . . ' Z ' ] 之間的字符才可以被包含。
若操作含有 List 的 List ,使用嵌套的 List comprehension 也是可以的。假設有個包含許多數(shù)值的 List 的 List ,讓我們在不拆開它的前提下除去其中的所有奇數(shù):
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 (元組)很像 List --都是將多個值存入一個個體的容器。但它們卻有著本質(zhì)的不同,一組數(shù)字的 List 就是一組數(shù)字,它們的類型相 同,且不關心其中包含元素的數(shù)量。而 Tuple 則要求你對需要組合的數(shù)據(jù)的數(shù)目非常的明確,它的類型取決于其中項的數(shù)目與其各自的類型。 Tuple 中的項 由括號括起,并由逗號隔開。
另外的不同之處就是 Tuple 中的項不必為同一類型,在 Tuple 里可以存入多類型項的組合。
動腦筋,在 haskell 中表示二維向量該如何?使用 List 是一種方法,它倒也工作良好。若要將一組向量置于一個 List 中來表示平面圖形又該怎樣?我們可以寫類似 [ [ 1 , 2 ] , [ 8 , 11 ] , [ 4 , 5 ] ] 的代碼來實現(xiàn)。但問題在于, [ [ 1 , 2 ] , [ 8 , 11 , 5 ] , [ 4 , 5 ] ] 也是同樣合法的,因為其中元素的類型都相同。盡管這樣并不靠譜,但編譯時并不會報錯。然而一個長度為 2 的 Tuple (也可以稱作序?qū)Γ琍air),是一個獨立的類 型,這便意味著一個包含一組序?qū)Φ?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)]
這告訴我們說程序在試圖將序?qū)腿M置于同一List中,而這是不允許的。同樣 [ ( 1 , 2 ) ,( " one " , 2 ) ] 這樣的 List 也不行,因為 其中的第一個 Tuple 是一對數(shù)字,而第二個 Tuple 卻成了一個字符串和一個數(shù)字。 Tuple 可以用來儲存多個數(shù)據(jù),如,我們要表示一個人的名字與年 齡,可以使用這樣的 Tuple : ( " Christopher " , " Walken " , 55 )。從這個例子里也可以看出,Tuple 中也可以存儲 List 。
使用 Tuple 前應當事先明確一條數(shù)據(jù)中應該由多少個項。每個不同長度的 Tuple 都是獨立的類型,所以你就不可以寫個函數(shù)來給它追加元素。而唯一能做的,就是通過函數(shù)來給一個 List 追加序?qū)?,三元組或是四元組等內(nèi)容。
可以有單元素的 List ,但 Tuple 不行。想想看,單元素的 Tuple 本身就只有一個值,對我們又有啥意義?不靠譜。
同 List 相同,只要其中的項是可比較的, Tuple 也可以比較大小,只是你不可以像比較不同長度的 List 那樣比較不同長度的 Tuple 。如下是兩個有用的序?qū)Σ僮骱瘮?shù):
fst返回一個序?qū)Φ氖醉棥?/p>
ghci> fst (8,11)
8
ghci> fst ("Wow", False)
"Wow"
snd 返回序?qū)Φ奈岔棥?/p>
ghci> snd (8,11)
11
ghci> snd ("Wow", False)
False
Note:這兩個函數(shù)僅對序?qū)τ行?,而不能應用于三元組,四元組和五元組之上。稍后,我們將過一遍從Tuple中取數(shù)據(jù)的所有方式。
有個函數(shù)很 cool ,它就是 zip 。它可以用來生成一組序?qū)?(Pair) 的 List 。它取兩個 List ,然后將它們交叉配對,形成一組序?qū)Φ?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 。第一個元素配第一個,第二個元素配第二個..以此類推。注意,由于序?qū)χ锌梢院胁煌念愋停瑉ip函數(shù)可能會將不同類型的序?qū)M合在一起。若是兩個不同長度的 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 的問題:如何取得所有三邊長度皆為整數(shù)且小于等于10,周長為 24 的直角三角形?首先,把所有三遍長度小于等于 10 的三角形都列出來:
ghci> let triangles = [ (a,b,c) | c <- [1..10], b <- [1..10], a <- [1..10] ]
剛才我們是從三個 List 中取值,并且通過輸出函數(shù)將其組合為一個三元組。只要在 ghci 下邊調(diào)用 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]
已經(jīng)差不多了。最后修改函數(shù),告訴它只要周長為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)]
得到正確結(jié)果!這便是函數(shù)式編程的一般思路:先取一個初始的集合并將其變形,執(zhí)行過濾條件,最終取得正確的結(jié)果。
更多建議: