Skip to main content

Bài 04 - Khớp mẫu và biểu thức Case

TÓM TẮT
  • Khớp mẫu trong các hàm
    • Các mẫu bắt tất cả
  • Nhìn kỹ hơn vào danh sách
  • Khớp mẫu
    • Danh sách
    • Bộ dữ liệu
  • Biểu thức Case
  • Kiểu khai báo VS Kiểu biểu thức
Video bài giảng
Chúng tôi đang dịch thuyết minh bài giảng sang tiếng Việt

Khớp mẫu (Pattern matching)

Khớp mẫu là hành động khớp dữ liệu (giá trị, loại, v.v.) với một mẫu, tùy chọn ràng buộc các biến để khớp thành công.

Chúng ta sẽ thảo luận về Khớp mẫu trong ba trường hợp:

  • Khớp mẫu trong định nghĩa hàm.
  • Kết hợp mẫu cho danh sách.
  • Kết hợp mẫu cho bộ dữ liệu.

Nghe có vẻ phức tạp, nhưng nó thực sự khá trực quan khi bạn hiểu rõ về nó. Sẽ rõ như ban ngày sau một vài ví dụ.

Hãy Khớp mẫu với một số hàm!

Khớp mẫu trong các hàm

Bạn còn nhớ hàm specialBirthday từ bài học trước không?

specialBirthday :: Int -> [Char]
specialbirthday age =
if age == 1
then "First birthday!"
else
if age == 18
then "You're an adult!"
else
if age == 60
then "Finally, I can stop caring about new lingo!"
else "Nothing special"

Tôi biết, tôi biết… chúng ta đã khắc phục sự phức tạp đó với guards. Nhưng bây giờ, chúng ta sẽ sáng tạo hơn và giải quyết vấn đề bằng cách Khớp mẫu!

Để Khớp mẫu trên các định nghĩa hàm, chúng ta chỉ cần định nghĩa cùng một hàm nhiều lần, thay thế các tham số bằng các giá trị. Như thế này:

specialBirthday :: Int -> [Char]
specialBirthday 1 = "First birthday!"
specialBirthday 18 = "You're an adult!"
specialBirthday 60 = "finally, I can stop caring about new lingo!"

Hàm của chúng ta đã được định nghĩa! Và nó trông đẹp hơn nhiều so với trước đây!

Nhưng làm như thế nào? Chà, khi được trình bày với mã như thế này, Haskell sẽ cố gắng khớp giá trị của age với định nghĩa đầu tiên. Nếu age /= 1, nó sẽ cố khớp với định nghĩa thứ hai. Nếu age /= 18, nó sẽ cố khớp với định nghĩa thứ ba. Và tiếp tục như vậy cho đến khi giá trị được truyền dưới dạng tham số khớp với một trong các giá trị của định nghĩa.

Và, tất nhiên, tôi chắc rằng bạn đã nhận thấy một vấn đề lớn. Điều gì xảy ra nếu chúng ta chuyển một số khác với số đã định nghĩa? Giống như 29? Chúng ta có thể giải quyết vấn đề đó bằng khớp mẫu tất cả!

Khớp mẫu tất cả

Khai báo của hàm nêu rõ rằng bạn có thể đưa vào bất kỳ giá trị nào thuộc loại Int.

Vì vậy, chúng ta có thể truyền vào 14 hoặc bất kỳ số nào khác. Nhưng hàm nên làm gì nếu chúng ta truyền vào 14? Chúng ta đã không chỉ định nó vì chúng ta không Khớp mẫu cho 14! Vì vậy, chương trình sẽ gặp sự cố 💥 vì không biết cách xử lý giá trị đó! 😱

Bởi vì chúng ta cần hàm hoạt động với bất kỳ giá trị nào mà các kiểu của chúng ta có thể chấp nhận, nên chúng ta cần Khớp mẫu cho tất cả các tình huống có thể xảy ra. Nhưng bạn không thể viết định nghĩa cho mọi giá trị đơn lẻ! Sau đó, bạn có thể làm gì?!?!

Bạn sử dụng một mô hình "khớp mẫu tất cả"!

Mẫu khớp tất cả cho phép bạn cung cấp một định nghĩa mặc định trong trường hợp không có định nghĩa cụ thể nào của bạn phù hợp

Trong trường hợp này, nó sẽ đóng vai trò else ở cuối specialBirthday.

Để sử dụng khớp mẫu tất cả, bạn phải cung cấp tên bắt đầu bằng chữ thường, như agex hoặc yearsSinceThisPoorSoulHasTouchedTheEarth.

Như thế này:

specialBirthday :: Int -> [Char]
specialBirthday 1 = "First birthday!"
specialBirthday 18 = "You're an adult!"
specialBirthday 60 = "finally, I can stop caring about new lingo!"
specialBirthday age = "Nothing special"

specialBirthday 18

"You're an adult!"

Bây giờ, nếu chúng ta chuyển bất kỳ số nào khác với 118, hoặc 60specialBirthday sẽ quy thành thành "Nothing special".

tip

QUAN TRỌNG: Luôn cung cấp đủ Mẫu phù hợp cho tất cả các tình huống có thể xảy ra! Nếu không, bạn sẽ nhận được cảnh báo tiếp theo: 'Pattern match(es) are non-exhaustive In an equation for specialBirthday' (Các) kết hợp mẫu không đầy đủ Trong một phương trình cho ngày đặc biệt.

Một chi tiết quan trọng khác là Haskell so khớp từ trên xuống dưới. Vì vậy, nếu bạn làm điều gì đó như:

specialBirthday :: Int -> [Char]
specialBirthday age = "Nothing special"
specialBirthday 1 = "First birthday!"
specialBirthday 18 = "You're an adult!"
specialBirthday 60 = "finally, I can stop caring about new lingo!"

specialBirthday 60

"Nothing special"

Định nghĩa đầu tiên sẽ nắm bắt tất cả các lần xuất hiện và chúng ta sẽ luôn nhận được "Nothing special" kết quả, bất kể chúng ta truyền vào số nào. Vì vậy, hãy đảm bảo thêm mẫu bắt tất cả làm định nghĩa cuối cùng.

Cuối cùng, chúng ta đã nói rằng bạn có thể tùy ý liên kết các biến với các kết quả khớp thành công và đó là những gì chúng ta vừa làm!

Khi sử dụng specialBirthday, mỗi khi giá trị rơi vào age mẫu bắt tất cả, chúng ta sẽ liên kết giá trị đó với biến age. Cho phép chúng ta sử dụng giá trị bên trong biểu thức của định nghĩa (nó chỉ là một đối số)!:

-- Note: You should know how to use `show`  if you did last week homework.

specialBirthday :: Int -> [Char]
specialBirthday 1 = "First birthday!"
specialBirthday 18 = "You're an adult!"
specialBirthday 60 = "finally, I can stop caring about new lingo!"
specialBirthday age = "Nothing special, you're just " ++ show age

specialBirthday 22

"Nothing special, you're just 22"

Bạn không thể phóng đại mức độ hữu ích của điều này! Bạn đang lọc các giá trị thành những giá trị khớp với một mẫu cụ thể VÀ đồng thời liên kết các giá trị đó với các biến để sử dụng sau này!

Một ví dụ hấp dẫn hơn về cách điều này hữu ích là khi mẫu khớp với các cấu trúc phức tạp hơn như danh sách và bộ dữ liệu. Hãy cùng khám phá điều đó.

Xem xét kỹ hơn các danh sách

Trước khi tìm hiểu về Khớp mẫu với danh sách, chúng ta cần xem xét kỹ hơn về danh sách.

Chúng ta biết rằng toán tử : thêm một phần tử vào đầu danh sách (đặt trước một phần tử):

-- (:) :: a -> [a] -> [a]

3 : [4,5] -- [3,4,5]

'L' : "ook, mom! I'm programming" -- "I'm programming"

[3,4,5]

"Look, mom! I'm programming"

Hãy nhớ khi tôi nói với bạn rằng String là cú pháp ngắn cho [Char]? Chà, hãy sẵn sàng cho một loạt cú pháp ngắn vì cách chúng ta viết danh sách cho đến nay thực sự là cú pháp ngắn cho cách Haskell thực sự nhìn thấy danh sách! Là một danh sách trống được thêm vào trước tất cả các phần tử mà nó chứa! 🤯

[1,2,3,4] == 1:2:3:4:[]  -- True

"Hello!" == 'H':'e':'l':'l':'o':'!':[] -- True

Sử dụng danh sách theo nghĩa đen

Hiện trạng: 1 : 2 : 3 : 4 : []

Tại sao không viết thành: [1, 2, 3, 4]

Sử dụng danh sách theo nghĩa đen

Hiện trạng: 'Xin chào' : '!' : [ ]

Tại sao không viết thành: ['Xin chào', '!']

True

True

Bây giờ, bạn có thể nghĩ: "Tại sao tôi phải quan tâm? Tôi sẽ tiếp tục viết danh sách như mọi khi." Đối với những gì tôi nói: "AHA! MẪU MATCHING!!"

Danh sách Khớp mẫu

Bây giờ chúng ta đã biết danh sách trông như thế nào khi không trang điểm 💅, chúng ta có thể sử dụng nó để Khớp mẫu với các định nghĩa hàm khác nhau tùy thuộc vào cấu trúc của danh sách!

Hãy Khớp mẫu theo nhiều cách khác nhau và tìm hiểu cách thức hoạt động của mã sau:

whatsInsideThisList :: [Int] -> String
whatsInsideThisList [] = "It's empty!"
whatsInsideThisList [x] = "A single element: " ++ show x
whatsInsideThisList [x, y] = "Two elements: " ++ show x ++ " and " ++ show y
whatsInsideThisList (x:y:z:[]) = "The list has three elements: " ++ show [x,y,z]
whatsInsideThisList (x:rest) = "The first element is: " ++ show x ++ ", and there are quite a few more!"

whatsInsideThisList [] -- "It's empty!"
whatsInsideThisList [1, 2] -- "Two elements: 1 and 2"
whatsInsideThisList [1, 2, 3] -- "The list has three elements: [1,2,3]"
whatsInsideThisList [1, 2, 3, 4] -- "The first element is: 1, and there are quite a few more!"

Sử dụng mẫu chữ danh sách

Hiện trạng: (XYZ : [])

Tại sao không: [XYZ]

"It's empty!"

"Two elements: 1 and 2"

"The list has three elements: [1,2,3]"

"The first element is: 1, and there are quite a few more!"

Như bạn có thể thấy, bạn có thể so Khớp mẫu với:

  • Danh sách trống [].
  • Danh sách có kích thước cố định, cả với ([x][x,y]) và không có (x:[]x:y:[]) cú pháp ngắn.
  • Danh sách không trống ở bất kỳ kích thước nào với x:rest. (Thường được sử dụng trong các hàm đệ quy và thường được đặt tên là x:xs.)

Chúng ta bao quanh () các mẫu của hai định nghĩa cuối cùng để chỉ ra rằng hàm lấy mọi thứ bên trong () làm một đối số duy nhất.

Và, bởi vì chúng ta đã ràng buộc các kết quả khớp với các biến (xyzrest), nên bạn có thể sử dụng các biến đó bên trong định nghĩa của hàm.

Nhưng nếu bạn không cần chúng thì sao? Điều gì sẽ xảy ra nếu bạn muốn làm điều gì đó khi một mẫu cụ thể khớp, nhưng không quan tâm đến giá trị/giá trị thực tế?

Liên kết các giá trị và sau đó bỏ qua chúng sẽ làm ô nhiễm môi trường của bạn bằng các biến mà bạn sẽ không bao giờ sử dụng! Nhưng đừng lo lắng. Để đặt quả anh đào lên hàng đầu, bạn có thể bỏ qua dữ liệu mà bạn không quan tâm trong khi Khớp mẫu cho phần còn lại! Hãy xem hàm sau. Nó cho chúng ta biết đâu là phần tử đầu tiên và thứ ba trong danh sách Bool (nếu có):

firstAndThird :: [Bool] -> String
firstAndThird (x:_:z:_) = "The first and third elements are: " ++ show x ++ " and " ++ show z
firstAndThird _ = "Don't have them!"

firstAndThird [True, True, False]

"The first and third elements are: True and False"

Định nghĩa đầu tiên sẽ Khớp mẫu cho bất kỳ danh sách nào có 3 phần tử trở lên, trong khi _ sẽ bỏ qua phần tử thứ hai và phần còn lại của danh sách.

Và đối với bất kỳ danh sách nào khác, chúng ta hoàn toàn bỏ qua nó với _ toàn bộ danh sách.

Tuyệt vời, phải không? Biết được điều này, chúng ta có thể sửa đổi initials hàm của bài học cuối cùng từ đây:

initials :: String -> String -> String
initials name lastName = if name == "" || lastName == ""
then "How was your name again?"
else let x = head name
y = head lastName
in [x] ++ "." ++ [y] ++ "."

initials' "Nikola" "Tesla"

"N.T."

Thành như thế này:

initials' :: String -> String -> String  
initials' (f:_) (l:_) = [f] ++ "." ++ [l] ++ "."
initials' _ _ = "How was your name again?"

initials' "Nikola" "Tesla"

"N.T."

Ngắn gọn và rõ ràng hơn.

Bây giờ hãy xem cách so Khớp mẫu làm cho cuộc sống của chúng ta dễ dàng hơn với các bộ dữ liệu!

Bộ dữ liệu phù hợp với mẫu

Như bạn có thể nhớ lại từ các bài học trước, chúng ta chỉ có thể lấy các phần tử bên trong một cặp (bộ gồm hai phần tử) bằng cách sử dụng hàm fst và snd.

Nếu bạn cần một giá trị từ các bộ dữ liệu lớn hơn thế, thì bạn đang gặp khó khăn. 👀 Nhưng bây giờ bạn đã là một ảo thuật gia Khớp mẫu, bầu trời là giới hạn!

Bạn muốn trích xuất phần tử đầu tiên của bộ 3 phần tử? Không có gì khó cả:

firstOfThree :: (a, b, c) -> a
firstOfThree (x, _, _) = x

firstOfThree (1,2,3)

1

Xong!

Bạn muốn tạo một cặp có phần tử thứ hai và thứ tư của bộ 4 phần tử? Giống như trước!:

pairFromFour :: (a, b, c, d) -> (b, d)
pairFromFour (_, x, _, y) = (x, y)

pairFromFour (1,2,3,4)

(2,4)

BÙM! 💥 Xong! Và bạn có thể tiếp tục nếu bạn muốn. Nhưng, ngay bây giờ, chúng ta sẽ chuyển sang biểu thức case.

Biểu thức case

Với các biểu thức case, chúng ta có thể thực thi một khối mã cụ thể dựa trên mẫu của một biến.

Tương tự như với switch các câu lệnh trong các ngôn ngữ lập trình khác. biểu thức case trông như thế này:

case <Exp> of <Pattern1> -> <Result1>
<Pattern2> -> <Result2>
<Pattern3> -> <Result3>
...

Trong đó giá trị của <Exp> được so sánh với mọi <Pattern> bên trong khối of. Và nếu nó phù hợp, tương ứng <Result> sẽ là giá trị.

note

Lưu ý rằng không có dấu =! Đó là bởi vì toàn bộ biểu thức case chỉ là một biểu thức. Không phải là hàm hay ràng buộc.

Ví dụ, chúng ta có thể viết một hàm nhận vào một Int bộ 3 và kiểm tra xem có bất kỳ phần tử nào trong hàm chứa số 0 không:

checkForZeroes :: (Int, Int, Int) -> String
checkForZeroes tuple3 = case tuple3 of
(0, _, _) -> "The first one is a zero!"
(_, 0, _) -> "The second one is a zero!"
(_, _, 0) -> "The third one is a zero!"
_ -> "We're good!"

checkForZeroes (32,0,256)

"The second one is a zero!"

Và tôi đã có thể nghe thấy bạn nói. "Không phải kết quả cuối cùng giống với kết quả mà chúng ta nhận được khi Khớp mẫu trên các tham số trong định nghĩa hàm sao?"

Vâng… vâng. Về cốt lõi, Khớp mẫu trên các tham số trong định nghĩa hàm chỉ là cú pháp ngắn cho các biểu thức chữ hoa chữ thường! Vì vậy, mã trước đó có thể hoán đổi với mã này:

checkForZeroes :: (Int, Int, Int) -> String
checkForZeroes (0, _, _) = "The first one is a zero!"
checkForZeroes (_, 0, _) = "The second one is a zero!"
checkForZeroes (_, _, 0) = "The third one is a zero!"
checkForZeroes _ = "We're good!"

checkForZeroes (32,0,256)

"The second one is a zero!"

Nhưng! Bởi vì bây giờ chúng ta đang sử dụng EXPRESSION viết hoa chữ thường, nên chúng ta có thể sử dụng chúng ở bất kỳ đâu mà một biểu thức có thể được sử dụng! Không chỉ khi định nghĩa một hàm. Vì vậy, ví dụ, chúng ta có thể nối kết quả đánh giá biểu thức case với một Chuỗi khác:

checkForZeroes' :: (Int, Int, Int) -> String
checkForZeroes' tuple3 = "The " ++ show tuple3 ++ " has " ++
case tuple3 of
(0, _, _) -> "a zero as its first element"
(_, 0, _) -> "a zero as its second element"
(_, _, 0) -> "a zero as its third element"
_ -> "no zeroes!"

checkForZeroes' (32,0,256)

"The (32,0,256) has a zero as its second element"

Điều đó làm cho case các biểu thức thuận tiện để sử dụng bên trong các biểu thức khác. Ngoài ra, hãy nhớ rằng bất kỳ điều gì bạn có thể làm với case các biểu thức đều có thể được thực hiện bằng cách định nghĩa các hàm với letwhere, hoặc guards.

Và điều đó đặt ra câu hỏi: "Tại sao chúng ta có quá nhiều cách để làm cùng một việc?!" Tôi sẽ cho bạn biết tại sao…

Kiểu khai báo vs Kiểu biểu thức

Có hai phong cách chính để viết các chương trình hàm trong Haskell:

  • Kiểu khai báo là nơi bạn xây dựng một thuật toán theo một số phương trình được thỏa mãn.
  • Kiểu biểu thức là nơi bạn soạn các biểu thức lớn từ các biểu thức nhỏ.

Những ~người tạo ra~ các vị thần Haskell đã tham gia vào một cuộc tranh luận gay gắt xem phong cách nào tốt hơn. Chủ yếu là vì nếu có thể, chỉ có một cách để làm điều gì đó sẽ ít gây nhầm lẫn và dư thừa hơn. Nhưng! Sau khi đổ máu, mồ hôi và nước mắt, họ quyết định hỗ trợ toàn diện về mặt cú pháp cho cả hai. Và hãy để những người bình thường sử dụng những gì họ thích nhất.

Như ví dụ về điều này, chúng ta đã nhận được:

Kiểu khai báoKiểu biểu thức
where mệnh đềBiểu thức let
Khớp mẫu trong định nghĩa hàm: f [] = 0Biểu thức case: f xs = case xs of [] -> 0
Guards trong định nghĩa hàm: `f [x]x > 0 = 'a'`
Đối số hàm ở phía bên tay trái: f x = x*xRút gọnLambda: f = \x -> x*x

Và thứ lambda ở cuối bảng là gì? Đó là một chủ đề cho bài học tuần tới! 😁 Vì vậy hãy chắc chắn rằng bạn đã xem nó!

Bây giờ, như một bản tóm tắt:

Bản tóm tắt

  • Khớp mẫu cho các định nghĩa hàm giúp dễ dàng thực hiện những việc khác nhau tùy thuộc vào cấu trúc hoặc giá trị của các đối số.
  • Khớp mẫu trên bộ dữ liệu, danh sách và các cấu trúc khác cho phép bạn dễ dàng trích xuất các giá trị chứa trong đó.
  • Các biểu thức chữ hoa chữ thường là một cách biểu đạt hơn của các định nghĩa hàm so Khớp mẫu, nhưng chúng cũng có thể được sử dụng ở hầu hết mọi nơi như bất kỳ biểu thức nào khác. (Không chỉ để định nghĩa hàm.)
  • Hai kiểu chính để viết lập trình hàm trong Haskell là "Kiểu khai báo" và "Kiểu biểu thức". Đừng lãng phí thời gian để tranh cãi xem cái nào là tốt nhất. Áp dụng cái bạn thích hơn hoặc trộn và kết hợp theo ý muốn.

Nguồn bài viết tại đây


Picture