Chương 7: Module

Trở về Mục lục cuốn sách

Nạp module

Trong Haskell, module là tập hợp các hàm, kiểu, và lớp chứa kiểu có liên quan. Chương trình Haskell là một tập hợp các module trong đó module chính nạp những module khác rồi dùng các hàm định nghĩa trong đó để thực hiện nhiệm vụ.

Việc chia mã lệnh vào trong nhiều module khác nhau có khá nhiều ưu điểm. Nếu một module đủ tính tổng quát, thì các hàm mà nó xuất ra sẽ có thể dùng được trong một loạt những chương trình khác nhau. Nếu mã lệnh của bạn được chia thành những nhiều module gói gọn, theo nghĩa không quá phụ thuộc vào nhau (ta cũng nói rằng chúng được ghép lỏng lẻo), thì sau này bạn có thể dùng lại chúng. Điều này giúp cho viết mã lệnh sẽ dễ quản lý hơn, khi mã lệnh được chia thành nhiều phần, mỗi phần có mục đích riêng nào đó.

modules

Thư viện chuẩn Haskell được chia thành các module, mỗi module chứa các hàm và kiểu có đôi chút liên quan với nhau và phục vụ một mục đích chung nào đó. Có một module để xử lý danh sách, một module để lập trình tương tranh (concurrent), một module giúp xử lý số phức, v.v. Tất cả mọi hàm, kiểu và lớp mà ta đã làm việc đến giờ đều thuộc về module Prelude, vốn mặc nhiên được nhập vào. Ở chương này, ta sắp xem xết một số module hữu dụng và các hàm trong module đó. Song trước hết, ta phải xem cách nhập module đã.

Cú pháp dùng để nhập module trong file mã lệnh Haskell là import <module name>. Điều này phải được thực hiện trước bất kì lời định nghĩa hàm nào, vì vậy các lệnh nhập thường được thực hiên ở đầu file. Dĩ nhiên là một file lệnh có thể nhập nhiều module. Chỉ cần đặt mỗi lệnh nhập trên một dòng riêng. Ta hãy nhập module Data.List, vốn có một loạt các hàm hữu ích giúp làm việc với danh sách và dùng một hàm mà nó xuất ra để tạo một hàm nói cho ta biết trong danh sách có bao nhiêu phần tử không trùng lặp nhau.

import Data.List

numUniques :: (Eq a) => [a] -> Int
numUniques = length . nub

Khi bạn viết import Data.List, tất cả các hàm được Data.List xuất ra đều trở nên dùng được trong không gian tên tổng thể, nghĩa là bạn có thể gọi nó từ bất kì chỗ nào trong file lệnh. nub là một hàm được định nghĩa trong Data.List; hàm này nhận vào một danh sách và gạt bỏ tất cả những phần tử lặp thừa. Việc hợp length với nub bằng cách viết length . nub sẽ tạo ra một hàm tương đương với \xs -> length (nub xs).

Bạn cũng có thể đặt các hàm của module bên trong không gian tên tổng thể khi dùng GHCI. Nếu bạn ở trong GHCI và muốn gọi được các hàm được xuất bởi Data.List, hãy gõ vào:

ghci> :m + Data.List

Nếu ta muốn nạp những tên từ nhiều module bên trong GHCI, ta không cần phải viết :m + nhiều lần, mà chỉ cần nạp nhiều module cùng lúc.

ghci> :m + Data.List Data.Map Data.Set

Tuy vậy, nếu bạn đã nạp một file lệnh mà nó đã nạp trước một module, bạn sẽ không cần phải dùng :m + để truy cập tới module đó.

Nếu chỉ cần một số hàm từ một module, bạn có thể thực hiện nhập có chọn lựa. Nếu ta muốn nhập mỗi hai hàm nubsort từ Data.List, ta sẽ viết:

import Data.List (nub, sort)

Bạn cũng có thể chọn cách nhập tất cả các hàm trong module trừ một số hàm nhất định. Điều này phát huy tác dụng khi một số module xuất các hàm có tên trùng nhau mà bạn muốn tránh những tên xung đột như vậy. Chẳng hạn ta đã có riêng hàm mang tên nub và muốn nhập vào tất cả các hàm từ Data.List ngoại trừ hàm nub:

import Data.List hiding (nub)

Một cách khác để xử lý xung đột về tên là dùng lệnh nhập chọn lọc. Module Data.Map, trong đó có cấu trúc dữ liệu để tra các giá trị tương ứng từ các khóa, xuất một loạt các hàm có tên giống như hàm trong Prelude, chẳng hạn filter hoặc null. Vì vậy khi ta nhập Data.Map rồi gọi filter, Haskell sẽ không biết rằng phải dùng hàm nào. Sau đây là cách giải quyết:

import qualified Data.Map

Như vậy sẽ khiến cho khi ta muốn chỉ đến hàm filter trong Data.Map, ta sẽ phải viết Data.Map.filter, còn viết mỗi filter nghĩa là ta vẫn chỉ đến hàm filter thông thường mà ta vẫn biết và yêu quý. Nhưng việc phải gõ Data.Map ở trước từng hàm một trong module đó thì thật là tẻ nhạt. Đó là lý do tại sao ta có thể đổi tên hàm nhập chọn lọc thành một thứ ngắn gọn hơn:

import qualified Data.Map as M

Bây giờ để chỉ hàm filter của Data.Map, ta chỉ cần viết M.filter.

Hãy dùng sổ tra cứu tiện lợi này để xem có những module nào ở trong thư viện chuẩn. Một cách hay để thu nhặt kiến thức Haskell là chỉ viết click chuột khắp các mục trong sổ tra cứu thư viện chuẩn và khám phá các module cùng hàm của chúng. Bạn cũng có thể xem mã nguồn Haskell của mỗi module. Việc đọc mã nguồn của một số module thực sự là cách làm hay để học Haskell và nắm được ý nghĩa của nó.

Để tìm kiếm hoặc tra vị trí của các hàm, hãy dùng Hoogle. Đó thực sự là máy tìm kiếm rất tốt dành cho Haskell; bạn có thể tìm theo tên hàm, tên module, hoặc thậm chí cả dấu ấn kiểu.

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 + 9, 10x3 + 98x3 + 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][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'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

anyall 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)]

initstails cũng giống như inittail, 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

isPrefixOfisSuffixOf 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

elemnotElem 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 spanbreak:

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

Nếu như spanbreak 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 zipzipWith. 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ó zip3, zip4, v.v. và zipWith3, zipWith4, 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"

wordsunwords đượ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 34.

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 length, take, drop, splitAt, !!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 genericLength, genericTake, genericDrop, genericSplitAt, genericIndexgenericReplicate. 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 nub, delete, union, intersect and group đều có dạng tổng quát hơn với tên gọi nubBy, deleteBy, unionBy, intersectBygroupBy. 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 sort, insert, maximumminimum 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. sortBy, insertBy, maximumByminimumBy 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 sortBysortBy :: (a -> a -> Ordering) -> [a] -> [a]. Nếu bạn còn nhớ từ trước, kiểu Ordering có thể mang giá trị LT, EQ hoặc GT. sort 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.

Data.Char

lego char

Module Data.Char thực hiện những nhiệm vụ mà tên gọi của nó gợi ra. Nó xuất khẩu những hàm thao tác với kí tự. Module này cũng có ích đối với việc lọc và ánh xạ trên các chuỗi vì chuỗi chỉ là danh sách các kí tự.

Data.Char xuất khẩu một loạt những vị từ thao tác với kí tự; nghĩa là những hàm nhận vào một kí tự rồi cho ta biết rằng liệu một giả thiết nào về kí tự này là đúng hay sai. Sau đây là các vị từ:

isControl kiểm tra xem một kí tự có phải là kí tự điều khiển hay không.

isSpace kiểm tra xem một kí tự có phải là kí tự trắng hay không; các kí tự trắng gồm có dấu cách, tab, xuống dòng, v.v.

isLower kiểm tra xem một kí tự có phải là chữ thường không.

isUpper kiểm tra xem một kí tự có phải là chữ in không.

isAlpha kiểm tra xem một kí tự có phải là chữ cái không.

isAlphaNum kiểm tra xem một kí tự có phải là chữ cái hoặc chữ số không.

isPrint kiểm tra xem một kí tự có in được hay không. Những kí tự như kí tự điều khiển chẳng hạn, thì không in được.

isDigit kiểm tra xem một kí tự có phải là chữ số không.

isOctDigit kiểm tra xem một kí tự có phải là chữ số trong hệ bát phân không.

isHexDigit kiểm tra xem một kí tự có phải là chữ số trong hệ thập lục phân không.

isLetter kiểm tra xem một kí tự có phải là chữ cái không. [Hai hàm isLetterisAlpha tương đương với nhau.]

isMark kiểm tra kí tự dấu Unicode; những kí tự dấu này kết hợp với kí tự liền trước nó để tạo thành các chữ có dấu. Nếu bạn nói tiếng Pháp, hãy dùng hàm này.

isNumber kiểm tra xem một kí tự có phải là chữ số không.

isPunctuation kiểm tra xem một kí tự có phải là dấu câu không.

isSymbol kiểm tra xem một kí tự có phải là dấu kí hiệu toán hoặc kí hiệu tiền tệ không.

isSeparator kiểm tra các dấu trống và dấu ngăn cách Unicode.

isAscii kiểm tra xem một kí tự có rơi vào nhóm 128 kí tự đầu tiên trong bảng mã Unicode không.

isLatin1 kiểm tra xem một kí tự có rơi vào nhóm 256 kí tự đầu tiên trong bảng mã Unicode không.

isAsciiUpper kiểm tra xem một kí tự có phải là ASCII và viết in không.

isAsciiLower kiểm tra xem một kí tự có phải là ASCII và viết thường không.

Tất cả những vị từ này đều có dấu ấn kiểu là Char -> Bool. Trong đa số các trường hợp bạn sẽ dùng đặc điểm này để lọc ra chuỗi hoặc những thứ như vậy. Chẳng hạn, giả sử ta viết một chương trình nhận vào tên người dùng mà tên gọi này chỉ chứa những chữ cái. Ta có thể dùng hàm all trong Data.List kết hợp với vị từ Data.Char để quyết định xem tên được nhập vào đó có hợp lệ không.

ghci> all isAlphaNum "bobby283"
True
ghci> all isAlphaNum "eddy the fish!"
False

Hay! Trong trường hợp bạn không nhớ, all nhận vào một vị từ cùng một danh sách rồi trả lại True chỉ khi vị từ đó thỏa mãn tất cả các phần tử trong danh sách.

Ta cũng có thể dùng isSpace để mô phỏng hàm words trong Data.List.

ghci> words "hey guys its me"
["hey","guys","its","me"]
ghci> groupBy ((==) `on` isSpace) "hey guys its me"
["hey"," ","guys"," ","its"," ","me"]
ghci>

Hừm, được rồi, nó hoạt động kiểu như words song bỏ lại cho ta những phần tử chỉ chứa những dấu cách. Hừm, ta sẽ làm gì đây? Tôi biết rồi, hãy lọc đi những thứ rác đó.

ghci> filter (not . any isSpace) . groupBy ((==) `on` isSpace) $ "hey guys its me"
["hey","guys","its","me"]

A ha!

Lớp Data.Char cũng xuất khẩu một kiểu dữ liệu giống như Ordering. Kiểu Ordering có thể nhận giá trị LT, EQ hoặc GT. Đó là một dạng liệt kê. Nó mô tả một vài kết quả có thể xảy ra khi so sánh hai phần tử. Kiểu GeneralCategory cũng là một dạng liệt kê. Nó trình bày một số thể loại mà một kí tự có thể rơi vào. Hàm cơ bản để lấy thể loại tổng quát của một kí tự là generalCategory. Nó có kiểu generalCategory :: Char -> GeneralCategory. Có khoảng 31 thể loại, vì vậy chúng tôi sẽ không kể hết ra ở đây, nhưng ta hãy thử dùng hàm này xem.

ghci> generalCategory ' '
Space
ghci> generalCategory 'A'
UppercaseLetter
ghci> generalCategory 'a'
LowercaseLetter
ghci> generalCategory '.'
OtherPunctuation
ghci> generalCategory '9'
DecimalNumber
ghci> map generalCategory " \t\nA9?|"
[Space,Control,Control,UppercaseLetter,DecimalNumber,OtherPunctuation,MathSymbol]

Vì kiểu GeneralCategory là một phần của lớp Eq, ta cũng có thể kiểm tra những thứ như generalCategory c == Space.

toUpper chuyển một kí tự thành chữ viết in. Các dấu cách, chữ số, v.v. giữ nguyên không đổi.

toLower chuyển một kí tự thành chữ viết thường.

toTitle chuyển một kí tự thành chữ tiêu đề. Tuyệt đại đa số kí tự thfi chữ tiêu đề cũng như chữu viết in.

digitToInt chuyển một kí tự thành kiểu Int. Muốn chuyển đổi được, kí tự phải nằm trong khoảng '0'..'9', 'a'..'f' hoặc 'A'..'F'.

ghci> map digitToInt "34538"
[3,4,5,3,8]
ghci> map digitToInt "FF85AB"
[15,15,8,5,10,11]

intToDigit là hàm ngược của digitToInt. Nó nhận vào một Int trong khoảng từ 0..15 rồi chuyển sang một chữ cái viết thường.

ghci> intToDigit 15
'f'
ghci> intToDigit 5
'5'

Các hàm ordchr chuyển đổi kí tự thành số tương ứng và ngược lại:

ghci> ord 'a'
97
ghci> chr 97
'a'
ghci> map ord "abcdefgh"
[97,98,99,100,101,102,103,104]

Hiệu số giữa hai giá trị ord của hai kí tự sẽ bằng khoảng cách giữa hai kí tự đó trong bảng mã Unicode.

Mật mã Caesar là một phương pháp thô sơ để mã hóa thông điệp chứa nhiều kí tự bằng cách chuyển từng kí tự đi một số vị trí nhất định trong bảng chữ cái. Ta có thể dễ dàng tạo ra một dạng mật mã Caesar, chỉ khác là không bị giới hạn ở những kí tự trong bảng chữ cái.

encode :: Int -> String -> String
encode shift msg =
    let ords = map ord msg
        shifted = map (+ shift) ords
    in  map chr shifted

Ở đây, đầu tiên là ta chuyển chuỗi thành một danh sách các số. Sau đó ta đem cộng khoảng dịch chuyển vào từng số trước khi chuyển ngược danh sách số về những kí tự. Nếu là người chuyên dùng hàm hợp thì bạn có thể viết phần thân của hàm này như sau:map (chr . (+ shift) . ord) msg. Ta hãy thử mã hóa một vài thông điệp.

ghci> encode 3 "Heeeeey"
"Khhhhh|"
ghci> encode 4 "Heeeeey"
"Liiiii}"
ghci> encode 1 "abcd"
"bcde"
ghci> encode 5 "Marry Christmas! Ho ho ho!"
"Rfww~%Hmwnxyrfx&%Mt%mt%mt&"

Việc mã hóa như vậy là ổn. Việc giải mã về cơ bản là dịch chuyển ngược về một số vị trí như ban đầu đã dịch chuyển đi.

decode :: Int -> String -> String
decode shift msg = encode (negate shift) msg
ghci> encode 3 "Im a little teapot"
"Lp#d#olwwoh#whdsrw"
ghci> decode 3 "Lp#d#olwwoh#whdsrw"
"Im a little teapot"
ghci> decode 5 . encode 5 $ "This is a sentence"
"This is a sentence"

Data.Map

Danh sách liên kết (còn có tên gọi “từ điển”) là những danh sách được dùng để lưu những cặp khóa-giá trị, hay khóa-trị, trong đó thứ tự không thành vấn đề. Chẳng hạn, ta có thể dùng danh sách liên kết để lưu trữ các số điện thoại, trong đó số điện thoại là giá trị và tên người là khóa. Ta không quan tâm đến thứ tự lưu trữ thế nào, chỉ cần lấy được đúng số điện thoại của từng người.

Cách tự nhiên nhất để biểu thị danh sách liên kết trong Haskell là có một danh sách các cặp. Thành phần thứ nhất trong cặp chính là khóa, còn thành phần thứ hai là giá trị. Sau đây là ví dụ một danh sách liên kết chứa các số điện thoại:

phoneBook = 
    [("betty","555-2938")
    ,("bonnie","452-2928")
    ,("patsy","493-2928")
    ,("lucille","205-2928")
    ,("wendy","939-8282")
    ,("penny","853-2492")
    ]

Dù cho cách viết thụt đầu dòng như thế này có vẻ hơi lạ, song đây chỉ là một danh sách các cặp. Nhiệm vụ thường gặp nhất khi xử lý danh sách liên kết là tra tìm giá trị theo khóa. Ta hãy lập ra một hàm để tra tìm giá trị dựa theo khóa nào đó cho trước.

findKey :: (Eq k) => k -> [(k,v)] -> v
findKey key xs = snd . head . filter (\(k,v) -> key == k) $ xs

Khá đơn giản. Hàm này nhận vào một khóa và một danh sách, lọc danh sách để chỉ giữ lại những khóa nào khớp, rồi lấy cặp khóa-trị khớp đầu tiên và trả lại giá trị. Nhưng điều gì sẽ xảy ra nếu khóa ta cần tìm không có trong danh sách liên kết? Hừm. Ở đây, nếu khóa không có trong danh sách liên kết, rút cục ta sẽ lấy phần tử đầu của một danh sách rỗng, và gây ra lỗi thực thi. Tuy nhiên, ta cần tránh cho chương trình đổ vỡ, vì vậy hãy dùng kiểu dữ liệu Maybe. Nếu tìm thấy khóa, hãy trả lại Nothing. Nếu tìm thấy, ta sẽ trả lại Just something, trong đó something là giá trị ứng với khóa đó.

findKey :: (Eq k) => k -> [(k,v)] -> Maybe v
findKey key [] = Nothing
findKey key ((k,v):xs) = if key == k
                            then Just v
                            else findKey key xs

Hãy nhìn vào lời khái báo kiểu. Nó nhận vào một khóa có thể so sánh [bằng hoặc khác] được, cùng một danh sách liên kết, rồi có thể tạo ra một giá trị. Nghe cũng hợp lý.

Đây là một ví dụ giáo khoa về hàm đệ quy hoạt động trên một danh sách. Có đủ cả điều kiện biên, việc chia danh sách thành đầu và phần đuôi, và các lời gọi đệ quy. Đây là dạng mẫu gấp có tính kinh điển, vì vậy ta sẽ xem cách lập nó bằng hàm gấp như thế nào.

findKey :: (Eq k) => k -> [(k,v)] -> Maybe v
findKey key = foldr (\(k,v) acc -> if key == k then Just v else acc) Nothing
Lưu ý: Thường thì nên sử dụng các hàm gấp cho dạng mẫu đệ quy tiêu chuẩn với danh sách này, thay vì viết tường minh cấu trúc đệ quy, vì cách làm thứ nhất dễ đọc và nhận diện hơn. Mọi người đều biết đó là hàm gấp khi trông thấy lời gọi foldr, nhưng sẽ mất chút thời gian hình dung để đọc được cấu trúc đệ quy tường minh.
ghci> findKey "penny" phoneBook
Just "853-2492"
ghci> findKey "betty" phoneBook
Just "555-2938"
ghci> findKey "wilma" phoneBook
Nothing

legomap

Chạy đẹp mê ly! Nếu ta tìm đuợc số điện thoại của cô gái, ta sẽ có Just số điện thoại, còn không thì Nothing.

Ta vừa tạo lập hàm lookup từ Data.List. Nếu muốn tìm giá trị tương ứng với một khóa, ta phải duyệt suốt toàn bộ những phần tử của danh sách đến khi tìm thấy. Module Data.Map có những danh sách liên kết chạy nhanh hơn (vì bản chất chúng được lập theo cấu trúc cây) và cũng có nhiều hàm ứng dụng hơn. Từ giờ trở đi, ta sẽ coi rằng đang làm việc với ánh xạ (map) thay vì các danh sách liên kết.

Data.Map xuất khẩu các hàm có tên xung đột với những hàm trong PreludeData.List, nên ta sẽ viết lệnh nhập chọn lọc.

import qualified Data.Map as Map

Đặt lệnh nhập nói trên vào trong một mã lệnh chương trình rồi tải mã lệnh đó từ GHCI.

Hãy tiếp tục và xem rằng Data.Map có gì cho chúng ta! Sau đây là tóm tắt cơ bản về các hàm trong đó.

Hàm fromList nhận vào một danh sách liên kết (dưới dạng danh sách) rồi trả về ánh xạ với cùng những mối liên kết đó.

ghci> Map.fromList [("betty","555-2938"),("bonnie","452-2928"),("lucille","205-2928")]
fromList [("betty","555-2938"),("bonnie","452-2928"),("lucille","205-2928")]
ghci> Map.fromList [(1,2),(3,4),(3,2),(5,5)]
fromList [(1,2),(3,2),(5,5)]

Nếu có những khóa trùng nhau trong trong danh sách ban đầu, thì những thứ lặp thừa sẽ được bỏ đi. Sau đây là dấu ấn kiểu của fromList

Map.fromList :: (Ord k) => [(k, v)] -> Map.Map k v

Nó nói rằng hàm này nhận vào một danh sách những cặp kiểu kv rồi trả về một ánh xạ trong đó chiếu các khóa có kiểu k đến kiểu v. Lưu ý rằng khi ta lập các danh sách liên kết từ danh sách thường, thì các khóa chỉ cần so sánh đồng nhất [bằng hoặc khác] được (nghĩa là kiểu của chúng thuộc về lớp Eq) nhưng trong trường hợp này thì chúng còn phải so sánh được. Đó là một điều kiện bắt buộc trong module Data.Map. Nó yêu cầu các khóa phải có thứ tự thì mới sắp xếp chúng lên cây được.

Bạn nên luôn dùng đến Data.Map cho các liên kết khóa-trị trừ khi bạn có trong tay các khóa không thuộc về lớp Ord.

empty biểu thị một ánh xạ rỗng. Nó không nhận đối số nào, và chỉ trả lại một ánh xạ rỗng.

ghci> Map.empty
fromList []

insert nhận vào một khóa, một giá trị và một ánh xạ rồi trả về một ánh xạ mới gần giống ánh xạ cũ, chỉ khác là chèn thêm khóa và giá trị vào.

ghci> Map.empty
fromList []
ghci> Map.insert 3 100 Map.empty
fromList [(3,100)]
ghci> Map.insert 5 600 (Map.insert 4 200 ( Map.insert 3 100  Map.empty))
fromList [(3,100),(4,200),(5,600)]
ghci> Map.insert 5 600 . Map.insert 4 200 . Map.insert 3 100 $ Map.empty
fromList [(3,100),(4,200),(5,600)]

Ta có thể tự lập riêng fromList bằng cách dùng ánh xạ rỗng, insert và một hàm gấp. Nhìn này:

fromList' :: (Ord k) => [(k,v)] -> Map.Map k v
fromList' = foldr (\(k,v) acc -> Map.insert k v acc) Map.empty

Đây là một phép gấp khá trực tiếp. Ta bắt đầu bằng một ánh xạ rỗng rồi gấp nó từ phía phải, bằng việc dần chèn các cặp khóa-trị vào danh sách tích lũy.

null kiểm tra xem liệu một ánh xạ có rỗng hay không.

ghci> Map.null Map.empty
True
ghci> Map.null $ Map.fromList [(2,3),(5,5)]
False

size báo lại kích cỡ của ánh xạ.

ghci> Map.size Map.empty
0
ghci> Map.size $ Map.fromList [(2,4),(3,3),(4,2),(5,4),(6,4)]
5

singleton nhận vào một khóa và một giá trị rồi tạo ra một ánh xạ chỉ có đúng một phần tử.

ghci> Map.singleton 3 9
fromList [(3,9)]
ghci> Map.insert 5 9 $ Map.singleton 3 9
fromList [(3,9),(5,9)]

lookup hoạt động giống như Data.List lookup, chỉ khác là nó hoạt động với ánh xạ. Hàm này trả lại Just something nếu tìm thấy something tương ứng với khoá và Nothing nếu không tìm thấy gì.

member là một vị từ nhận vào một khóa và một ánh xạ rồi báo lại xem liệu khóa có nằm trong ánh xạ hay không.

ghci> Map.member 3 $ Map.fromList [(3,6),(4,3),(6,9)]
True
ghci> Map.member 3 $ Map.fromList [(2,5),(4,5)]
False

mapfilter hoạt động rất giống với các dạng tương đương dùng trong danh sách.

ghci> Map.map (*100) $ Map.fromList [(1,1),(2,4),(3,9)]
fromList [(1,100),(2,400),(3,900)]
ghci> Map.filter isUpper $ Map.fromList [(1,'a'),(2,'A'),(3,'b'),(4,'B')]
fromList [(2,'A'),(4,'B')]

toList là hàm nghịch đảo của fromList.

ghci> Map.toList . Map.insert 9 2 $ Map.singleton 4 3
[(4,3),(9,2)]

keyselems lần lượt trả lại danh sách các khóa và giá trị. keys là dạng tương đương của map fst . Map.toList còn elems tương đương với map snd . Map.toList.

fromListWith là một hàm nhỏ rất hay. Nó hoạt động giống như fromList, chỉ khác là nó không bỏ đi những khóa trùng lặp thừa, thay vào đó là dùng một hàm được cung cấp để quyết định xem nên làm gì với những khóa trùng lặp đó. Giả sử rằng một cô gái có thể có nhiều số điện thoại và ta có một danh sách liên kết được lập nên như sau.

phoneBook = 
    [("betty","555-2938")
    ,("betty","342-2492")
    ,("bonnie","452-2928")
    ,("patsy","493-2928")
    ,("patsy","943-2929")
    ,("patsy","827-9162")
    ,("lucille","205-2928")
    ,("wendy","939-8282")
    ,("penny","853-2492")
    ,("penny","555-2111")
    ]

Bây giờ nếu chỉ dùng fromList để đưa số liệu trên vào một ánh xạ, thì ta sẽ đánh mất một vài số điện thoại! Vì vậy ta sẽ làm như sau:

phoneBookToMap :: (Ord k) => [(k, String)] -> Map.Map k String
phoneBookToMap xs = Map.fromListWith (\number1 number2 -> number1 ++ ", " ++ number2) xs
ghci> Map.lookup "patsy" $ phoneBookToMap phoneBook
"827-9162, 943-2929, 493-2928"
ghci> Map.lookup "wendy" $ phoneBookToMap phoneBook
"939-8282"
ghci> Map.lookup "betty" $ phoneBookToMap phoneBook
"342-2492, 555-2938"

Nếu tìm thấy một khóa trùng lặp thì hàm sẽ dùng đến hàm được truyền vào, để kết hợp giá trị của các khóa đó vào một giá trị khác. Ta cũng có thể bắt đầu bằng việc chuyển các giá trị trong danh sách liên kết thành các danh sách một phần tử rồi mới dùng ++ để kết hợp các con số.

phoneBookToMap :: (Ord k) => [(k, a)] -> Map.Map k [a]
phoneBookToMap xs = Map.fromListWith (++) $ map (\(k,v) -> (k,[v])) xs
ghci> Map.lookup "patsy" $ phoneBookToMap phoneBook
["827-9162","943-2929","493-2928"]

Khá gọn gàng! Một trường hợp sử dụng khác là khi đang lập một ánh xạ từ danh sách liên kết gồm các số, ta tìm thấy một khóa bị trùng, và muốn giữ lại giá trị lớn nhất để cho khóa.

ghci> Map.fromListWith max [(2,3),(2,5),(2,100),(3,29),(3,22),(3,11),(4,22),(4,15)]
fromList [(2,100),(3,29),(4,22)]

Hoặc là ta có thể chọn để cộng lại các giá trị có cùng khóa.

ghci> Map.fromListWith (+) [(2,3),(2,5),(2,100),(3,29),(3,22),(3,11),(4,22),(4,15)]
fromList [(2,108),(3,62),(4,37)]

insertWith đối với insert thì cũng như fromListWith đối với fromList. Nó chèn một cặp khóa-trị vào trong một ánh xạ, nhưng nếu ánh xạ đó đã sẵn chứa khóa này, thì nó sẽ dùng hàm được truyền vào để quyết định xem cần làm gì.

ghci> Map.insertWith (+) 3 100 $ Map.fromList [(3,4),(5,103),(6,339)]
fromList [(3,104),(5,103),(6,339)]

Chỉ có một vài hàm trong Data.Map. Bạn có thể tìm thấy danh sách đầy đủ các hàm ở tài liệu .

Data.Set

legosets

Module Data.Set cho ta các tập hợp. Giống như tập hợp toán học ấy. Tất cả các phần tử trong tập hợp đều là duy nhất. Và vì bản chất là chúng được lập bằng cấu trúc cây (rất giống với ánh xạ trong Data.Map), nên chúng có thứ tự. Việc kiểm tra quan hệ thuộc, chèn phần tử, xóa phần tử, v.v. nhanh hơn nhiều so với những thao tác tương tự trên danh sách. Phép thao tác thường gặp nhất khi làm việc với tập hợp là chèn vào tập hợp, kiểm tra xem phần tử có thuộc tập hợp hay không, và chuyển đổi tập hợp thành danh sách.

Vì các tên hàm trong Data.Set xung đột [trùng lặp] nhiều với tên trong PreludeData.List, nên ta cần nhập có chọn lọc.

Hãy viết câu lệnh nhập này trong file mã lệnh:

import qualified Data.Set as Set

Rồi sau đó nạp file mã lệnh trong GHCI.

Giả sử rằng ta có hai đoạn văn bản chữ. Ta cần tìm ra xem những kí tự nào được dùng trong cả hai đoạn đó.

text1 = "I just had an anime dream. Anime... Reality... Are they so different?"
text2 = "The old man left his garbage can out and now his trash is all over my lawn!"

Hàm fromList hoạt động rất giống với những gì bạn trông đợi. Nó nhận vòa một danh sách rồi chuyển đổi nó thành một tập hợp.

ghci> let set1 = Set.fromList text1
ghci> let set2 = Set.fromList text2
ghci> set1
fromList " .?AIRadefhijlmnorstuy"
ghci> set2
fromList " !Tabcdefghilmnorstuvwy"

Như bạn đã thấy, các phần tử được xếp thứ tự và mỗi phần tử là duy nhất. Bây giờ hãy dùng hàm intersection để xem những phần tử nào có chung trong cả hai tập hợp này.

ghci> Set.intersection set1 set2
fromList " adefhilmnorstuy"

Ta có thể dùng hàm difference để xem những chữu cái nào có trong tập hợp thứ nhất nhưng không có trong tập hợp thứ hai, và ngược lại.

ghci> Set.difference set1 set2
fromList ".?AIRj"
ghci> Set.difference set2 set1
fromList "!Tbcgvw"

Hoặc là ta có thể xem những chữ cái duy nhất dùng trong hai câu này [không cần chung] bằng hàm union.

ghci> Set.union set1 set2
fromList " !.?AIRTabcdefghijlmnorstuvwy"

Các hàm null, size, member, empty, singleton, insertdelete, tất cả đều hoạt động theo cách bạn trông đợi.

ghci> Set.null Set.empty
True
ghci> Set.null $ Set.fromList [3,4,5,5,4,3]
False
ghci> Set.size $ Set.fromList [3,4,5,3,4,5]
3
ghci> Set.singleton 9
fromList [9]
ghci> Set.insert 4 $ Set.fromList [9,3,8,1]
fromList [1,3,4,8,9]
ghci> Set.insert 8 $ Set.fromList [5..10]
fromList [5,6,7,8,9,10]
ghci> Set.delete 4 $ Set.fromList [3,4,5,4,3,4,5]
fromList [3,5]

Ta cũng có thể kiểm tra các mối quan hệ tập con hoặc tập con thực sự. Tập hợp A được gọi là tập con của tập B nếu B chứa tất cả những phần tử có trong A. Tập A được gọi là tập con thực sự của tập B nếu B chứa tất cả những phần tử của A, ngoài ra còn những phần tử khác nữa.

ghci> Set.fromList [2,3,4] `Set.isSubsetOf` Set.fromList [1,2,3,4,5]
True
ghci> Set.fromList [1,2,3,4,5] `Set.isSubsetOf` Set.fromList [1,2,3,4,5]
True
ghci> Set.fromList [1,2,3,4,5] `Set.isProperSubsetOf` Set.fromList [1,2,3,4,5]
False
ghci> Set.fromList [2,3,4,8] `Set.isSubsetOf` Set.fromList [1,2,3,4,5]
False

Ta cũng có thể map [ánh xạ] trên tập hợp và filter [lọc] chúng.

ghci> Set.filter odd $ Set.fromList [3,4,5,6,7,2,3,4]
fromList [3,5,7]
ghci> Set.map (+1) $ Set.fromList [3,4,5,6,7,2,3,4]
fromList [3,4,5,6,7,8]

Tập hợp thường được dùng để gạt một tập hợp những phần tử trùng lặp khỏi một danh sách gốc bằng cách đầu tiên là chuyển nó về một tập hợp, bằng hàm fromList và rồi chuyển ngược nó về bằng toList. Hàm nub trong Data.List đã làm việc đó, nhưng việc gạt bỏ trùng lặp trên danh sách lớn sẽ được thực hiện nhanh hơn nhiều nếu bạn nhồi nó vào một tập hợp rồi chuyển ngược về danh sách bằng cách dùng nub. Nhưng để dùng nub chỉ cần kiểu của các phần tử danh sách thuộc về lớp Eq, còn nếu muốn nhồi các phần tử vào tập hợp thì kiểu của danh sách phải thuộc về Ord.

ghci> let setNub xs = Set.toList $ Set.fromList xs
ghci> setNub "HEY WHATS CRACKALACKIN"
" ACEHIKLNRSTWY"
ghci> nub "HEY WHATS CRACKALACKIN"
"HEY WATSCRKLIN"

Nhìn chung, setNub nhanh hơn nub khi hoạt động trên danh sách lớn, song như bạn có thể thấy, nub bảo toàn thứ tự các phần tử trong danh sách, còn setNub thì không.

Tự lập ra các module của mình

making modules

Đến giờ ta đã xem xét một vài module khá hay, nhưng làm thế nào để tự tạo lập được module của riêng mình? Hầu hết ngôn ngữ lập trình đều cho phép bạn phân chia mã lệnh thành nhiều file và Haskell cũng không phải ngoại lệ. Khi viết chương trình, một phương pháp hay là nhóm các hàm và kiểu dùng cho một mục đích cụ thể vào cùng một module. Bằng cách này, bạn có thể dễ dàng sử dụng lại các hàm đó trong những chương trình khác, chỉ việc nhập module bạn đã viết.

Ta hãy xem làm thế nào để tự lập ra các module, qua việc chế tạo một module nhỏ chứa các hàm để tính thể tích và diện tích của một số khối hình học. Ta sẽ bắt đầu bằng việc tạo ra một file có tên là Geometry.hs.

Ta nói rằng một module xuất khẩu các hàm. Điều này có nghĩa là khi tôi nhập một module, tôi có thể dùng các hàm mà module đó xuất khẩu. Dù module này có thể định nghĩa các hàm mà bên trong nó các hàm khác của nó gọi đến, nhưng ta chỉ có thể thấy được những hàm nào mà module xuất khẩu.

Ở đầu mỗi module, chúng ta chỉ định tên của module. Nếu ta có file tên là Geometry.hs, thì ta nên đặt tên module là Geometry. Tiếp theo, ta chỉ định tên các hàm mà module này xuất khẩu, và sau đó ta có thể bắt tay vào viết các hàm. Vì vậy ta sẽ bắt đầu như sau:

module Geometry
( sphereVolume
, sphereArea
, cubeVolume
, cubeArea
, cuboidArea
, cuboidVolume
) where

Bạn thấy đấy, chúng ta sẽ tính diện tích và thể tích các khối cầu, khối lập phương và khối hộp chữ nhật. Hãy tiếp tục đi định nghĩa các hàm:

module Geometry
( sphereVolume
, sphereArea
, cubeVolume
, cubeArea
, cuboidArea
, cuboidVolume
) where

sphereVolume :: Float -> Float
sphereVolume radius = (4.0 / 3.0) * pi * (radius ^ 3)

sphereArea :: Float -> Float
sphereArea radius = 4 * pi * (radius ^ 2)

cubeVolume :: Float -> Float
cubeVolume side = cuboidVolume side side side

cubeArea :: Float -> Float
cubeArea side = cuboidArea side side side

cuboidVolume :: Float -> Float -> Float -> Float
cuboidVolume a b c = rectangleArea a b * c

cuboidArea :: Float -> Float -> Float -> Float
cuboidArea a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2

rectangleArea :: Float -> Float -> Float
rectangleArea a b = a * b

Ở đây toàn những kiến thức hình học cơ bản. Dù vậy, có một điều cần lưu ý. Vì một khối lập phương chỉ là một trường hợp riêng của khối hộp chữ nhật, nên ta định nghĩa diện tích và thể tích khối lập phương như là khối hộp mà các cạnh đều dài bằng nhau. Ta cũng định nghĩa một hàm phụ trợ tên là rectangleArea, để tính diện tích khối hộp chữ nhật dựa trên chiều dài các cạnh bên của nó. Hàm này khá “tủn mủn” vì ta chỉ dùng nó trong module này thôi (cụ thể là dùng trong các hàm cuboidAreacuboidVolume) mà không xuất khẩu nó! Vì ta muốn module được viết chỉ trình bày các hàm để tính cho hình khối ba chiều, nên tuy dùng đến rectangleArea nhưng ta không xuất khẩu nó.

Khi tạo module, ta thường chỉ xuất các hàm nào đóng vai trò như một giao diện đến module, còn những chi tiết thực hiện cần phải giấu đi. Khi người khác dùng module Geometry, bản thân họ không quan tâm đến các hàm mà ta không xuất khẩu. Ta có thể quyết định thay đổi toàn bộ những hàm này, hoặc xóa bỏ chúng trong phiên bản mới (ta có thể xóa bỏ hàm rectangleArea và chỉ dùng phép *) mà không ai để ý đến vì từ đầu ta đã không xuất khẩu hàm đó.

Để sử dụng module vừa tạo lập, ta chỉ cần viết:

import Geometry

Để thực hiện điều này, Geometry.hs phải nằm trong cùng thư mục với chương trình nhập nó.

Ta cũng có thể bố trí cấu trúc thừa kế đối với module. Mỗi module có thể chứa một số các module nhỏ và mỗi module nhỏ có thể chứa module nhỏ của riêng mình. Ta hãy tách nhỏ ra, cụ thể là module Geometry có ba module con, mỗi module cho một loại hình khối.

Trước hết, ta sẽ tạo một thư mục có tên Geometry. Chú ý chữ G viết in. Trong đó, ta đặt ba file: Sphere.hs, Cuboid.hs, vaf Cube.hs. Sau đây là nội dung từng file:

Sphere.hs

module Geometry.Sphere
( volume
, area
) where

volume :: Float -> Float
volume radius = (4.0 / 3.0) * pi * (radius ^ 3)

area :: Float -> Float
area radius = 4 * pi * (radius ^ 2)

Cuboid.hs

module Geometry.Cuboid
( volume
, area
) where

volume :: Float -> Float -> Float -> Float
volume a b c = rectangleArea a b * c

area :: Float -> Float -> Float -> Float
area a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2

rectangleArea :: Float -> Float -> Float
rectangleArea a b = a * b

Cube.hs

module Geometry.Cube
( volume
, area
) where

import qualified Geometry.Cuboid as Cuboid

volume :: Float -> Float
volume side = Cuboid.volume side side side

area :: Float -> Float
area side = Cuboid.area side side side

Được rồi! Đầu tiên là Geometry.Sphere. Lưu ý cách ta đặt nó vào trong một thư mục có tên Geometry và rồi định nghĩa module có tên là Geometry.Sphere. Ta làm việc tương tự đối với khối hộp chữ nhật. Cũng lưu ý cách mà trong 3 module con, ta đã định các hàm có cùng tên. Ta có thể làm điều này được vì chúng là các module riêng rẽ. Ta muốn dùng các hàm từ Geometry.Cuboid trong Geometry.Cube nhưng không thể trực tiếp viết import Geometry.Cuboid vì module này xuất khẩu các hàm có cùng tên với Geometry.Cube. Đó là lý do tại sao ta thực hiện nhập có chọn lọc và mọi việc đều ổn.

Vì vậy bây giờ nếu ta trong một file có cùng cấp với thư mục Geometry thì ta có thể viết, chẳng hạn:

import Geometry.Sphere

Rồi ta có thể gọi areavolume và chúng sẽ trả lại diện tích và thể tích một khối cầu. Và nếu ta muốn tráo đổi nhiều module thế này, ta phải nhập chọn lọc vì các module đó xuất khẩu các hàm có cùng tên. Vì vậy ta chỉ cần viết kiểu như sau:

import qualified Geometry.Sphere as Sphere
import qualified Geometry.Cuboid as Cuboid
import qualified Geometry.Cube as Cube

Và rồi ta có thể gọi Sphere.area, Sphere.volume, Cuboid.area, v.v. Mỗi hàm sẽ tính diện tích hoặc thể tích của hình khối tương ứng.

Lần tới khi bạn phải viết một file rất lớn và có rất nhiều hàm, hãy cố thử xem những hàm nào phục vụ cùng một mục đích chung và xem liệu bạn có thể đặt chúng vào một module riêng hay không. Bạn sẽ có thể chỉ nhập module riêng đó lần tới khi bạn viết một chương trình đòi hỏi tính năng tương tự nào đó.

Advertisements

1 phản hồi

Filed under Haskell

One response to “Chương 7: Module

  1. Pingback: Tự học lấy Haskell | Blog của Chiến

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s