模式比對/Guard 運算
February 3, 2022在處理過一陣子 list 之後,會發現常有個固定模式,就是取「頭」取「尾」,這表示你面對的問題,經常會以循序處理,例如〈map/filter/fold〉可以處理的問題。
list 的結構
list 是個有序結構,而且經常會以循序處理,Haskell 最初在定義 list 時,就是考量了這點來定義結構,以〈map/filter/fold〉中實作的 map'
為例:
map' :: (a -> a) -> [a] -> [a]
map' mapper lt =
if null lt then lt
else (mapper $ head lt) : (map' mapper $ tail lt)
null lt
檢查表示處理資料時,list 在結構會有 []
的可能性,當你只有一個元素時,例如,[10]
,首個元素會是 10,未走訪清單會是 []
,這就是你面對的 list。
Haskell 在定義 list 時,就是基於首元素、尾清單的結構,在〈初探 list 操作〉中也提過,[1, 2, 3]
其實就是 1:2:3:[]
的語法蜜糖,而對於 1 來說,2:3:[]
其尾清單,對於 2 來說,3:[]
是其尾清單,對於 3 來說,[]
是其尾清單。
也就是說,對於一個非空的 list,總是可以折解 x:xs
的模式,x
是首元素,xs
是尾清單。
模式比對
在 Haskell 中,若你知道資料的結構,就可以依結構來拆解資料,例如:
ghci> let x:xs = [1, 2, 3, 4]
ghci> x
1
ghci> xs
[2,3,4]
ghci>
因而像方才的 map'
,就可以改寫為以下形式:
map' :: (a -> a) -> [a] -> [a]
map' mapper lt =
if null lt then lt
else
let x:xs = lt in (mapper x) : (map' mapper xs)
不過對於結構模式的比對,Haskell 提供方便的 case/of
語法:
map' :: (a -> a) -> [a] -> [a]
map' mapper lt =
case lt of [] -> []
x:xs -> (mapper x) : (map' mapper xs)
如果 case/of
的使用很單純,像是以上的 map'
實作,會直接在函式上定義模式比對:
map' :: (a -> a) -> [a] -> [a]
map' _ [] = []
map' mapper (x:xs) = (mapper x) : (map' mapper xs)
這種寫法是 case/of
的語法糖;既然可以比對 []
,這表示除了比對結構的模式之外,模式比對也可以直接比對值,因此像〈費式數列〉,使用模式比對來實作的話:
fibonacci :: Int -> Int
fibonacci 0 = 0
fibonacci 1 = 1
fibonacci n = fibonacci (n - 1) + fibonacci (n - 2)
這不就是與費式數的定義是對應嗎?這也是為何純函數式,會被稱為宣告式的關係:
F₀ = 0
F₁ = 1
Fₙ = Fₙ₋₁ + Fₙ₋₂
Haskell 總是優先思考資料該具有什麼樣的結構,因此在拿到資料時,總是能基於結構的模式來進行比對,在 Haskell 中,模式比對是處理資料時再自然不過的一種方式。
case/of、where 與 let
方才談到如果 case/of
的使用很單純,會直接在函式上定義模式比對;case/of
是個運算式,若想將 case/of
的運算結果拿來進一步處理,就會直接使用 case/of
,例如:
descOddEven :: Int -> String
descOddEven n =
show n ++ " is " ++ case n `mod` 2 of 0 -> "Even"
1 -> "Odd"
上面的 descOddEven
不是很好讀,結合 where
來定義一個 oddOrEven
或許會好一些:
descOddEven :: Int -> String
descOddEven n =
show n ++ " is " ++ oddOrEven n
where oddOrEven n = case n `mod` 2 of 0 -> "Even"
1 -> "Odd"
這個例子也可以改為 let in
:
descOddEven :: Int -> String
descOddEven n =
let oddOrEven n = case n `mod` 2 of 0 -> "Even"
1 -> "Odd"
in show n ++ " is " ++ oddOrEven n
差別在於,where
中定義的名稱,對整個函式中可見,而 let
中定義的名稱,只對 in
的運算式可見,let in
是個運算式,然而 where
只是語法,不能在一個 =
後接上 where
,或者在某運算式之後接上 where
。
方才談到,直接在函式上定義模式比對,不過是 case of
的語法糖,那麼,在 where
之後可以如下使用模式比對就不足為奇了,下面這個函式可以求得費式數列:
fibonacciLt :: Int -> [Int]
fibonacciLt n =
[fibonacci x | x <- [0 .. n]]
where fibonacci 0 = 0
fibonacci 1 = 1
fibonacci n = fibonacci (n - 1) + fibonacci (n - 2)
使用 let
定義函式時,當然也可以用模式比對:
fibonacciLt :: Int -> [Int]
fibonacciLt n =
let fibonacci 0 = 0
fibonacci 1 = 1
fibonacci n = fibonacci (n - 1) + fibonacci (n - 2)
in [fibonacci x | x <- [0 .. n]]
Guard 運算
case.of
或函式的模式比對,是依資料的結構模式來比對,那麼,對於大於、小於、大於等於、小於等於呢?使用 if else
當然是可以解決問題,不過,如果要比較的條件很多,就會形成一長串的 if else
:
grade :: Int -> String
grade score =
if score >= 90 then "A"
else if score >= 80 then "B"
else if score >= 70 then "C"
else if score >= 60 then "D"
else "E"
這種情況下,你可以使用 Guard 運算:
grade :: Int -> String
grade score
| score >= 90 = "A"
| score >= 80 = "B"
| score >= 70 = "C"
| score >= 60 = "D"
| otherwise = "E"
Guard 是個布林運算,只有在 |
的條件式為 True
的情況下,才會傳回 =
右邊的值。
where
中也可以用 guard:
descGrade :: Int -> String
descGrade score =
"Your score is " ++ show score ++ " and is level " ++ grade score
where grade score
| score >= 90 = "A"
| score >= 80 = "B"
| score >= 70 = "C"
| score >= 60 = "D"
| otherwise = "E"
let
中也可以,只是不見得好讀罷了:
descGrade :: Int -> String
descGrade score =
let grade score
| score >= 90 = "A"
| score >= 80 = "B"
| score >= 70 = "C"
| score >= 60 = "D"
| otherwise = "E"
in "Your score is " ++ show score ++ " and is level " ++ grade score
怎麼使用這篇介紹到的語法,看範圍、看作用,也看可讀性,在這之間作個權衡!