Haskell #18.2 Module

 

Data.List

Module Data.List hiển nhiên là chứa tất cả mọi thứ về danh sách. Nó cung cấp một số hàm rất hữu dụng để xử lý danh sách. Ta đã gặp một vài hàm trong số đó (như map and filter) vì module Prelude tiện tay xuất một số hàm từ Data.List. Bạn không phải nhập Data.List thông qua kiểu hình thức thuộc tính vì nó không xung ddoojt với bất kì cái tên nào trong Prelude trừ những thứ mà Prelude đã vay mượn từ Data.List. Hãy xem một số hàm mà ta chưa từng gặp trước đây.

intersperse nhận vào một phần tử và một danh sách rồi cài phần tử đó vào giữa các phần tử kế tiếp trong danh sách. Sau đây là ví dụ minh họa:

ghci> intersperse '.' "MONKEY"
"M.O.N.K.E.Y"
ghci> intersperse 0 [1,2,3,4,5,6]
[1,0,2,0,3,0,4,0,5,0,6]

intercalate nhận một danh sách chứa danh sách, cùng một danh sách khác. Hàm này thực hiện chèn danh sách lẻ vào giữa các phần tử danh sách thuộc danh sách phức hợp, rồi là phẳng kết quả.

ghci> intercalate " " ["hey","there","guys"]
"hey there guys"
ghci> intercalate [0,0,0] [[1,2,3],[4,5,6],[7,8,9]]
[1,2,3,0,0,0,4,5,6,0,0,0,7,8,9]

transpose chuyển vị một danh sách chứa danh sách. Nếu bạn hình dung danh sách chứa danh sách như một ma trận 2 chiều, thì chuyển vị làm cho cột biến thành hàng và ngược lại.

ghci> transpose [[1,2,3],[4,5,6],[7,8,9]]
[[1,4,7],[2,5,8],[3,6,9]]
ghci> transpose ["hey","there","guys"]
["htg","ehu","yey","rs","e"]

Giả sử ta có các đa thức 3x2 + 5x + 910x3 + 9 và 8x3 + 5x2 + x – 1; ta muốn cộng chúng lại với nhau. Có thể dùng các danh sách [0,3,5,9][10,0,0,9] và [8,5,1,-1] để biểu diễn chúng trong Haskell. Bây giờ, để cộng chúng lại, tất cả công việc mà ta cần làm là:

ghci> map sum $ transpose [[0,3,5,9],[10,0,0,9],[8,5,1,-1]]
[18,8,6,17]

Khi chuyển vị ba danh sách này, các số hạng lập phương sẽ nằm ở hàng một, bình phương nằm ở hàng hai và cứ như vậy. Thực hiện sum với kết quả trên sẽ cho ta đáp số cần tìm.

shopping lists

foldl' và foldl1' là những dạng nghiêm ngặt hơn so với các “hiện thân” lười biếng của chúng. Khi dùng các phép gấp lười biếng đối với danh sách khổng lồ, thường là bạn sẽ gặp lỗi tràn ngăn xếp. Nguyên nhân là do bản chất lười biếng trong các hàm gấp, khiến cho thật ra giá trị tích lũy không được cập nhật trong quá trình gấp. Điều thực tế xảy ra là giá trị tích lũy kiểu như hứa hẹn rằng nó sẽ tính giá trị của bản thân khi được hỏi đến, để trả lại kết quả thật (còn được gọi là “thunk”). Điều này xảy ra đối với mỗi giá trị tích lũy trung gian và tất cả những thunk đó đã làm tràn ngăn xếp. Còn các hàm gấp nghiêm ngặt thì không có tính lười biếng và thực sự phải tìm ra các giá trị trung gian trong quá trình xử lý thay vì chất đống các thunk vào ngăn xếp. Vì vậy nếu lúc nào bạn gặp lỗi tràn ngăn xếp trong khi dùng cách gấp lười biếng, thì hãy thử chuyển sang dạng nghiêm ngặt tương ứng.

concat làm phẳng một danh sách chứa danh sách về một danh sách chứa các phần tử.

ghci> concat ["foo","bar","car"]
"foobarcar"
ghci> concat [[3,4,5],[2,3,4],[2,1,1]]
[3,4,5,2,3,4,2,1,1]

Việc này chỉ làm giảm một cấp độ lồng ghép. Vì vậy nếu bạn muốn làm phẳng hoàn toàn [[[2,3],[3,4,5],[2]],[[2,3],[3,4]]], tức là một danh sách chứa danh sách, thì bạn phải kết nối nó hai lần.

Thực hiện concatMap cũng giống như là đầu tiên là ánh xạ một hàm lên một danh sách rồi sau đó kết nối danh sách bằng concat.

ghci> concatMap (replicate 4) [1..3]
[1,1,1,1,2,2,2,2,3,3,3,3]

and nhận vào một danh sách các giá trị boole rồi trả lại True chỉ khi tất cả những giá trị trong danh sách đều là True.

ghci> and $ map (>4) [5,6,7,8]
True
ghci> and $ map (==4) [4,4,4,3,4]
False

or có phần giống and, nhưng nó trả lại True nếu có giá trị boole nào đó trong danh sách là True.

ghci> or $ map (==4) [2,3,4,5,6,1]
True
ghci> or $ map (>4) [1,2,3]
False

any và all nhận vào một vị từ rồi kiểm tra xem, với any là có phần tử nào trong sách thỏa mãn vị từ không; và với all, là tất cả phần tử đều thỏa mãn không. thực hiện ta sẽ dùng hai hàm này thay vì ánh xạn lên danh sách
rồi mới viết and hoặc or.

ghci> any (==4) [2,3,5,6,1,4]
True
ghci> all (>4) [6,9,10]
True
ghci> all (`elem` ['A'..'Z']) "HEYGUYSwhatsup"
False
ghci> any (`elem` ['A'..'Z']) "HEYGUYSwhatsup"
True

iterate nhận vào một hàm cùng một giá trị khởi đầu. Nó áp dụng hàm với giá trị khởi đầu, sau đó áp dụng cũng hàm này lên kết quả tìm được. Kết quả lại được áp dụng hàm một lần nữa và cứ như vậy, v.v. Nó trả lại tất cả các giá trị dưới dạng một danh sách vô hạn.

ghci> take 10 $ iterate (*2) 1
[1,2,4,8,16,32,64,128,256,512]
ghci> take 3 $ iterate (++ "haha") "haha"
["haha","hahahaha","hahahahahaha"]

splitAt nhận vào một số và một danh sách. Nó tách danh sách tại vị trí phần tử thứ tự tương ứng với số đó, trả lại hai danh sách trong một bộ.

ghci> splitAt 3 "heyman"
("hey","man")
ghci> splitAt 100 "heyman"
("heyman","")
ghci> splitAt (-3) "heyman"
("","heyman")
ghci> let (a,b) = splitAt 3 "foobar" in b ++ a
"barfoo"

takeWhile thực sự là một hàm nhỏ nhưng hữu ích. Nó nhận vào các phần tử từ danh sách trong khi vị từ còn thỏa mãn, rồi khi bắt gặp một phần tử không thỏa mãn với vị từ, thì sẽ cắt đứt. Điều này hóa ra rất có ích.

ghci> takeWhile (>3) [6,5,4,3,2,1,2,3,4,5,4,3,2,1]
[6,5,4]
ghci> takeWhile (/=' ') "This is a sentence"
"This"

Chẳng hạn, ta cần biết tổng của tất cả số lập phương mà nhỏ hơn 10000. Ta không thể ánh xạ (^3) lên [1..], áp dụng lọc rồi cộng kết quả lại vì việc lọc một danh sách vô hạn sẽ không bao giờ kết thúc. Bạn có thể biết rằng ở đây tất cả những phần tử có thứ tự tăng dần, nhưng Haskell thì không. Chính đó là lý do tại sao ta có thể viết như sau:

ghci> sum $ takeWhile (<10000) $ map (^3) [1..]
53361

Ta áp dụng (^3) lên một danh sách vô hạn rồi sau đó một khi bắt gặp phần tử vượt quá 10000 thì danh sách sẽ bị cắt đứt. Bây giờ ta có thể dễ dàng lấy tổng.

dropWhile cũng tương tự, chỉ khác là nó bỏ rơi đi tất cả những phần tử nào trong khi vị từ còn đúng. Một khi vị từ bằng với False, hàm sẽ trả về phần còn lại của danh sách. Một hàm rất đáng yêu và hữu ích!

ghci> dropWhile (/=' ') "This is a sentence"
" is a sentence"
ghci> dropWhile (<3) [1,2,2,2,3,4,5,4,3,2,1]
[3,4,5,4,3,2,1]

Có người cho chúng ta một danh sách biểu diễn trị giá cổ phiếu theo ngày. Danh sách được hợp thành từ những bộ, trong đó phần tử thứ nhất là giá trị cổ phiếu, phần tử thứ hai là năm, phần tử thứ ba là tháng và thứ tư là ngày. Ta muốn biết khi nào giá trị cổ phiếu lần đầu tiên vượt ngưỡng 1000 đô-la!

ghci> let stock = [(994.4,2008,9,1),(995.2,2008,9,2),(999.2,2008,9,3),(1001.4,2008,9,4),(998.3,2008,9,5)]
ghci> head (dropWhile (\(val,y,m,d) -> val < 1000) stock)
(1001.4,2008,9,4)

span có dạng giống như takeWhile, chỉ khác là nó trả lại một cặp danh sách. Danh sách thứ nhất chứa mọi thứ mà danh sách thu được từ takeWhile đáng ra đã chứa nếu nó được gọi với cùng vị từ và cùng danh sách. Danh sách thứ hai chứa phần của danh sách mà đáng ra đã được bỏ rơi.

ghci> let (fw, rest) = span (/=' ') "This is a sentence" in "First word:" ++ fw ++ ", the rest:" ++ rest
"First word: This, the rest: is a sentence"

Trong khi span như một cầu nối danh sách khi mà vị từ còn đúng, thì break lại phá vỡ nó khi vị từ có giá trị đúng lần đầu tiên. Viết break p cũng tương đương với span (not . p).

ghci> break (==4) [1,2,3,4,5,6,7]
([1,2,3],[4,5,6,7])
ghci> span (/=4) [1,2,3,4,5,6,7]
([1,2,3],[4,5,6,7])

Khi dùng break, danh sách thứ hai trong kết quả sẽ bắt đầu bằng phần tử đầu tiên thoả mãn đúng vị từ.

sort chỉ đơn giản là sắp xếp một danh sách. KIểu của những phần tử trong danh sách phải thuộc về lớp Ord, vì nếu các phần tử trong danh sách không thể được xếp đặt theo một dạng thứ tự nào đó, thì ta sẽ không thể sắp xếp được danh sách.

ghci> sort [8,5,3,2,1,6,4,2]
[1,2,2,3,4,5,6,8]
ghci> sort "This will be sorted soon"
"    Tbdeehiillnooorssstw"

group nhận vào một danh sách rồi nhóm các phần tử lân cận vào các danh sách con nếu chúng bằng nhau.

ghci> group [1,1,1,1,2,2,2,2,3,3,2,2,2,5,6,7]
[[1,1,1,1],[2,2,2,2],[3,3],[2,2,2],[5],[6],[7]]

Nếu sắp xếp một danh sách trước khi nhóm, ta có thể tính ra được mỗi phần tử xuất hiện trong danh sách bao nhiêu lần.

ghci> map (\l@(x:xs) -> (x,length l)) . group . sort $ [1,1,1,1,2,2,2,2,3,3,2,2,2,5,6,7]
[(1,4),(2,7),(3,2),(5,1),(6,1),(7,1)]

inits và tails cũng giống như init và tail, chỉ khác là chúng áp dụng một cách đệ quy đối với một danh sách cho đến tận khi không còn gì nữa. Xem này.

ghci> inits "w00t"
["","w","w0","w00","w00t"]
ghci> tails "w00t"
["w00t","00t","0t","t",""]
ghci> let w = "w00t" in zip (inits w) (tails w)
[("","w00t"),("w","00t"),("w0","0t"),("w00","t"),("w00t","")]

Ta hãy dùng một hàm gấp để thực hiện tìm kiếm danh sách con trong một danh sách.

search :: (Eq a) => [a] -> [a] -> Bool
search needle haystack = 
    let nlen = length needle
    in  foldl (\acc x -> if take nlen x == needle then True else acc) False (tails haystack)

Đầu tiên ta gọi tails với danh sách mà ta đang tìm trong đó. Tiếp theo, ta duyệt qua từng phần đuôi để xem nó có bắt đầu bằng thứ mà ta cần tìm không.

Bằng cách này, thực ra ta vừa tạo một hàm có biểu hiện giống như isInfixOf. Hàm isInfixOf tìm kiếm một danh sách con trong một danh sách rồi trả lại True nếu danh sách con cần tìm nằm đâu đó trong danh sách lớn.

ghci> "cat" `isInfixOf` "im a cat burglar"
True
ghci> "Cat" `isInfixOf` "im a cat burglar"
False
ghci> "cats" `isInfixOf` "im a cat burglar"
False

isPrefixOf và isSuffixOf lần lượt tìm kiếm một danh sách con ở đầu hoặc cuối danh sách.

ghci> "hey" `isPrefixOf` "hey there!"
True
ghci> "hey" `isPrefixOf` "oh hey there!"
False
ghci> "there!" `isSuffixOf` "oh hey there!"
True
ghci> "there!" `isSuffixOf` "oh hey there"
False

elem và notElem kiểm tra xem nếu một phần tử nằm trong hoặc không nằm trong một danh sách.

partition nhận vào một danh sách và một vị từ rồi trả lại một cặp danh sách. Trong cặp đó, danh sách thứ nhất chứa tất cả những phần tử thỏa mãn vị từ, còn danh sách thứ hai chứa những phần tử không thỏa mãn.

ghci> partition (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy"
("BOBMORGAN","sidneyeddy")
ghci> partition (>3) [1,3,5,6,3,2,1,0,3,7]
([5,6,7],[1,3,3,2,1,0,3])

Cần hiểu được cách làm này khác thế nào so với span và break:

ghci> span (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy"
("BOB","sidneyMORGANeddy")

Nếu như span và break hoàn thành công việc ngay khi chúng bắt gặp phần tử đầu tiên mà không thỏa mãn vị từ, thì partition vẫn tiếp tục duyệt toàn bộ danh sách và thực hiện chia rẽ danh sách đó, tùy theo vị từ.

find nhận vào một danh sách và một vị từ rồi trả về phần tử đầu tiên thoả mãn vị từ đó. Nhưng nó trả về phần tử đó bọc trong một giá trị Maybe. Trong chương sau chúng tôi sẽ đề cập thêm tới kiểu dữ liệu đại số, nhưng bây giờ thì bạn chỉ cần biết rằng: một giá trị Maybe có thể hoặc là Just something (“chỉ là một thứ gì đó”) hoặc Nothing (“không có gì”). Rất giống với việc một danh sách có thể là một danh sách rỗng hoặc một danh sách có phần tử nào đó, giá trị Maybe có thể là không phần tử nào hoặc là một phần tử. và cũng giống như kiểu của một danh sách, chẳng hạn danh sách số nguyên, là [Int], kiểu của maybe với số nguyên là
Maybe Int. Dù sao thì ta hãy khởi động hàm find cái đã.

ghci> find (>4) [1,2,3,4,5,6]
Just 5
ghci> find (>9) [1,2,3,4,5,6]
Nothing
ghci> :t find
find :: (a -> Bool) -> [a] -> Maybe a

Lưu ý kiểu của find. Kết quả của nó là Maybe a. Điều này cũng giống như có kiểu là [a], chỉ một giá trị thuộc kiểu Maybe mới có thể chứa không phần tử nào hoặc một phần tử; còn danh sách có thể chứa không phần tử, một phần tử, hoặc nhiều phần tử.

Hãy nhớ lúc chúng ta tìm kiếm thời điểm lần đầu cổ phiếu vượt quá ngưỡng $1000. Ta đã viết head (dropWhile (\(val,y,m,d) -> val < 1000) stock). Nhớ lại rằng head không thực sự an toàn. Điều gì sẽ xảy ra nếu cổ phiếu không bao giờ vượt quá $1000? Khi đó, việc áp dụng dropWhile sẽ trả lại một danh sách rỗng và lấy phần tử đầu của một danh sách rỗng sẽ gây ra lỗi. Tuy vậy, nếu ta viết lại thành find (\(val,y,m,d) -> val > 1000) stock thì sẽ an toàn hơn nhiều. Nếu cổ phiếu không bao giờ vượt quá ngưỡng $1000 (tức là nếu không có phần tử nào thỏa mãn vị từ), thì ta sẽ nhận lại được Nothing. Nhưng nếu trong danh sách có một đáp số đúng thì ta sẽ thu được, chẳng hạn như Just (1001.4,2008,9,4).

elemIndex là hàm giống như elem, chỉ khác là nó không trả lại một giá trị. Có thể nó trả lại chỉ số của phần tử mà ta cần tìm. Nếu phần tử đó không có trong danh sách, hàm sẽ trả lại Nothing.

ghci> :t elemIndex
elemIndex :: (Eq a) => a -> [a] -> Maybe Int
ghci> 4 `elemIndex` [1,2,3,4,5,6]
Just 3
ghci> 10 `elemIndex` [1,2,3,4,5,6]
Nothing

elemIndices giống như elemIndex, nhưng nó trả về một danh sách các chỉ số, trong trường hợp phần tử cần tìm xuất hiện trong danh sách nhiều lần. Vì chúng ta dùng một danh sách để biểu diễn các chỉ số nên sẽ không cần đến kiểu Maybe, vì việc không tìm thấy sẽ được biểu diễn bởi danh sách rỗng, vốn rất giống với Nothing.

ghci> ' ' `elemIndices` "Where are the spaces?"
[5,9,13]

findIndex cũng giống như find, nhưng có thể trả về chỉ số của phần tử đầu tiên thỏa mãn vị từ. findIndices trả lại chỉ số của tất cả những phần tử thỏa mãn vị từ, dưới hình thức một danh sách.

ghci> findIndex (==4) [5,3,2,1,6,4]
Just 5
ghci> findIndex (==7) [5,3,2,1,6,4]
Nothing
ghci> findIndices (`elem` ['A'..'Z']) "Where Are The Caps?"
[0,6,10,14]

Chúng tôi đã đề cập đến zip và zipWith. Chúng tôi cũng lưu ý rằng các hàm này có tác dụng đan cài (zip) hai danh sách lại, hoặc là hợp vào một bộ, hoặc là tham gia vào một hàm nhị phân (nghĩa là hàm nhận hai tham số). Nhưng sẽ thế nào nếu ta muốn đan cài 3 danh sách? Hoặc đan càn ba danh sách bằng một hàm nhận ba tham số? Ồ, để làm như vậy ta có zip3zip4, v.v. và zipWith3zipWith4, v.v. Các dạng này còn có đến tận 7. Có vẻ như cách làm này rất “thủ công chắp vá”, nhưng nó hoạt động tốt, vì chẳng mấy khi ta cần đan cài đến 8 danh sách với nhau. Cũng có một cách rất khéo để đan cài một số lượng vô hạn các danh sách, nhưng giờ thì trình độ của ta chưa cho phép tìm hiểu thêm.

ghci> zipWith3 (\x y z -> x + y + z) [1,2,3] [4,5,2,2] [2,2,3]
[7,9,8]
ghci> zip4 [2,3,3] [2,2,2] [5,5,3] [2,2,2]
[(2,2,5,2),(3,2,5,2),(3,2,3,2)]

Cũng giống như với đan cài thông thường, các danh sách dài hơn sẽ được cắt bớt để còn lại độ dài tương ứng với danh sách ngắn.

lines là một hàm hữu ích khi xử lý các file hoặc số liệu đầu vào từ nơi khác. Hàm này nhận vào một chuỗi rồi trả lại tất cả những dòng của chuỗi đó dưới dạng một danh sách.

ghci> lines "first line\nsecond line\nthird line"
["first line","second line","third line"]

'\n' là kí tự xuống dòng trong Unix. Các dấu sổ ngược (\) có ý nghĩa đặc biệt trong chuỗi và kí tự của Haskell.

unlines là hàm ngược của lines. Nó nhận vào một danh sách các chuỗi và nối chúng lại với nhau bằng '\n'.

ghci> unlines ["first line", "second line", "third line"]
"first line\nsecond line\nthird line\n"

words và unwords được dùng để chia cắt một dòng chữ thành các từ hoặc nối các từ thành dòng chữ. Rất có ích.

ghci> words "hey these are the words in this sentence"
["hey","these","are","the","words","in","this","sentence"]
ghci> words "hey these           are    the words in this\nsentence"
["hey","these","are","the","words","in","this","sentence"]
ghci> unwords ["hey","there","mate"]
"hey there mate"

Chúng ta đã biết đến nub. Nó nhận vào một danh sách và bỏ đi các phần tử thừa do trùng lặp, trả lại một danh sách trong đó mỗi phần tử là duy nhất! Hàm này có tên thật lạ. Hóa ra là “nub” có nghĩa là một khối nhỏ, hoặc phần thiết yếu của một thứ nào đó. Theo tôi thì tên cần được đặt là những từ thông dụng nghiêm chỉnh thay vì những từ cổ.

ghci> nub [1,2,3,4,3,2,1,2,3,4,3,2,1]
[1,2,3,4]
ghci> nub "Lots of words and stuff"
"Lots fwrdanu"

delete nhận vào một phần tử cùng một danh sách rồi xóa đi sự xuất hiện đầu tiên của phần tử này trong danh sách.

ghci> delete 'h' "hey there ghang!"
"ey there ghang!"
ghci> delete 'h' . delete 'h' $ "hey there ghang!"
"ey tere ghang!"
ghci> delete 'h' . delete 'h' . delete 'h' $ "hey there ghang!"
"ey tere gang!"

\\ là hàm hiệu. Về cơ bản, nó giống như phép hiệu của tập hợp. Với mỗi phần tử trong danh sách ở vế phải, nó bỏ đi phần tử tương ứng trong danh sách ở vế trái.

ghci> [1..10] \\ [2,5,9]
[1,3,4,6,7,8,10]
ghci> "Im a big baby" \\ "big"
"Im a  baby"

Việc viết [1..10] \\ [2,5,9] cũng giống như viết delete 2 . delete 5 . delete 9 $ [1..10].

union cũng có tác dụng như một hàm đối với danh sách. Nó trả lại hợp của hai danh sách. Hàm này giống với việc duyệt từng phần tử trong danh sách thứ hai và bổ sung nó vào danh sách thứ nhất nếu phần tử này chưa có mặt trong đó. Dù vậy cần chú ý rằng, những giá trị trùng lặp sẽ bị gạt bỏ khỏi danh sách thứ hai!

ghci> "hey man" `union` "man what's up"
"hey manwt'sup"
ghci> [1..7] `union` [5..10]
[1,2,3,4,5,6,7,8,9,10]

intersect có tác dụng như phép giao tập hợp. Nó trả lại những phần tử nào chỉ tìm thấy trong cả hai danh sách.

ghci> [1..7] `intersect` [5..10]
[5,6,7]

insert nhận vào một phần tử cùng một dánh sách các phần tử có thể sắp xếp rồi chèn phần tử này vào chỗ mà nó vẫn còn nhỏ hơn hoặc bằng phần tử kế tiếp. Nói cách khác, insert sẽ bắt đầu từ đầu danh sách rồi cứ tiếp tục đến khi nó tìm thấy một phần tử bằng hoặc lớn hơn phần tử định chèn và nó sẽ chèn nó đúng vào trước phần tử tìm thấy vừa rồi.

ghci> insert 4 [3,5,1,2,8,2]
[3,4,5,1,2,8,2]
ghci> insert 4 [1,3,4,4,1]
[1,3,4,4,4,1]

Ở ví dụ thứ nhất, phần tử 4 được chèn vào ngay sau 3 và trước 5. Ở ví dụ thứ hai thì được chèn vào giữa 3 và 4.

Nếu ta dùng insert để chèn vào một danh sách đã được sắp xếp, thì danh sách thu được cũng vẫn giữ được trật tự sắp xếp.

ghci> insert 4 [1,2,3,5,6,7]
[1,2,3,4,5,6,7]
ghci> insert 'g' $ ['a'..'f'] ++ ['h'..'z']
"abcdefghijklmnopqrstuvwxyz"
ghci> insert 3 [1,2,4,3,2,1]
[1,2,3,4,3,2,1]

Đặc điểm chung giữa các hàm lengthtakedropsplitAt!! và replicate là chúng nhận vào một trong những tham số Int (hoặc trả về một Int), mặc dù lẽ ra chúng đã tổng quát hơn và hữu ích hơn nếu chúng chấp nhận bất kì kiểu nào thuộc về lớp Integral hoặc Num (tùy theo từng hàm). Lý do có từ quá khứ [trong quá trình xây dựng Haskell]. Tuy nhiên, chỉnh lại điều này sẽ phá vỡ rất nhiều mã lệnh sẵn có. Vì vậy, Data.List đã có những dạng tương đương tổng quát hơn, có tên genericLengthgenericTakegenericDropgenericSplitAtgenericIndex và genericReplicate. Chẳng hạn, length có dấu ấn kiểu là length :: [a] -> Int. Nếu ta thử lấy trung bình của một danh sách các số bằng cách viết let xs = [1..6] in sum xs / length xs, ta sẽ nhận được lỗi về kiểu, vì bạn không thể dùng / đối với Int. Còn genericLength lại có dấu ấn kiểu là genericLength :: (Num a) => [b] -> a. Vì Num có thể đóng vai trò là số chấm động [có phần thập phân], nên việc lấy trung bình bằng cách viết let xs = [1..6] in sum xs / genericLength xs cũng được.

Các hàm nubdeleteunionintersect and group đều có dạng tổng quát hơn với tên gọi nubBydeleteByunionByintersectBy và groupBy. Sự khác biệt giữa chúng là nhóm các hàm thứ nhất thì dùng == để kiểm tra sự bằng nhau, còn các hàm By thì nhận một hàm kiểu đẳng thức rồi so sánh chúng bằng hàm đẳng thức đó. group cũng giống như groupBy (==).

Chẳng hạn, giả sử ta có một danh sách mô tả giá trị của hàm biến đổi trong mỗi giây. Ta cần phân đoạn nó vào trong những danh sách con, tùy theo giá trị dưới 0 và trên 0. Nếu ta chỉ dùng hàm group thông thường, thì nó chỉ nhóm lại các giá trị ngang bằng đứng kề sát nhau lại. Nhưng điều ta cần là nhóm chúng lại theo dấu (âm hoặc dương). Đó là trường hợp mà groupBy thể hiện công dụng! Hàm dạng đẳng thức cung cấp cho các hàm By cần có hai phần tử cùng kiểu và trả lại True nếu nó cho rằng hai phần tử bằng nhau theo tiêu chuẩn được xét.

ghci> let values = [-4.3, -2.4, -1.2, 0.4, 2.3, 5.9, 10.5, 29.1, 5.3, -2.4, -14.5, 2.9, 2.3]
ghci> groupBy (\x y -> (x > 0) == (y > 0)) values
[[-4.3,-2.4,-1.2],[0.4,2.3,5.9,10.5,29.1,5.3],[-2.4,-14.5],[2.9,2.3]]

Từ đay, ta thấy rõ phần nào dương và phần nào âm. Ở đây cung cấp cho groupBy là hàm đẳng thức; hàm này nhận vào hai phần tử rồi trả lại True chỉ khi hai phần tử này đều âm hoặc đều dương. Hàm đẳng thức này cũng có thể được viết thành \x y -> (x > 0) && (y > 0) || (x <= 0) && (y <= 0), mặc dù theo tôi thì cách viết đầu là dễ đọc hơn. Thậm chí còn có cách viết hàm đẳng thức rõ ràng hơn cho các hàm By nếu bạn nhập hàm on từ Data.Function. Hàm on được định nghĩa như sau:

on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
f `on` g = \x y -> f (g x) (g y)

Vì vậy, viết (==) `on` (> 0) sẽ trả lại một hàm đảng thức giống với \x y -> (x > 0) == (y > 0)on được dùng nhiều với các hàm By vì với nó, ta có thể viết:

ghci> groupBy ((==) `on` (> 0)) values
[[-4.3,-2.4,-1.2],[0.4,2.3,5.9,10.5,29.1,5.3],[-2.4,-14.5],[2.9,2.3]]

Thật rất dễ đọc! Bạn có thể đọc lên thành tiếng: Nhóm danh sách này bằng đẳng thức phát biểu rằng các phần tử có cùng lớn hơn 0 hay không.

Tương tự, các hàm sortinsertmaximum và minimum cũng có dạng tương đương tổng quát hơn. Những hàm như groupBy nhận vào một hàm có tác dụng quyết định hai phần tử có bằng nhau không. sortByinsertBymaximumBy và minimumBy nhận vào một hàm quyết định xem nếu một phần tử lớn hơn, nhỏ hơn hoặc bằng phần tử kia. Dấu ấn kiểu của sortBy là sortBy :: (a -> a -> Ordering) -> [a] -> [a]. Nếu bạn còn nhớ từ trước, kiểu Ordering có thể mang giá trị LTEQ hoặc GTsort thì tương đương với sortBy compare, vì “compare” chỉ nhận hai phần tử có kiểu thuộc lớp Ord rồi trả lại quan hệ thứ tự giữa chúng.

Danh sách cũng có thể so sánh được, nhưng lúc đó, chúng được so sánh theo thứ tự từ vựng. Sẽ thế nào nếu ta có một danh sách chứa danh sách và muốn sắp xếp danh sách lớn không dựa trên nội dung danh sách con, mà là dựa trên chiều dài của các danh sách con này? À, như bạn có thể đã đoán được, ta dùng hàm sortBy.

ghci> let xs = [[5,4,5,4,4],[1,2,3],[3,5,4,3],[],[2],[2,2]]
ghci> sortBy (compare `on` length) xs
[[],[2],[2,2],[1,2,3],[3,5,4,3],[5,4,5,4,4]]

Tuyệt! compare `on` length … này bạn, nghe như tiếng Anh thực thụ nhỉ! Nếu bạn không chác chắn rằng ở đây on có tác dụng ra sao, thì cần nhớ compare `on` length tương đương với \x y -> length x `compare` length y. Khi dùng các hàm By nhận vào một hàm dạng đẳng thức, bạn thường viết (==) `on` something [với something là một thứ nào đó] và khi bạn đang dùng hàm By nhận vào một hàm thứ tự, bạn thường viết compare `on` something.