Haskell Tutorial(6)從 List 處理初試函數式風格 << 前情
在〈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' 加上你指定的過濾條件,來做任何你想要過濾的事了:
![使用 filter'](https://www.codedata.com.tw/wp-content/uploads/2014/12/haskell-tutorial-7-1.jpg)
看到了嗎?因為你將 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 來做加總或相乘的動作了:
![使用 fold](https://www.codedata.com.tw/wp-content/uploads/2014/12/haskell-tutorial-7-2.jpg)
感覺很神奇嗎?若從結構上來看,無疑地,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 ,概念類似,不過有些比較深入的細節要探討,以後有機會再來談!
後續 >> Haskell Tutorial(8)懶惰是美德之一
|
Jeff Chen
12/15
在大部分的 haskell code 常見到使用 pattern matching 抓掉 “null”
如下:
sum' :: [Int] -> Int
sum' [] = 0 -- 或者回傳error
sum' (x:xs) = x + sum' xs
對於判斷 base case 的直覺可以多加練習:)
---
嚴格來說 "null" 發生在 nil 這個 list 上(null nil == True )
而 nil 又被簡寫為 "[]"