到目前為止,介紹了不少 Haskell 基本語法,當然,還有些進階語法沒談到,不過,暫且換個方向,來看看 Haskell 中提供的標準模組中,有哪些基本常用的函式可以使用,詳細介紹每個 API 如何使用其實也蠻沒意義的,這是 API 文件該做的事,這邊的重點還是擺在一個足夠讓你往後探索的基礎上。
Hackage 與 Hoogle
Haskell 可用的套件相關說明資訊,可以在 Hackage 找到,標準套件可以在 The base package 找到。
在開始之前,得介紹一下 Hoogle,一個 Haskell API 搜尋引擎,搜尋頁面 https://www.haskell.org/hoogle/,對於查詢 API 如何使用相當方便:
Data.List 模組
之前介紹過不少 List 的應用,也用過幾個函式,像是 ++
、head
、tail
等,這些函式實際上是位於 Data.List
模組,不過這幾個函式因為常用,基於方便將之匯入了 Prelude
模組之中,因此,之前不用特別 import
也能使用。
如先前介紹,++
可以串接兩個 List,這沒什麼好多做解釋的,到是來看看怎麼實現會比較有趣:
add :: [a] -> [a] -> [a]
add [] xs2 = xs2
add (x:[]) xs2 = x : xs2
add (x:xs) xs2 = x : add xs xs2
這個 add
函式,用起來就像 ++
,例如:
從 add
實作中可以看到,它會走訪第一個 List,因此若第一個 List 很長,使用 add
會比較沒效率,也就是說,對於 ++
左邊的 List 很長時,使用 ++
串接會沒有效率,因此,如果是單純地在 List 中收集元素,應當避免 xs ++ [1]
、xs ++ [2, 3]
這樣的動作,改使用 1 : xs
、2 : 3 : xs
會是比較好的選擇。
依此類推的話,就是說,對於字串的串接,若 ++
左邊的字串很長,也要避免使用 ++
來串接,例如,單純地收集字元的話,應避免使用 xs ++ "a"
、xs ++ "bc"
,而可以改用 'a' : xs
、b : c : xs
,會是比較好的選擇。
head
、tail
實際上都可以改用 Pattern Matching,視情況而定,有時用 head
、tail
方便或易讀,有時用 Pattern Matching 方便或易讀,與它們相對的是 init
與 last
,分別取首清單與尾元素,實作上像是:
init' :: [a] -> [a]
init' [] = error "empty list"
init' (x:[]) = []
init' (x:xs) = x : init' xs
last' :: [a] -> a
last' [] = error "empty list"
last' (x:[]) = x
last' (_:xs) = last' xs
對於一個元素在不在 List 中,可以使用 elem
,如果想知道索引位置,可以使用 elemIndex
,後者沒有匯入 Prelude
,需要 import Data.List
才能使用,來看看怎麼實現:
elem' :: Ord a => a -> [a] -> Bool
elem' _ [] = False
elem' ele (x:xs) = if ele == x then True else elem' ele xs
elemIdx :: (Eq a, Num a) => a -> [a] -> Maybe a
elemIdx _ [] = Nothing
elemIdx ele xs = idxOf xs 0
where
idxOf (x:xs) idx =
if ele == x then Just idx
else if null xs then Nothing else idxOf xs (idx + 1)
elem'
比較簡單,就是傳回 True
或 False
,而在 elemIdx
這方面,因為元素可能不存在於 List 中,因此搜尋結果可能有索引值也可能沒有,因此,傳回型態會是 Maybe
。
像這樣來試著做一些基本函式的實作,對於模組中函式的原理與使用,就會有比較多的認識。
關於 fold 的相關函式
在〈Haskell Tutorial(7)filter、map、fold 模式〉中,談過 fold 模式,當時的 fold
函式實現方式,其實就類似 Haskell 內建的 foldr
函式,不過,實際的 foldr
函式還接受一個起始值,例如:
實現的方式會像是:
foldR :: (a -> b -> b) -> b -> [a] -> b
foldR f z [] = z
foldR f z (x:xs) = x `f` (foldR f z xs)
之所以稱之為 foldr
,是因為這就像是在折紙,把 1
、2
、3
、4
、5
由左而右畫在紙上,你就是從右折紙,每折一次,就將上次執行結果與左邊的元素用指定的函式處理,foldr
的 r 就是 right 的意思,記得,foldr
接受的函式,左邊參數當次的元素,第右參數是上一次計算的結果,記法是,每次折紙時處理的是左邊的元素。以下是 foldr (+) 0 [1, 2, 3, 4, 5]
,在走訪整個 List 到最右端之後,接下來就是:
1 + (2 + (3 + (4 + (5 + 0)))
1 + (2 + (3 + (4 + 5)))
1 + (2 + (3 + 9))
1 + (2 + 12)
1 + 14
15
有 foldr
,那是不是表示有 foldl
?是的!foldl
也像是在折紙,foldl (+) 0 [1, 2, 3, 4, 5]
的話,就像是把 1
、2
、3
、4
、5
由左而右畫在紙上,你就是從左折紙,每折一次,就將上次執行結果與右邊的元素用指定的函式處理,實作上像是:
foldL :: (a -> b -> a) -> a -> [b] -> a
foldL f z [] = z
foldL f z (x:xs) = let z' = z `f` x
in foldL f z' xs
記得,foldl
接受的函式,左邊參數是上一次計算的結果,右邊參數是當次的元素,與 foldr
接受的函式是相反的,記法是,每次折紙時處理的是右邊的元素。單從 foldL
示範實作看來,如果執行 foldl (+) 0 [1, 2, 3, 4, 5]
,過程會像是:
foldl f (0 + 1) [2, 3, 4, 5]
foldl f (1 + 2) [3, 4, 5]
foldl f (3 + 3) [4, 5]
foldl f (6 + 4) [5]
foldl f (10 + 5) []
15
這就像是逐步消減 List 來計算,因此,foldl
有時在其他語言中稱為 reduce,不過,上面的過程,應該是嚴格版本的 foldl'
之行為,foldl'
不會進行惰性求值,而 foldl
會進行惰性求值,因此,foldl
實際在運算時,會像是:
foldl f (0 + 1) [2, 3, 4, 5]
foldl f ((0 + 1) + 2) [3, 4, 5]
foldl f (((0 + 1) + 2) + 3) [4, 5]
foldl f ((((0 + 1) + 2) + 3) + 4) [5]
foldl f (((((0 + 1) + 2) + 3) + 4) + 5) []
foldl f ((((1 + 2) + 3) + 4) + 5) []
foldl f (((3 + 3) + 4) + 5) []
foldl f ((6 + 4) + 5) []
foldl f (10 + 5) []
foldl f 15 []
15
因此,使用 foldl
處理很長的 List 時,常會遇到 stackoverflow 的問題,這時可改用嚴格版本的 foldl'
。
如果指定的處理函式不具結合律,應瞭解 foldl
、foldr
的差別,例如 +
、*
具有結合律,因為 (a + b) + c
與 a + (b + c)
是相同的,-
就不具結合律,例如 foldl (-) 0 [1 .. 10]
結果是 -55
,而 foldr (-) 0 [1 .. 10]
結果是 -5
。
有時指定的函式本身具有結合律,也要留意一下函式本身的特性,例如 foldl (++) "" ["Justin", "Monica", "Irene"]
,雖然 ++
具結合律,不過在上面討論過,++
會將左邊的 List 整個走訪過一遍,因此左邊的 List 若很長,會有效率上的問題,執行 foldl (++) "" ["Justin", "Monica", "Irene"]
的話,最後會有 "JustinMonica" ++ "Irene"
的結果,也就是左邊的字串會越來越長,++
效率就會越來越差,換用 foldr (++) "" ["Justin", "Monica", "Irene"]
會比較好一些,類似地,如果結果是要產生新的 List,使用 foldr
效率會好一些。
foldl
、foldr
必須指定初值,foldl1
、foldr1
會使用折紙時第一個遇到的元素當初值,因此使用 foldl1
處理 [1, 2, 3, 4, 5]
時,因為是從左折,初值就是 1
,使用 foldr1
處理 [1, 2, 3, 4, 5]
時,因為從右折,初值就是 5
。
恒等值的考量
除了惰性、結合律以及指定處理函式本身的特性考量之外,fold 相關函式還可以有讓初值為恒等值(Identity)的考量。所謂恒等值,是指使用指定的函式 f
來處理恒等值 n
及 List 中任一元素 m
,結果還是還是 m
,也就是 f m n
結果會是 m
,例如,指定函式為 +
而恒等值為 0
時, 0 + 1
還是 1
、0 + 2
還是 2
,這麼做的目的在於,如果 List 很長,打算將之切為數等分來計算時,也不致於因為發生累計問題(處理函式要具有結合律)。
例如,foldl (+) 0 [1 .. 100]
的結果是 5050
,也許將來你可以實現一個 parallelFoldl
,會將 [1 .. 100]
切成四份平行運算,實作概念相當於 (foldl (+) 0 [1 .. 25]) + (foldl (+) 0 [26 .. 50]) + (foldl (+) 0 [51 .. 75]) + (foldl (+) 0 [76 .. 100])
,這樣就沒有問題,結果還是 5050
。
如果因為某個原因,希望從 5
開始累加,在不平分處理下,雖可以寫成 foldl (+) 5 [1 .. 100]
,結果就是 5055
,不過,若 foldl
改為自定義的 parallelFoldl (+) 5 [1 .. 100]
,結果會相當於 (foldl (+) 5 [1 .. 25]) + (foldl (+) 5 [26 .. 50]) + (foldl (+) 5 [51 .. 75]) + (foldl (+) 5 [76 .. 100])
,也就是 5070
,那就不對了,對於這個需求,一開始應該是寫成 5 + foldl (+) 0 [1 .. 100]
才是正確的,這樣若 foldl
改為 parallelFoldl
,結果就相當於 5 + (foldl (+) 0 [1 .. 25]) + (foldl (+) 0 [26 .. 50]) + (foldl (+) 0 [51 .. 75]) + (foldl (+) 0 [76 .. 100])
,這樣就沒有問題。
誰能處理無限長 List?
foldl
、foldr
經常被討論的問題之一就是,誰能處理無限長的 List?這問題就在於,無限長的 List 長什麼樣呢?[1 ..]
!從左開始折的話,這紙一定是折不盡的,可是,如果從右邊開始折,開始折的邊界又在哪呢?再來看一下 foldR
:
foldR :: (a -> b -> b) -> b -> [a] -> b
foldR f z [] = z
foldR f z (x:xs) = x `f` (foldR f z xs)
為了完成 f
的處理,除了 x
之外,還得等待 f
第二個引數,也就是 foldR f z xs
的結果,這就是上面對 foldr
指定 +
時,必須走訪整個 List 才能得到結果的情況。那麼,如果 f
不用等待第二個引數呢?像是指定 \ele y -> ele
會如何?
這就是了,真的是蠻聰明的,雖說打算從右折無限長的 List,不過,每次都只傳回當次元素,一路往左看到,盡頭不就是個 1
嗎?
不過,指定 \ele y -> ele
看起來沒什麼用,來想個有用的好了!嗯!對一個元素為 Bool
的無限長 List 做 ||
處理如何(裏頭可能有 True
也可能有 False
)?在 Data.List
中有個 iterate
可以建立無限長的 List,例如,iterate not True
就會產生 [True, False, True, False, True ...]
一直循環下去,那麼 foldr (||) False $ iterate not True
會如何?
簡單!雖說是無限長,不過 ||
的特性是,只要有一個為 True
,就可以判定整個為 True
了,不用等另一個引數處理完畢了,那麼 foldr (||) $ iterate not True
又有什麼用?只能判定 List 是不是有 True
存在嘛!那麼這個呢?
any' :: (a -> Bool) -> [a] -> Bool
any' f = (foldr (||) False) . (map f)
f
是可將值對應至 Bool
的函式,map f
就可以將任意 List 轉為 Bool
元素的 List
,這個 List 交給 foldr (||) False
,就成了一個,可以判斷 List 中,是否有符合特定條件的元素存在之函式,例如,想知道無限長的 List 中,是不是有能某值整除的數:
在 Data.List
中,已經存在 any
這個函式,不過以上的探討,是個蠻有趣的過程。
如果你想快速地瞭解 Data.List
中有哪些現成的函式可以使用,可以參考 Data.List 中的說明。