Haskell #1.4 List comprehension

 

List comprehension

Nếu bạn đã từng theo học môn toán, có lẽ bạn đã gặp phải set comprehension. Cách viết này thường được dùng để chỉ một tập hợp cụ thể từ tập hợp tổng quát. Một set comprehension cơ bản của tập hợp chứa 10 số tự nhiên chẵn đầu tiên là {2x|xN,x10}. Phần đứng trước dấu | được gọi là hàm đầu ra, x là biến, N là tập hợp đầu vào và x10 là vị ngữ. Có nghĩa là tập hợp sẽ chứa các chẵn của tất cả các số tự nhiên nào thỏa mãn vị ngữ. Nếu ta muốn viết biểu thức này trong Haskell, ta có thể viết chẳng hạn như take 10 [2,4..]. Nhưng nếu ta không muốn 10 số chẵn đầu tiên mà muốn một kiểu hàm nào đó phức tạp hơn được áp dụng cho chúng thì sao? Ta có thể dùng List comprehension. List comprehension rất giống với set comprehension. Tạm thời ta sẽ thống nhất dùng ví dụ 10 số chẵn đầu tiên. List comprehension ta có thể dùng là [x*2 | x <- [1..10]]x được rút ra từ [1..10] và với mỗi phần tử trong [1..10] (mà ta đã “gắn” với x), ta lấy phần tử đó, chỉ x*2. Sau đây là comprehension đó khi được thực thi.

ghci> [x*2 | x <- [1..10]]
[2,4,6,8,10,12,14,16,18,20]

Như bạn đã thấy, ta thu được kết quả mong muốn. Bây giờ hãy thêm vào một điều kiện (hoặc một vị ngữ) vào comprehension đó. Vị ngữ đi sau phần gán và được phân cách với phần gắn này bởi dấu phẩy.

Chẳng hạn, ta chỉ cần các phần tử khi x*2 thì lớn hơn hoặc bằng 12.

ghci> [x*2 | x <- [1..10], x*2 >= 12]  
[12,14,16,18,20]

Tuyệt, nó đã chạy đúng. Thế còn nếu muốn tất cả những số từ 50 đến 100 sao cho phần dư khi chia cho 7 thì bằng 3? Thật dễ.

ghci> [ x | x <- [50..100], x `mod` 7 == 3]
[52,59,66,73,80,87,94]

Thành công rồi! Lưu ý rằng việc dùng vị ngữ để bỏ bớt phần tử trong List còn được gọi là filtering (lọc). Ta lấy List rồi lọc chúng bằng vị ngữ. Bây giờ hãy xét ví dụ khác. Chẳng hạn, ta muốn một comprehension để thay mỗi số lẻ lớn hơn 10 bằng “BANG!” và từng số lẻ nhỏ hơn 10 bằng “BOOM!”. Nếu một số không phải số lẻ, ta sẽ vứt nó. Để cho tiện, ta sẽ đặt comprehension này vào trong một hàm để sau này dùng lại.

boomBangs xs = [ if x < 10 then "BOOM!" else "BANG!" | x <- xs, odd x]

Phần sau cùng của comprehension là vị ngữ. Hàm odd trả lại True khi gặp số lẻ và False khi gặp số chẵn. Phần tử chỉ được đứng trong List nếu tất cả các vị ngữ đều được lượng giá là True.

ghci> boomBangs [7..13]
["BOOM!","BOOM!","BANG!","BANG!"]

Ta có thể đưa vào nhiều vị ngữ khác nhau. Nếu ta muốn tất cả số từ 10 đến 20 mà không phải là 13, 15, hay 19, ta có thể viết:

ghci> [ x | x <- [10..20], x /= 13, x /= 15, x /= 19]
[10,11,12,14,16,17,18,20]

Ta không những có thể có nhiều vị ngữ trong List comprehension (một phần tử phải thỏa mãn tất cả vị ngữ mới tồn tại trong List kết quả), mà còn có thể rút từ nhiều List khác nhau. Khi rút từ nhiều List, comprehension sẽ tạo ra tất cả những tổ hợp trong các List đã cho rồi nối chúng lại bằng hàm đầu ra mà ta chỉ định. Nếu comprehension trong đó rút từ hai List có chiều dài 4 sẽ cho ra một List có chiều dài 16, nếu như ta không sử dụng filter. Giả sử ta có hai List, [2,5,10] và [8,10,11]. Nếu muốn hàm trả về tích của tất cả những tổ hợp có thể giữa các số trong List này, ta có thể viết như sau.

ghci> [ x*y | x <- [2,5,10], y <- [8,10,11]]
[16,20,22,40,50,55,80,100,110]

Như mong đợi, độ dài của List mới là 9. Thế còn nếu ta muốn tất cả tích phải lớn hơn 50?

ghci> [ x*y | x <- [2,5,10], y <- [8,10,11], x*y > 50]
[55,80,100,110]

Thế còn List comprehension trong đó kết hợp một List các tính từ và một List các động từ tiếng Anh cho vui?

ghci> let nouns = ["hobo","frog","pope"]
ghci> let adjectives = ["lazy","grouchy","scheming"]
ghci> [adjective ++ " " ++ noun | adjective <- adjectives, noun <- nouns]
["lazy hobo","lazy frog","lazy pope","grouchy hobo","grouchy frog",
"grouchy pope","scheming hobo","scheming frog","scheming pope"]

Hãy viết một phiên bản riêng cho hàm length của riêng mình! Ta sẽ gọi nó là length'.

length' xs = sum [1 | _ <- xs]

_ có nghĩa là chúng ta cũng không quan tâm về List cho nên thay vì đặt một tên biến không bao giờ dùng đến, ta chỉ viết _. Hàm này thay thế mọi phần tử của List với 1 rồi cộng chúng lại (kết quả sẽ là chiều dài của List). Tôi xin phép nhắc lại: vì string cũng là List nên ta có thể dùng List comprehension để xử lý và sản sinh ra string. Sau đây là một hàm nhận vào một string và in ra chữ in hoa.

removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]

Khi ta thử hàm này:

ghci> removeNonUppercase "Hahaha! Ahahaha!"
"HA"
ghci> removeNonUppercase "IdontLIKEFROGS"
"ILIKEFROGS"

Vị ngữ ở đây đảm nhiệm toàn bộ công việc. Nó nói rằng kí tự được bỏ vào List mới chỉ khi nó là một phần tử của List [‘A’..’Z’].

List comprehension có thể được lồng vào nhau nếu bạn làm việc với List trong đó chứa List. Một List chứa nhiều List số. Ví dụ, xử lý List trong List mà không cần phải làm phẳng List [nghĩa là ta vẫn được giữ cấu trúc List ban đầu].

ghci> let xxs = [[1,3,5,2,3,1,2,4,5],[1,2,3,4,5,6,7,8,9],[1,2,4,2,1,6,3,1,3,2,3,6]]
ghci> [ [ x | x <- xs, even x ] | xs <- xxs]
[[2,2,4],[2,4,6,8],[2,4,2,6,2,6]]