模式比對/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

怎麼使用這篇介紹到的語法,看範圍、看作用,也看可讀性,在這之間作個權衡!

分享到 LinkedIn 分享到 Facebook 分享到 Twitter