iT邦幫忙

2023 iThome 鐵人賽

DAY 8
0
Software Development

Haskell 從入門到放棄系列 第 8 篇

[Haskell 從入門到放棄] Day 08 - Pattern Matching (2)

  • 分享至 

  • xImage
  •  

今天我們將繼續了解 pattern matching 的語法。

Guard

昨天只講到 pattern 如果是「特定值」那我們做什麼行為,但如果我們是有多個參數要一起參與判斷式的組成,那感覺就不太夠用了。

所以 Haskell 為我們提供了 Guard ,我們先來寫了一個 function 來判斷三個邊長是否可以構成一個合法的三角型

calcTriangle :: Int -> Int -> Int -> String
calcTriangle a b c
    | a + b <= c || a + c <= b || b + c <= a = "Invalid"
    | a == b && b == c = "Equilateral"
    | a == b || b == c || a == c = "Isosceles"
    | otherwise = "Scalene"

Guard 的用法很簡單,只要用 | 並在後面加上一個最後會回傳 Bool 的 expression 以及match到後要回傳的 expression ,然後依序從上到下如果失敗就會往下繼續 match 。

首先我們用三角形任意兩邊一定大於第三邊來當作判斷,如果成立為 True 就會回傳 "Invalid" ,如果不是就會接到下一個 guard ,接下來我們來判斷如果三邊相等那就代表他為正三角形所以回傳 "Equilateral" ,如果還不是就會繼續到下一個 guard ,以此類推直到最後的 otherwise 。

otherwise 就是 True ,意思就是一定會執行右邊的 expression ,通常就是放在最後面有點類似其他語言的 switch case 的 default 之類的用途。但如果沒有提供 otherwise 且最後沒有匹配到任何一個 guard ,那跟一般的 pattern matching 一樣最後會拋出錯誤。

Where

上面的程式感覺還是有點羅嗦,有什麼辦法可以簡化重複的 pattern 嗎?這時候我們可以使用 where 幫定義我們需要重複的 expression。

calcTriangle' :: Int -> Int -> Int -> String
calcTriangle' a b c
    | a + b <= c || a + c <= b || b + c <= a = "Invalid"
    | abEq && bcEq = "Equilateral"
    | abEq || bcEq || acEq = "Isosceles"
    | otherwise = "Scalene"
    where
        abEq = a == b
        bcEq = b == c
        acEq = a == c

where 的用法就是放在 | 的後面然後定義名稱及值 ,所以我們可以 a == b 等等的布林運算定義成另外一個名稱,然後原本的 | a == b && b == c = "Equilateral" 就可以變成 | abEq && bcEq = "Equilateral" 讓我們在使用 pattern matching 時增加整體的可讀性,且 where 中所綁定的名稱只在這個 function 也就是 calcTriangle' 裡才有用。

除了綁定數值以外,where 也可以用來綁定 function

calcTriangle'' :: Int -> Int -> Int -> String
calcTriangle'' a b c
    | isValid a b c || isValid a c b || isValid b c a= "Invalid"
    | abEq && bcEq = "Equilateral"
    | abEq || bcEq || acEq = "Isosceles"
    | otherwise = "Scalene"
    where
        abEq = a == b
        bcEq = b == c
        acEq = a == c
        isValid x y z = x + y <= z

我們可以把 a + b <= c 這類的運算改用 where 來定義成一個 fucntion ,雖然在這裡看不出有太多的差異或者有什麼理由一定要這樣寫,但假設我們今天有一個很小的 function 想要抽出原本的 expression 但又不需要寫成全域的 function 我們也可以用 where 來協助我們達成這件事情。

拿我們前幾天的 List comprehension 的例子舉例:

[email | email <- emailList, elem '@' email && elem '.' email ] 

原本我們有一個 List comprehension 然後他的限制條件是判斷 email 裡有無 @ 且 . 但假設我們想把這個判斷抽出來也可以使用 where 來達成

filterValidEmail :: [String] -> [String]
filterValidEmail xs = 
    [email | email <- xs,  isValid email ]
    where 
        isValid email = '@' `elem` email &&  '.' `elem` email

這裡可以看到我們使用 where 來將 isValid 的邏輯抽出來,然後我再把原本的限制條件改為使用 isValid

總結來說 where 有點像是局部變數的概念,我們可以將某些數值或者 function 固定在某個 scope 有作用而已。

雖然沒有特別提到,但在 Haskell 中 function 也是一種變數。

Let

說到區域變數,也許有人會疑惑那跟 let 又差在哪裡?

我們先把上面的程式碼改為用 let in

filterValidEmail' :: [String] -> [String]
filterValidEmail' xs =
  let
    isValid email = '@' `elem` email && '.' `elem` email
  in
    [email | email <- xs, isValid email]

跟之前所介紹的一樣 let 綁定的名稱只在 in 裡面有作用,let 與 where 的根本差異是 in 後面是接一個 expression 且 let 可以在任何地方使用

-- ghci 
foo = let a = 200 in a +1
foo -- 201

foo = [let a = 200 in a+1 , 202]
foo -- [201,202]

那至於該選擇 let 還是 where 呢?只能說看習慣與場合,但多數情況**「我個人認為」** where 比較好讀一點點

Case

case 使用其實就真的很像其他語言的 switch case 一樣 ,我們 case 一個值然後根據他的值而執行什麼。

sumList :: Num a => [a] -> a
sumList [] = 0
sumList (x:xs) = x + sumList xs

sumList' :: Num a => [a] -> a
sumList' list = case list of
    [] -> 0
    (x:xs) -> x + sumList' xs

那這樣 sumList 跟 sumList' 有什麼差?答案就是「沒差」,我們在 function 使用的 pattern matching 就只是 case 的語法糖而已。

但 case 看的出來也是一種 expression 所以我們一樣能在任何地方使用

foo x =
    [case x of
        'a' -> 10
        'b' -> 11
        'c' -> 12
        'd' -> 13
        'e' -> 14
        'f' -> 15
        _ -> error "Invalid hex digit"
    ,1]

今天大致上講到了大部分 pattern matching 的用法及特性了,相信各位應該對於 Haskell 的認識又更深了一層。

想起之前與同事有討論過 pattern matching (沒有特指哪一個的語言實作)跟 js/ts 的 switch case 差在哪裡,最直觀的差異是一個通常是 expression 而一個是 statement ,這個差異就就讓我們在使用場景差很多了,
因為只要是 expression 我可以大部分地方使用並取得我們要的值,但如果是 statement 我們可能需要用一個 function 包裝起來最後回傳一個值或者是一個 mutable variable 在各個 case 中間去更新值。

以及 pattern matching 不只是匹配 「值」 而已,而是連 「結構」 都可以匹配,以及匹配對象(或者該說流程的分支)的彈性都強大許多。也不難想像會什麼會有ts-pattern這個 library 的出現,畢竟 pattern matching 的好,用過就知道。


今天的程式碼:https://github.com/toddLiao469469/30days-for-haskell


上一篇
[Haskell 從入門到放棄] Day 07 - Pattern Matching
下一篇
[Haskell 從入門到放棄] Day 09 - 終於來到 higher order function
系列文
Haskell 從入門到放棄 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
yaoooo
iT邦新手 5 級 ‧ 2023-09-23 22:20:24

一班一般

thanks~

我要留言

立即登入留言