模式比對/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
怎麼使用這篇介紹到的語法,看範圍、看作用,也看可讀性,在這之間作個權衡!


