Chương 2: Xuất phát

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

Chuẩn bị, sẵn sàng, xuất phát!

Được rồi, ta hãy bắt đầu! Nếu bạn là kiểu người tồi tệ, không đọc phần hướng dẫn bao giờ và đã bỏ qua chương trước, có thể đằng nào bạn vẫn phải đọc mục cuối cùng ở đó vì nó giải thích những gì bạn cần để theo được quyển hướng dẫn này và cách chúng ta nạp các hàm như sắp thực hiện. Điều đầu tiên ta sẽ làm là chạy chế độ tương tác của ghc và gọi một số hàm để có được cảm nhận chung về haskell. Hãy mở cửa sổ lệnh ra và gõ vào ghci. Bạn sẽ được đón chào với dòng chữ kiểu như sau.

GHCi, version 6.8.2: http://www.haskell.org/ghc/ 😕 for help
Loading package base ... linking ... done.
Prelude>

egg Xin chúc mừng, bạn đã vào được GHCI! Dấu nhắc ở đây là Prelude> nhưng vì nó có thể dài thêm khi bạn nạp nhiều thứ trong khi làm việc, nên ta sẽ dùng kí hiệu ghci>. Nếu bạn muốn có dấu nhắc giống như vậy, chỉ cần gõ vào :set prompt "ghci> ". Sau đây là một số phép toán số học đơn giản.

ghci> 2 + 15
17
ghci> 49 * 100
4900
ghci> 1892 - 1472
420
ghci> 5 / 2
2.5
ghci>

Điều này thật dễ giải thích. Ta cũng có thể thực hiện một vài phép tính trên một dòng và trình tự tính toán giống như thông thường. Cũng có thể dùng cặp ngoặc đơn để làm cho thứ tự phép tính rõ ràng hơn, hoặc để thay đổi thứ tự thực hiện.

ghci> (50 * 100) - 4999
1
ghci> 50 * 100 - 4999
1
ghci> 50 * (100 - 4999)
-244950

Hay đấy nhỉ? Ừ, tôi biết rằng cũng không có gì hay nhưng xin bạn hãy thông cảm một chút. Có một lỗi dễ mắc phải ở đây là số âm. Nếu bạn muốn có một số âm, tốt nhất là luôn luôn kẹp nó giữa cặp ngoặc đơn. Nếu viết 5 * -3 GHCI sẽ rầy la bạn nhưng viết 5 * (-3) sẽ ổn. Phép tính logic (boole) cũng tương đối rạch ròi. Bạn có thể đã biết, && nghĩa là phép logic , || nghĩa là phép logic hoặc. not làm phủ định một giá trị True (đúng), hoặc giá trị False (sai).

ghci> True && False
False
ghci> True && True
True
ghci> False || True
True 
ghci> not False
True
ghci> not (True && True)
False

Việc kiểm tra sự bằng nhau được làm như sau.

ghci> 5 == 5
True
ghci> 1 == 0
False
ghci> 5 /= 5
False
ghci> 5 /= 4
True
ghci> "hello" == "hello"
True

Thế còn việc viết 5 + "llama" hay 5 == True? À, nếu bạn thử gõ vào lệnh thứ nhất, bạn sẽ nhận được một thông báo lỗi đáng sợ!

No instance for (Num [Char])
arising from a use of `+' at <interactive>:1:0-9
Possible fix: add an instance declaration for (Num [Char])
In the expression: 5 + "llama"
In the definition of `it': it = 5 + "llama"

Oái! Điều mà GHCI báo với chúng ta ở đây là "llama" không phải là một số vì vậy nó không biết cộng từ này với số 5. Ngay cả khi từ đó không phải "llama" mà là "four" (4) hay "4" đi nữa, Haskell vẫn không thể coi nó là một con số. Dấu + trông đợi cả bên trái và bên phải nó đều là số. Nếu ta thử gõ vào True == 5, GHCI sẽ bảo ta là kiểu của chúng không khớp nhau. Trong khi + chỉ làm việc được với những thứ được coi là số, thì == làm việc với bất kì hai thứ nào so sánh được với nhau. Nhưng điều cần nói ở đây là chúng phải có cùng kiểu. Bạn không thể so sánh cam với táo. Sau này chúng ta sẽ xem xét kĩ hơn về kiểu. Lưu ý, bạn có thể gõ vào 5 + 4.05 giỏi luồn lách và có thẻ đóng vai trò số nguyên lẫn số có phần thập phân (dấu phẩy động). 4.0 thì không thể làm số nguyên, vì vậy 5 phải biến đổi để thích nghi. Bạn có thể chưa biết điều này, nhưng từ đầu đến giờ chúng ta luôn luôn dùng các hàm. Chẳng hạn, * là một hàm nhận vào hai số rồi nhân chúng với nhau. Như bạn đã thây, chúng ta gọi hàm này bằng cách kẹp nó giữa hai số. Đó là kiểu hàm trung tố. Đa số các hàm không thực hiện phép tính với con số thì là hàm tiền tố. Ta hãy xem xét chúng. phoen Các hàm thường có dạng tiền tố vì vậy từ giờ trở đi ta sẽ không nói cụ thể là một hàm viết theo kiểu tiền tố, mà giả định sẵn như vậy rồi. Trong đa số các ngôn ngữ lập trình mệnh lệnh, hàm được gọi bằng cách viết tên hàm rồi viết các tham số của nó trong cặp ngoặc tròn, thường được phân tách bởi các dấu phẩy. Trong Haskell, hàm được gọi bằng cách viết tên hàm, một dấu cách, sau đó là các tham số, được phân biệt bởi các dấu cách. Để bắt đầu, ta sẽ thử gọi một trong số các hàm nhàm chán nhất của Haskell.

ghci> succ 8
9

Hàmsucc nhận vào bất cứ thứ gì mà có thứ kế tiếp nó được xác định, rồi trả lại thứ đứng kế tiếp này. Như bạn có thể thấy, ta chỉ ngăn cách tên hàm với tham số bằng một dấu cách. Việc gọi hàm với vài tham số khác nhau cũng đơn giản. Hàm minmax nhận vào hai thứ có thể sắp xếp được (chẳng hạn hai con số!). min trả lại số nhỏ hơn còn max trả lại số lớn hơn. Bạn hãy tự kiểm tra điều này.

ghci> min 9 10
9
ghci> min 3.4 3.2
3.2
ghci> max 100 101
101

Phép áp dụng hàm (gọi hàm bằng cách đặt một dấu cách ở sau nó rồi gõ vào các tham số) là phép toán có độ ưu tiên cao nhất. Điều này, đối với chúng ta, có nghĩa là hai câu lệnh sau thì tương đương.

ghci> succ 9 + max 5 4 + 1
16
ghci> (succ 9) + (max 5 4) + 1
16

Tuy nhiên, nếu ta muốn tìm số đứng liền sau kết quả tích giữa các số 9 và 10, ta không thể viết succ 9 * 10 bởi nếu thế thì máy sẽ lấy số đứng liền sau của 9, như vậy là số 10 đem nhân với 10. Tức là 100. Ta phải viết là succ (9 * 10) mới được kết quả muốn tìm là 91. Nếu một hàm nhận hai tham số, ta cũng có thể gọi nó dưới dạng hàm trung tố bằng cách kẹp trung tố này giữa hai dấu nháy ngược. Chẳng hạn, hàm div nhận vào hai số nguyên và thực hiện phép chia nguyên giữa chúng. Viết div 92 10 ta thu được số 9. Nhưng khi ta gọi hàm như vậy, có thể vẫn gây nhầm lẫn rằng đâu là số bị chia và đâu là số chia. Vì vậy ta có thể gọi hàm này dưới dạng trung tố bằng cách viết 92 `div` 10 và đột nhiên nó đã rõ ràng hơn nhiều. Nhiều người trước đây học ngôn ngữ mệnh lệnh có xu hướng gắn với kí hiệu trong đó cặp ngoặc biểu thị cho áp dụng hàm. Chẳng hạn, trong C, bạn dùng cặp ngoặc để gọi những hàm kiểu như foo(), bar(1) hay baz(3, "haha"). Như tôi đã nói, dấu cách được dùng cho áp dụng hàm trong Haskell. Vì vậy những hàm đó nếu trong Haskell sẽ được viết là foo, bar 1baz 3 "haha". Bởi thế, nếu bạn thấy một chỗ mã lệnh nào đó như bar (bar 3), thì nó không có nghĩa là bar được gọi với các tham số bar3. Mà điều đó có nghĩa là đầu tiên ta gọi hàm bar với tham số 3 để nhận được một số nào đó rồi mới gọi bar một lần nữa với số vừa thu được. Nếu viết trong C, đoạn mã lệnh sẽ là bar(bar(3)).

Các hàm đầu tiên ở trình độ trẻ con

Ở mục trước ta đã có một cảm nhận cơ bản về việc gọi hàm. Bây giờ, ta hãy thử làm riêng hàm của bạn! Hãy mở trình soạn file chữ bạn ưa thích rồi gõ vào hàm sau để nhận vào một con số rồi nhân đôi nó.

doubleMe x = x + x

Các hàm được định nghĩa theo cách tương tự như cách nó được gọi. Tên hàm được theo sau bởi những tham số được tách rời bởi các dấu cách. Nhưng khi định nghĩa hàm, phải có một dấu = và sau đó ta sẽ định nghĩa xem hàm thực hiện điều gì. Hãy lưu file này lại với tên baby.hs hoặc một tên nào đó. Bây giờ di chuyển tới thư mục nơi bạn vừa lưu file rồi chạy ghci từ đó. Một khi đã ở trong GHCI, hãy gõ vào :l baby. Bây giờ khi đoạn mã của chúng ta được tải lên, ta có thể nghịch với các hàm vừa định nghĩa.

ghci> :l baby
[1 of 1] Compiling Main             ( baby.hs, interpreted )
Ok, modules loaded: Main.
ghci> doubleMe 9
18
ghci> doubleMe 8.3
16.6

+ cũng dùng được với cả những số nguyên lẫn số dấu phẩy động (có phần thập phân bất kì), nên hàm vừa viết có thể tính với bất kì số nào. Ta hãy tạo một hàm nhận vào hai số, đem nhân từng số với 2 rồi cộng chúng lại.

doubleUs x y = x*2 + y*2

Đơn giản. Ta cũng có thể đjnh nghĩa nó như doubleUs x y = x + x + y + y. Việc chạy thử sẽ cho ta kết quả chắc như dự đoán (hãy nhở thêm hàm này vào file baby.hs, lưu file lại rồi gõ :l baby từ trong GHCI).

ghci> doubleUs 4 9
26
ghci> doubleUs 2.3 34.2
73.0
ghci> doubleUs 28 88 + doubleMe 123
478

Bạn có thể gọi các hàm bạn vừa viết từ những hàm mà bạn đã viết từ trước; đây là điều không ngoài mong đợi. Theo cách này, ta có thể định nghĩa lại doubleUs như sau:

doubleUs x y = doubleMe x + doubleMe y

Đây là một ví dụ rất đơn giản về một dạng mẫu thông dụng mà bạn sẽ thấy xuyên suốt Haskell. Tạo ra các hàm cơ bản và hiển nhiên đúng rồi ghép chúng lại trong các hàm phức tạp hơn. Bằng cách này bạn cũng tránh được việc lặp lại. Điều gì sẽ xảy ra nếu nhà toán học nào đó chợt thấy số 2 đáng ra phải là số 3, và bạn phải thay đổi chương trình? Bạn có thể chỉ cần định nghĩa lại doubleMe thành x + x + x và vì doubleUs gọi doubleMe, nó ẽ tự động chạy đúng trong hoàn cảnh kì quặc này khi 2 là 3. Trong Haskell, hàm không nhât thiết phải có thứ tự cụ thể, vì vậy nếu bạn định nghĩa doubleMe trước rồi mới đến doubleUs, hay theo thứ tự ngược lại thì kết quả không khác gì nhau. Bây giờ ta sẽ chuẩn bị tạo một hàm để nhân một số với 2 nhưng chỉ khi số đó nhỏ hơn hoặc bằng 100, vì những số lớn hơn 100 thì bản thân chúng đã đủ lớn rồi!

doubleSmallNumber x = if x > 100
                        then x
                        else x*2

this is you Ngay ở đây ta biết đến câu lệnh if của Haskell. Có thể bạn đã quen với lệnh if từ các ngôn ngữ lập trình khác. Điểm khác biệt giữa lệnh if trong Haskell và if trong các ngôn ngữ mệnh lệnh là ở chỗ vế else là bắt buộc trong Haskell. Ở các ngôn ngữ mệnh lệnh bạn có thể bỏ qua luôn một số bước nếu điều kiện không được thỏa mãn nhưng trong Haskell, mỗi biểu thứ và hàm phải trả lại một thứ gì đó. Ta cũng đã có thể viết toàn bộ lệnh if trên một dòng nhưng tôi thấy viết kiểu như trên dễ đọc hơn. Một điểm khác về câu lệnh if trong Haskell là ở chỗ nó là một biểu thức. Biểu thức về cơ bản là một đoạn mã lệnh để trả lại một giá trị. 5 là một biểu thức vì nó trả lại 5, 4 + 8 là một biểu thức, x + y là một biểu thức vì nó trả lại tổng của xy. Vì vế else là bắt buộc, nên lệnh if luôn trả lại một thứ gì đó và do vậy nên nó là một biểu thức. Nếu ta muốn cộng thêm 1 vào số được tính ra bởi hàm trên thì có thể viết phần thân hàm như sau.

doubleSmallNumber' x = (if x > 100 then x else x*2) + 1

Nếu ta bỏ cặp ngoặc tròn thì ta chỉ cộng 1 vào trong trường hợp nếu x không lớn hơn 100. Lưu ý dấu ' ở cuối tên hàm. Dấu nháy này không có bất kì một ý nghĩa đặc biệt nào trong cú pháp của Haskell. Nó là kí tự hợp lệ được dùng để đặt tên hàm. Chúng tôi thường dùng ' để chỉ hoặc là một hàm viết the kiểu chặt chẽ (tức là không có tính “lười biếng”) hoặc một phiên bản sửa đổi của một hàm hoặc biến. Vì ' là kí tự hợp lệ trong hàm nên ta có thể tạo một hàm như sau.

conanO'Brien = "It's a-me, Conan O'Brien!"

Có hai điều cần lưu ý ở đây. Thứ nhât là trong tên hàm ta không viết hoa tên chữ Conan. Đó là vì hàm không thể bắt đầu bằng một chữ in hoa. Sau này ta sẽ biết tại sao. Thứ hai là hàm này không nhận bất kì tham số nào. Khi một hàm không nhận tham số, ta thường nói nó là một định nghĩa (hay một tên). Vì ta không thể thay đổi ý nghĩa của tên (và hàm) một khi ta đã định nghĩa chúng, nên conanO'Brien và chuỗi "It's a-me, Conan O'Brien!" có thể tráo đổi cho nhau được.

Giới thiệu về danh sách

BUY A DOG Rất giống với danh sách đi chợ ngoài đời, những danh sách trong Haskell là rât cần thiết. Đó là cấu trúc dữ liệu thường dùng nhât và nó có thể được sử dụng theo nhiều cách để mô phỏng và giải quyết một loạt bài toán khác nhau. Danh sách THẬT LÀ tuyệt vời. Trong mục này ta sẽ tìm hiểu kiến thức cơ bản về danh sách, chuỗi kí tự (cũng là một dạng danh sách) và danh sách liệt kê. Trong Haskell, danh sách là một cấu trúc dữ liệu đồng nhất. Nó lưu trữ nhiều phần tử có cùng kiểu. Điều này nghĩa là ta có thể có một danh sách số nguyên hay một danh sách kí tự nhưng không thể có một danh sách vừa có những số nguyên lại có một vài kí tự. Và đây, một danh sách!

Lưu ý: Ta có thể dùng từ khóa let để định nghĩa một tên ngay ở trong GHCI. Viết let a = 1 trong GHCI cũng tương đương với việc viết a = 1 trong một đoạn mã lệnh rồi tải nó.
ghci> let lostNumbers = [4,8,15,16,23,42]
ghci> lostNumbers
[4,8,15,16,23,42]

Như bạn có thể thấy, danh sách được kí hiệu bởi cặp ngoặc vuông và các giá trị trong danh sách được ngăn cách bởi các dấu phẩy. Nếu ta thử một danh sách như [1,2,'a',3,'b','c',4], Haskell sẽ phàn nàn rằng các kí tự (được biểu diễn trong cặp dấu nháy đơn) không phải là số. Lại nói về kí tự, chuỗi cũng chính là danh sách các kí tự. "hello" chỉ là một dạng cú pháp thuận lợi thay cho ['h','e','l','l','o']. Bởi vì chuỗi là danh sách nên ta cũng có thể dùng hàm đối với chúng; điều này quả là tiện. Một nhiệm vụ thường gặp là ghép hai danh sách vào cạnh nhau. Điều này được thực hiện bằng cách dùng toán tử ++.

ghci> [1,2,3,4] ++ [9,10,11,12]
[1,2,3,4,9,10,11,12]
ghci> "hello" ++ " " ++ "world"
"hello world"
ghci> ['w','o'] ++ ['o','t']
"woot"

Hãy cẩn thận khi bạn liên tiếp dùng toán tử ++ đối với các danh sách dài. Khi bạn xếp hai danh sách cạnh nhau (ngay cả khi bạn thêm một danh sách đơn phần tử vào một danh sách, chẳng hạn: [1,2,3] ++ [4]), thì ở bên trong, Haskell phải dò dọc theo toàn bộ danh sách vế trái của ++. Điều này không đáng ngại nếu ta chỉ xử lý những danh sách không quá lớn. Nhưng nếu đặt một thứ vào cuối một danh sách gồm 50 triệu phần tử thì sẽ mất một chút thời gian. Tuy nhiên, đặt một thứ gì đó vào đầu danh sách bằng toán tử : (còn được gọi là toán tử cons) thì sẽ hiệu quả tức thì.

ghci> 'A':" SMALL CAT"
"A SMALL CAT"
ghci> 5:[1,2,3,4,5]
[5,1,2,3,4,5]

Lưu ý cách mà : nhận một số và một danh sách số, hoặc một kí tự và một danh sách kí tự, trong khi ++ nhận vào hai danh sách. Ngay cả khi nếu bạn thêm một phần tử vào cuối danh sách với ++, bạn phải kẹp phần tử đó giữa cặp ngoặc vuông để biến nó thành danh sách. [1,2,3] thực ra là dạng cú pháp thuận lợi thay cho 1:2:3:[]. [] là một danh sách rỗng. Nếu ta đặt 3 vào trước, nó sẽ trở thành [3]. Nếu ta đặt 2 vào trước, danh sách mới sẽ là [2,3], và cứ như vậy.

Lưu ý: [], [[]][[],[],[]] là các thứ khác nhau. Cái đầu là một danh sách rỗng, cái thứ hai là mọt danh sách bao gồm một danh sách rỗng, còn cái thư bá là một danh sách bao gồm ba danh sách rỗng.

Nếu bạn muốn lấy một phần tử với chỉ số nhất định khỏi một danh sách, hãy dùng !!. Các chỉ số bắt đầu từ 0.

ghci> "Steve Buscemi" !! 6
'B'
ghci> [9.4,33.2,96.2,11.2,23.25] !! 1
33.2

Nhưng nếu bạn cố gắng lấy phần tử thứ sáu khỏi một danh sách chỉ có bốn phần tử, bạn sẽ nhận được lỗi, vì vậy phải rất cẩn thận! Danh sách cũng có thể chứa danh sách. Chúng có thể chứa các danh sách mà bản thân danh sách này lại chứa danh sách bên trong …

ghci> let b = [[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]
ghci> b
[[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]
ghci> b ++ [[1,1,1,1]]
[[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3],[1,1,1,1]]
ghci> [6,6,6]:b
[[6,6,6],[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]
ghci> b !! 2
[1,2,2,3,4]

Các danh sách bên trong một danh sách có thể dài ngắn khác nhau nhưng chúng không thể khác kiểu nhau. Cũng như bạn không thể có danh sách chứa đồng thời mấy kí tự và mấ con số, bạn không thể có một danh sách chưa một vài danh sách kí tự và một vài danh sách số. Danh sách cũng có thể được so sánh với nhau nếu các thứ chứa trong đó so sánh được. Khi dùng <, <=, >>= để so sánh các danh sách, việc so sánh được thực hiện theo thứ tự từ vựng. Trước hết, các phần tử đầu được so sánh với nhau. Nếu chúng bằng nhau thì các phần tử thứ hai được so sánh, và cứ như vậy.

ghci> [3,2,1] > [2,1,0]
True
ghci> [3,2,1] > [2,10,100]
True
ghci> [3,4,2] > [3,4]
True
ghci> [3,4,2] > [2,4]
True
ghci> [3,4,2] == [3,4,2]
True

Điều gì ta còn có thể làm với danh sách nữa? Sau đây là một số hàm cơ bản thực hiện với danh sách. head nhận vào một danh sách rồi trả lại phần tử đầu của nó. Phần tử đầu của danh sách chính là phần tử thứ nhất.

ghci> head [5,4,3,2,1]
5

tail nhận vào một danh sách rồi trả lại đuôi của nó. Nói cách khác, nó chặt bỏ đầu của danh sách đi.

ghci> tail [5,4,3,2,1]
[4,3,2,1]

last nhận vào một danh sách rồi trả lại phần tử cuối cùng của nó.

ghci> last [5,4,3,2,1]
1

init nhận vào một danh sách rồi trả lại tất cả mọi thứ trừ phần tử cuối cùng.

ghci> init [5,4,3,2,1]
[5,4,3,2]

Nếu ta hình dung danh sách như một con quái vật, thì các khá niệm trên sẽ như sau. list monster Nhưng điều gì sẽ xảy ra nếu ta thử lấy đầu của một danh sách rỗng?

ghci> head []
*** Exception: Prelude.head: empty list

Ồi giời, kết quả tệ không ngờ được! Không có quái vật, sẽ không có cái đầu nào. Khi dùng head, tail, lastinit, hãy cẩn thận tránh dùng nó với các danh sách rỗng. Lỗi kiểu này không thể bị bắt lúc biên dịch được, cho nên điều hay là luôn phải phòng ngừa cho việc tình cờ bảo Haskell đưa cho bạn một phần tử nào đó từ một danh sách rỗng. length nhận vào một danh sách rồi trả lại độ dài của nó.

ghci> length [5,4,3,2,1]
5

null kiểm tra xem danh sách có rỗng không. Nếu có, nó sẽ trả lại True, còn nếu không nó sẽ trả lại False. Hãy dùng hàm này thay cho việc viết xs == [] (nếu bạn có một danh sách tên là xs)

ghci> null [1,2,3]
False
ghci> null []
True

reverse đảo ngược một danh sách.

ghci> reverse [5,4,3,2,1]
[1,2,3,4,5]

take nhận vào một con số và một danh sách. Nó lấy ra từng ấy số phần tử kể từ đầu danh sách. Xem này.

ghci> take 3 [5,4,3,2,1]
[5,4,3]
ghci> take 1 [3,9,3]
[3]
ghci> take 5 [1,2]
[1,2]
ghci> take 0 [6,6,6]
[]

Lưu ý rằng nếu ta thử lấy nhiều phần tử hơn số vốn có trong danh sách, nó sẽ trả lại toàn bộ danh sách. Nếu ta thử lấy 0 phần tử, ta sẽ nhận được một danh sách rỗng. drop làm việc theo cách tương tự, nhưng lại bỏ đi từng ấy số phần tử kể từ đầu danh sách.

ghci> drop 3 [8,4,2,1,5,6]
[1,5,6]
ghci> drop 0 [1,2,3,4]
[1,2,3,4]
ghci> drop 100 [1,2,3,4]
[]

maximum nhận vào một danh sách các thứ có thể sắp xếp được theo cách nào đó, rồi trả lại phần tử lớn nhất. minimum trả lại phần tử nhỏ nhất.

ghci> minimum [8,4,2,1,5,6]
1
ghci> maximum [1,9,2,3,4]
9

sum nhận vào một danh sách số rồi trả lại tổng của chúng. product nhận vào một danh sách rồi trả lại tích của chúng.

ghci> sum [5,2,1,6,3,2,5,7]
31
ghci> product [6,2,1,2]
24
ghci> product [1,2,5,6,7,9,2,0]
0

elem nhận vào một thứ và một danh sách các thứ rồi báo cho chúng ta biết liệu thứ đó có là phần tử thuộc danh sách không. Nó thường được gọi là hàm trung tố vì nếu đọc theo kiểu đó sẽ dễ hơn.

ghci> 4 `elem` [3,4,5,6]
True
ghci> 10 `elem` [3,4,5,6]
False

Đó là một số hàm cơ bản hoạt động với danh sách. Ta sẽ xem thêm một số hàm với danh sách sau này

Texas ranges

draw Vậy ta sẽ tính thế nào nếu muốn có một danh sách gồm tất cả con số từ 1 đến 20? Chắc chắn là ta có thể gõ chúng cả vào nhưng hiển nhiên đây không phải giải pháp của người lịch sự muốn ngôn ngữ lập trình của mình phải xuất sắc. Thay vào đó, chúng ta có thể dùng dãy. Dãy là một cách tạo danh sách chứa loạt các phần tử mà ta đếm được. Số có thể đếm được. Một, hai, ba, bốn, v.v. Kí tự cũng có thể đếm được. Bảng chữ cái chính là một cách đếm kí tự từ A đến Z. Tên thì không thể đếm được. Cái gì đứng kế tiếp “John”? Tôi không biết. Để tạo một dãy chứa các số tự nhiên từ 1 đến 20, bạn phải viết [1..20]. Điều này tương đương với viết [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] và không có sự khác biệt nào giữa hai cách này ngoại trừ việc viết cả dãy liệt kê đầy đủ là việc làm ngốc nghếch.

ghci> [1..20]
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
ghci> ['a'..'z']
"abcdefghijklmnopqrstuvwxyz"
ghci> ['K'..'Z']
"KLMNOPQRSTUVWXYZ"

Dãy thật tuyệt vì bạn cũng có thể chỉ định một bước nhảy. Ta sẽ làm gì nếu muốn có tất cả những số chẵn giữa 1 và 20? Hoặc cứ cách ba số thì lấy một số trong khoảng từ 1 đến 20?

ghci> [2,4..20]
[2,4,6,8,10,12,14,16,18,20]
ghci> [3,6..20]
[3,6,9,12,15,18]

Chỉ cần phân tách hai phần tử đầu tiên bằng một dấu phẩy rồi chỉ định xem giới hạn trên bằng bao nhiêu. Tuy rằng cách này khá thông minh, nhưng các dãy có bước nhảy không thông minh được như nhiều người mong đợi. Bạn không thể viết [1,2,4,8,16..100] rồi trông đợi sẽ thu được tất cả số lũy thừa của 2. Trước hết là vì bạn chỉ có thể chỉ định một bước nhảy duy nhất. Thứ hai là vì một số dãy không có tính chất số học sẽ rất mơ hồ nếu ta chỉ cung cấp một số ít phần tử đầu tiên. Để tạo một danh sách với tất cả những số từ 20 về 1, bạn không thể chỉ viết [20..1], mà phải viết [20,19..1]. Hãy cẩn thận khi dùng số có dấu phẩy động trong dãy! Vì chúng không chính xác hoàn toàn (theo định nghĩa) nên việc dùng chúng trong dãy có thể cho kết quả khá kì quái.

ghci> [0.1, 0.3 .. 1]
[0.1,0.3,0.5,0.7,0.8999999999999999,1.0999999999999999]

Lời khuyên của tôi là không nên dùng chúng trong dãy. Bạn cũng có thể dùng dãy để tạo ra danh sách vô hạn bằng việc không chỉ định giới hạn trên. Sau này ta sẽ tìm hiểu sâu hơn về dãy vô hạn. Còn lúc này, hãy kiểm tra xem bạn làm thế nào để lấy được 24 bội số đầu tiên của 13. Tất nhiên là bạn có thể viết [13,26..24*13]. Như còn một cách khác hay hơn: take 24 [13,26..]. Vì Haskell lười biếng, nên nó sẽ không thử lượng giá lập tức cả dãy vô hạn này vì việc đó sẽ không bao giờ kết thúc. Haskell sẽ đợi xem bạn muốn lấy thứ gì từ dãy vô hạn đó. Và bây giờ khi biết được rằng bạn chỉ muốn 24 phần tử đầu tiên, nó ngoan ngoãn nghe lời. Một số ít các hàm tạo ra danh sách vô hạn: cycle nhận vào một danh sách rồi lặp quay vòng nó để tạo ra một danh sách vô hạn. Nếu bạn chỉ thử hiển thị kết quả, nó sẽ chạy mãi mãi vì vậy bạn phải cắt lát nó ở một vị trí nào đó.

ghci> take 10 (cycle [1,2,3])
[1,2,3,1,2,3,1,2,3,1]
ghci> take 12 (cycle "LOL ")
"LOL LOL LOL "

repeat nhận vào một phần tử rồi tạo ra một danh sách vô hạn dùng chính phần tử đó. Đây là kiểu quay vòng chỉ dùng đúng một phần tử.

ghci> take 10 (repeat 5)
[5,5,5,5,5,5,5,5,5,5]

Mặc dù sẽ đơn giản hơn nếu ta chỉ dùng hàm replicate nếu muốn một số lần nào đó lặp lại một phần tử trong danh sách. replicate 3 10 trả lại [10,10,10].

Tôi là dạng gộp danh sách

frog Nếu bạn đã từng theo học môn toán, có lẽ bạn đã gặp phải dạng gộp tập hợp. Cách viết này thường được dùng để lập ra các tập hợp cụ thể từ tập hợp tổng quát. Một dạng gộp cơ bản của tập hợp chứa 10 số tự nhiên chẵn đầu tiên là set notation. Phần đứng trước dấu sổ được gọi là hàm đầu ra, x là biến, N là tập hợp đầu vào và x <= 10 là vị ngữ. Điều đó có nghĩa là tập hợp sẽ chứa các số nhân đôi 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 gấp đôi 10 số tự nhiên đầu tiên nhưng 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 một dạng gộp danh sách cho mục đích này. Dạng gộp danh sách rất giống với dạng gộp tập hợp. Tạm thời ta sẽ thống nhất dùng ví dụ 10 số chẵn đầu tiên. Dạng gộp danh sách 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ỉ việc nhân đôi lên. Sau đây là dạng gộp đó khi được thực thi.

ghci> [x*2 | x <- [1..10]]

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 dạng gộp đó. 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, giả sử ta chỉ cần các phần tử nào khi gấp đôi lên thì lớn hơn hoặc bằng 12.

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

Hay, nó đã chạy đúng. Thế còn nếu ta muốn tất cả những số từ 50 đến 100 sao cho số 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 danh sách còn được gọi là lọc. Ta lấy một danh sách số rồi lọc chúng bằng vị ngữ. Bây giờ hãy xét thêm một ví dụ khác. Chẳng hạn, giả sử ta muốn một dạng rút gọn để thay từng 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 bỏ nó khỏi danh sách. Để cho tiện, ta sẽ đặt dạng rút gọn này vào trong một hàm để sau này tiện dùng lại.

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

Phần sau cùng của dạng rút gọn 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 danh sách 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 dạng gộp danh sách (một phần tử phải thỏa mãn tất cả vị ngữ mới được đứng trong danh sách kết quả), mà còn có thể rút từ nhiều danh sách khác nhau. Khi rút từ nhiều danh sách, dạng gộp sẽ tạo ra tất cả những tổ hợp trong các danh sách đã cho rồi nối chúng lại bằng hàm đầu ra mà ta chỉ định. Nếu dạng gộp trong đó rút từ hai danh sách có chiều dài 4 sẽ cho ra một danh sách có chiều dài 16, nếu như ta không lọc gì. Giả sử ta có hai danh sách, [2,5,10][8,10,11]. Nếu muốn nhận được tích của tất cả những tổ hợp có thể giữa các số trong danh sách 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 danh sách mới là 9. Thế còn nếu ta muốn tất cả tích có thể nhưng 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 một dạng gộp danh sách trong đó kết hợp một danh sách các tính từ và một danh sách 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"]

Tôi biết rồi! 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à đằng nào chúng ta cũng không quan tâm về danh sách cho nên thay vì đặt một tên biến mà sẽ không bao giờ dùng đến, ta chỉ viết _. Hàm này thay thế mọi phần tử của danh sách với 1 rồi cộng chúng lại. Điều đó có nghĩa là kết quả tổng số sẽ là chiều dài của danh sách. Tôi xin phép nhắc bạn nhẹ nhàng: vì chuỗi cũng là danh sách nên ta có thể dùng dạng gộp danh sách để xử lý và sản sinh ra chuỗi. Sau đây là một hàm nhận vào một chuỗi rồi bỏ đi mọi thứ trong chuỗi đó, chỉ trừ các chữ in.

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ự sẽ được bao gồm trong danh sách mới chỉ khi nó là một phần tử của danh sách ['A'..'Z']. Dạng gộp danh sách có thể được lồng ghép với nhau nếu bạn làm việc với danh sách trong đó chứa danh sách. Một danh sách chứa nhiều danh sách số. Hãy bỏ tất cả các danh sách mà không san phẳng danh sách [nghĩa là ta vẫn phải giữ cấu trúc danh sách 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]]

Bạn có thể viết dạng gộp danh sách trên nhiều dòng. Vì vậy, nếu bạn ở ngoài GHCI, tốt hơn hết là cắt dòng lệnh chứa dạng gộp danh sách dài thành nhiều dòng, đặc biệt là khi chúng được lồng ghép.

Bộ

tuples Ở mức độ nào đó, bộ cũng giống như danh sách — đó là một cách lưu giữ nhiều giá trị riêng rẽ vào một giá trị. Tuy nhiên có một vài điểm khác biệt cơ bản. Danh sách số là một danh sách chỉ chứa con số. Đó là kiểu của danh sách này và nó không ảnh hưởng gì nếu chỉ chứa có một số hay là chứa vô hạn các con số. Còn bộ được dùng khi bạn đã biết chắc chắn có bao nhiêu giá trị cần được kết hợp lại với nhau và kiểu của bộ phụ thuộc vào số lượng thành phần trong nó và kiểu của từng thành phần. Bộ được kí hiệu bằng cặp ngoặc tròn và các thành phần được ngăn cách bởi dấu phẩy. Một điểm khác biệt cơ bản khác là bộ không nhất thiết phải đồng nhất. Khác với danh sách, bộ có thể là sự kết hợp nhiều kiểu dữ liệu khác nhau. Hãy hình dung cách ta biểu thị một véc-tơ hai chiều trong Haskell. Một cách làm là dùng danh sách. Có vẻ như cách này được. Vậy sẽ ra sao nếu ta muốn đưa nhiều véc-tơ vào trong một danh sách để biểu diễn các điểm trong một hình phẳng (hai chiều)? Ta có thể viết như [[1,2],[8,11],[4,5]]. Vấn đề với cách này là ta cũng viết được, chẳng hạn [[1,2],[8,11,5],[4,5]]; điều này Haskell không cấm vì nó vẫn là danh sách số nhưng đã mất ý nghĩa toán học. Còn một bộ với kích thước bằng 2 (mà ta cũng gọi là một cặp) thì có kiểu riêng của nó; nghĩa là một danh sách không thể chứa một vài cặp trong đó rồi lại cũng chứa một bộ ba số. Vì vậy ta hãy dùng bộ. Thay vì bao quanh véc-tơ bởi cặp ngoặc vuông, ta dùng cặp ngoặc tròn: [(1,2),(8,11),(4,5)]. Vậy sẽ ra sao nếu ta thử lập một hình kiểu như [(1,2),(8,11,5),(4,5)]? Ồ, ta sẽ gặp lỗi này:

Couldn't match expected type `(t, t1)'
against inferred type `(t2, t3, t4)'
In the expression: (8, 11, 5)
In the expression: [(1, 2), (8, 11, 5), (4, 5)]
In the definition of `it': it = [(1, 2), (8, 11, 5), (4, 5)]

Nó báo cho ta biết rằng ta đã dùng một cặp và một bộ ba trong cùng danh sách, điều này không cho phép xảy ra. Bạn cũng không thể tạo danh sách kiểu [(1,2),("One",2)] vì phàn tử đầu trong danh sách là một cặp số còn phần tử thứ hai là một cặp gồm một chuỗi và một số. Bộ cũng có thể được dùng để biểu diễn nhiều loại dữ liệu khác nhau. Chẳng hạn, nếu ta muốn biểu diễn tên và tuổi của một người, trong Haskell ta có thể dùng bộ ba: ("Christopher", "Walken", 55). Như ở ví dụ này, ta thấy được bộ có thể còn chứa danh sách. Dùng bộ khi bạn đã biết trước có bao nhiêu phần tử mà một đối tượng dữ liệu cần chứa. So với danh sách thì bộ cứng nhắc hơn vì mỗi kích thước của bộ là kiểu riêng của nó, vì vậy bạn không thể viết một hàm tổng quát để bổ sung một phần tử vào một bộ — bạn phải viết một hàm để bổ sung vào một cặp, một hàm khác để bổ sung vào một bộ ba, một hàm khác nữa để bổ sung vào bộ tứ, v.v. Trong khi có danh sách chứa một phần tử, lại không có bộ nào như vậy. Thực ra khái niệm đó vô nghĩa. Bộ một phần tử chính là giá trị phần tử đó và như vậy đối với ta sẽ không có ích gì. Cũng như danh sách, hai bộ có thể so sánh với nhau được nếu các phần tử của chúng có thể so sánh được. Bạn chỉ không thể so sánh được hai bộ khác kích thước, nhưng hai danh sách khác kích thước lại so sánh được. Có hai hàm có ích khi thao tác với cặp: fst nhận vào một cặp và trả về phần tử thứ nhất của nó.

ghci> fst (8,11)
8
ghci> fst ("Wow", False)
"Wow"

snd nhận vào một cặp và trả về phần tử thứ nhất của nó. Thật ngạc nhiên!

ghci> snd (8,11)
11
ghci> snd ("Wow", False)
False
Lưu ý: các hàm này chỉ có tác dụng đối với cặp. Ta không dùng được chúng với các bộ ba, bộ tứ, bộ năm, v.v. Sau này ta sẽ đề cập đến cách khác để lấy thông tin từ bộ.

Một hàm rất tiện để tạo ra một danh sách các cặp: zip. Nó nhận vào hai danh sách rồi nhập chúng lại [thử liên tưởng đến hình ảnh phéc-mơ-tuya, cũng vì vậy mà tên hàm này là zip] bằng cách ghép các phần tử có cùng só thứ tự trong hai dành sách thành các đôi. Đây là một hàm thực sự đơn giản nhưng có vô vàn ứng dụng. Nó đặc biệt có ích khi bạn muốn kết hợp hai danh sách theo cách nào đó, hoặc đồng thời duyệt theo hai danh sách. Sau đây là ví dụ sử dụng.

ghci> zip [1,2,3,4,5] [5,5,5,5,5]
[(1,5),(2,5),(3,5),(4,5),(5,5)]
ghci> zip [1 .. 5] ["one", "two", "three", "four", "five"]
[(1,"one"),(2,"two"),(3,"three"),(4,"four"),(5,"five")]

Hàm này cặp đôi các phần tử lại và tạo ra một danh sách mới. Phần tử thứ nhất đi với phần tử thứ nhất, phần tử thứ hai đi với phần tử thứ hai, v.v. Lưu ý rằng vì cặp đôi có thể có kiểu khác nhau, nên zip có thể nhận vào hai danh sách có kiểu khác nhau rồi cặp lại. Điều gì sẽ xảy ra nếu chiều dài của các danh sách này không bằng nhau?

ghci> zip [5,3,2,6,2,7,2,5,4,6,6] ["im","a","turtle"]
[(5,"im"),(3,"a"),(2,"turtle")]

Danh sách dài hơn sẽ được cắt bớt đi để dài bằng danh sách ngắn. Vì Haskell lười biếng, nên ta có thể cặp danh sách hữu hạn với danh sách vô hạn:

ghci> zip [1..] ["apple", "orange", "cherry", "mango"]
[(1,"apple"),(2,"orange"),(3,"cherry"),(4,"mango")]

look at meee Sau đây là một bài toán kết hợp bộ với dạng gộp danh sách: Tam giác vuông nào có các cạnh đều là số nguyên và các cạnh đều dài bằng hoặc ngắn hơn 10 đồng thời có chu vi bằng 24? Trước hết, ta hãy thử phát sinh tất cả tam giác với cạnh dài bằng hoặc ngắn hơn 10:

ghci> let triangles = [ (a,b,c) | c <- [1..10], b <- [1..10], a <- [1..10] ]

Ta chỉ việc rút ba số nguyên từ ba danh sách và hàm kết quả làm nhiệm vụ kết hợp chúng thành một bộ ba. Nếu bạn lượng giá hàm này bằng cách gõ vào triangles trong GHCI, bạn sẽ nhận được một danh sách các tam giác với ba cạnh đều không dài quá 10. Tiếp theo, ta sẽ thêm vào một điều kiện để buộc chúng là tam giác vuông. Ta cũng sẽ thay đổi hàm này bằng cách tính đến cạnh b không lớn hơn cạnh huyền còn cạnh a thì không lớn hơn cạnh b.

ghci> let rightTriangles = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2]

Ta gần xong việc rồi. Bây giờ, chỉ cần sửa lại hàm này bằng cách nói rằng ta muốn những tam giác nào có chu vi bằng 24.

ghci> let rightTriangles' = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2, a+b+c == 24]
ghci> rightTriangles'
[(6,8,10)]

Và đó là đáp số! Trên đây là một dạng thông dụng của lập trình hàm. Bạn đem một tập hợp các giá trị (nghiệm) khởi đầu rồi áp dụng các phép chuyển đổi cho những nghiệm đó rồi lọc đi đến khi nhận được nghiệm đúng.

4 phản hồi

Filed under Haskell

4 responses to “Chương 2: Xuất phát

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

  2. Pingback: Chương 11: Functor, Functor áp dụng và Monoid | Blog của Chiến

  3. Phần list comprehension code bị thiếu có vẻ thiếu ạ?

    Đáng lý là
    “`
    [x*2 | x <- [1..10]]
    “`

    “`
    [x*2 | x = 12]
    “`

Gửi phản hồ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