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