Haskell Tutorial(11)case、let 、where 與 guard


前面幾篇講了許多觀念,這篇比較瑣碎,主要是將一些函式中可用的語法一次講講,這幾個語法基本上是關於比對、流程語法、名稱等,彼此可以交互組合應用,某些程度上看這篇應該還輕鬆一些,多看幾個例子,就會知道怎麼應用了。

case .. of ..

在〈Haskell Tutorial(10)從 Tuple 初試模式比對〉談到了模式比對,在將值指定給變數的場合,都可以看到模式比對的應用,其中當然也包括了函式的參數,例如〈filter、map、fold 模式〉中的 map' 函式:

map' :: (Int -> Int) -> [Int] -> [Int]
map' mapper lt =
    if null lt then lt
    else (mapper $ head lt) : (map' mapper $ tail lt)

可以改寫為模式比對方式:

map' :: (Int -> Int) -> [Int] -> [Int]
map' _ []          = []
map' mapper (x:xs) = mapper x : map' mapper xs

實際上,前者不過是下面這個寫法的語法蜜糖:

map' :: (Int -> Int) -> [Int] -> [Int]
map' mapper lt =
    case lt of []   -> []
               x:xs ->  mapper x : map' mapper xs

在函式參數上直接使用模式比對,是 case of 的一個特例,就 map' 函式這個例子來說,沒必要改用 case of 來自找麻煩,最主要的,case of 是個運算式,因此它可以用在任何地方:

descOddEven :: Int -> String
descOddEven n = 
    show n ++ " is " ++ case n `mod` 2 of 0 -> "Even"
                                          1 -> "Odd"

where 與 let

上頭的 descOddEven 不是很好讀,下面這個或許會好一些:

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"

注意到,grade score 旁沒有接上 == 是接在每個 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

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