在〈Haskell Tutorial(6)從 List 處理初試函數式風格〉最後出的題目應該很簡單吧!解答如下 …
sum' :: [Int] -> Int
sum' lt =
if null lt
then 0
else head lt + (sum' $ tail lt)
main = do
putStrLn $ show $ sum' [] -- 顯示 0
putStrLn $ show $ sum' [1, 2] -- 顯示 3
putStrLn $ show $ sum' [3, 4, 5] -- 顯示 12
打鐵趁熱,繼續來看更多的函數式風格,你會發現沒那麼難!該從哪邊開始呢?有稍微涉獵過函數式的開發者,應該多少都有聽過 filter
、map
之類的,在〈Haskell Tutorial(5)如喝水般自然的高階函式〉中也會稍微談到,就來介紹一下吧!
List 的 filter 模式
實際上,filter
函式是處理 List 時經常遇到的一個模式實現,就稱它為 filter 模式好了,此模式的發掘很簡單,舉例來說,若你想寫一個 greaterThan5
函式,如果 List 為空,那就直接回傳,否則測試首元素是否為大於 5
,是的話留下,不是就別理它了,然後測試剩餘清單,因此函式會是:
greaterThan5 :: [Int] -> [Int]
greaterThan5 lt =
if null lt then lt
else
if head lt > 5 then head lt : (greaterThan5 $ tail lt)
else greaterThan5 $ tail lt
如果想寫一個 smallerThan4
函式呢?
smallerThan4 :: [Int] -> [Int]
smallerThan4 lt =
if null lt then lt
else
if head lt < 4 then head lt : (smallerThan4 $ tail lt)
else smallerThan4 $ tail lt
如果你真的誠實地自己寫了這個 smallerThan4
,而不是從上頭 greaterThan5
複製、貼上之後,把 > 5
修改為 < 4
,greaterThan5
修改為 smallerThan4
,應該也發現了,兩個結構是一模一樣地,就只有 > 5
、< 4
不同以及函式名稱不同,> 5
、< 4
是函式,這在〈Haskell Tutorial(5)如喝水般自然的高階函式〉中說過了,為什麼不寫個 filter'
呢?
filter' :: (Int -> Bool) -> [Int] -> [Int]
filter' cond lt =
if null lt then lt
else
if cond $ head lt then head lt : (filter' cond $ tail lt)
else filter' cond $ tail lt
如果你還不是很習慣 Haskell 的型態宣告方式,暫時忽略它沒有關係,Haskell 會試為你推斷出一個適當的型態,雖然我還是建議試著看懂它,因為其實沒那麼難!好吧!我承認,->
是多了一點!
總之,這個 filter'
的結構跟前兩個函式還是很像,只不過,現在可以用這個 filter'
加上你指定的過濾條件,來做任何你想要過濾的事了:
看到了嗎?因為你將 List 分而治之,因而,很容易發覺 greaterThan5
與 smallerThan4
有著類似的結構,因而重構出 filter'
函式,單看這函式也很簡單,如果 List 為空,那就直接回傳,否則測試首元素,True
的話留下,不是就別理它了,然後過濾剩餘清單。
List 的 map 模式
類似地,map
函式是處理 List 時經常遇到的一個模式實現,照樣也稱它為 map 模式好了。照例從一個簡單需求開始,將一個整數 List 全部加 5,傳回一個新的 List:
map5To :: [Int] -> [Int]
map5To lt =
if null lt then lt
else head lt + 5 : (map5To $ tail lt)
夠簡單!應該稍微熟悉這種思考方式了!空 List 就直接回傳,要不然就是首元素加 5,然後剩下的 List 繼續 map5To
。如果要寫個 multify3To
呢?也許你會阻止我寫出實作了,因為必然會有類似的結構,僅僅只是將 (+ 5)
改為 (* 3)
,然後函式名稱改一改而已,(+ 5)
、(* 3)
沒問題地就是函式,直接來寫個 map'
函式:
map' :: (Int -> Int) -> [Int] -> [Int]
map' mapper lt =
if null lt then lt
else (mapper $ head lt) : (map' mapper $ tail lt)
map'
本身也不難理解,如果是空 List 的話直接回傳,要不然就用 mapper
處理一下首元素,剩下的 List 繼續 map'
,遞迴就是這麼一回事,只要關心這次遞迴該做什麼就好,剩下的 List 如何處理?那是下一次 map'
該擔心的事。
當然,這邊的 filter'
、map'
只是針對 Int
,Haskell 內建的 filter
、map
不只針對 Int
,這是為了簡化範例而已,對應瞭解這兩個模式而言是足夠了。
List 的 fold 模式
接下來要談的 fold 模式稍微囉嗦一點,出發點是一開始看到的 sum'
函式,一般來說,如果是空 List,加總值應該就是 0
,不過,也許你會有不同的意見,既然要對清單中的元素加總,那麼傳入空 List 就應該是個錯誤,如果是這樣的話,來修改一下 sum'
函式:
sum' :: [Int] -> Int
sum' lt =
if null $ tail lt
then head lt
else head lt + (sum' $ tail lt)
因為 tail
函式若傳入一個空 List,就會引發錯誤,因此,sum'
現在也不能接受空 List 了,接下來,來看看如果你要寫個將 List 所有元素乘在一起的 multiplyTogether
該怎麼寫:
multiplyTogether :: [Int] -> Int
multiplyTogether lt =
if null $ tail lt
then head lt
else head lt * (multiplyTogether $ tail lt)
依照前面的經驗,一眼就可以看出結構很像,那麼就來重構,寫個 fold
如何?
fold :: (Int -> Int -> Int) -> [Int] -> Int
fold fn lt =
if null $ tail lt
then head lt
else fn (head lt) (fold fn $ tail lt)
注意!這次需要的是一個接受兩個引數的函式,因此 fn 參數的型態是 (Int -> Int -> Int)
,現在,你可以用 fold
來做加總或相乘的動作了:
感覺很神奇嗎?若從結構上來看,無疑地,sum'
、multiplyTogether
與 fold
都是類似的,只是這個 fold
直接看,感覺比較抽象,以 fold (+) [1, 2, 3, 4, 5]
來說,fold
的相加順序是 5
會 4
相加得到 9
,然後 9
與 3
相加得到 12
,然後 12
與 2
相加得到 14
,然後 14
與 1
相加得到最後的 15
。
其實這就像是在折紙,把 1
、2
、3
、4
、5
由左而右畫在紙上,你就是從最右邊開始折紙,每折一次,就將上一次執行結果與左邊的元素用 fn
處理,這就是為什麼它叫做 fold
的原因,簡單來說,如果你打算走訪整個 List 來求得單一值,可以使用這個 fold
。
Haskell 中有一些函式以 fold 開頭,像是 foldl
、foldr
、foldl1
、foldr1
,概念類似,不過有些比較深入的細節要探討,以後有機會再來談!