Haskell #1.2 Tool

 

Cần gì để bắt tay vào thực hành?

Giống như những ngôn ngữ lập trình khác, để thực hành chúng ta sẽ cần một editor (như VScode, …) và bản phân phối Haskell (Haskell Stack). Trong bản phân phối này sẽ chứa những thứ cơ bản như compiler (GHC), package manager (Cabal), Haskell language server, …

GHC có thể nhận một file Haskell (.hs) để biên dịch, tuy vậy nó cũng có chế độ console (GHCi) cho phép code trực tiếp trên terminal như Python. Bạn có thể gọi hàm từ chương trình đã biên dịch và kết quả sẽ được hiển thị tức thì. Để phục vụ mục đích học tập thì cách này dễ và nhanh hơn nhiều so với phải biên dịch mỗi khi bạn sửa đổi rồi chạy lại. Chế độ console được khởi động bằng cách gõ vào stack ghci vào terminal. Nếu bạn đã định nghĩa một số hàm trong file có tên như là myfunctions.hs, thì thao tác biên dịch các hàm này là gõ :l myfunctions, sau đó bạn có thể gọi chúng, miễn là myfunctions.hs được đặt ở cùng thư mục nơi mà ghci được khởi động. Nếu sửa code, thì chỉ cần gõ lại :l myfunctions hoặc :r.

Chuẩn bị

Điều đầu tiên là chạy interactive mode của ghc và gọi một số hàm để chill cùng Haskell.

Configuring GHCi with the following packages: 
GHCi, version 8.8.3: https://www.haskell.org/ghc/  :? for help
Loaded GHCi configuration from ...
Prelude> :l

Dấu nhắc ở đây có tên là Prelude> nhưng vì nó dài, nên ta sẽ dùng kí hiệu ghci>. Nếu bạn muốn đặt lại tên cho dấu nhắc, chỉ cần gõ vào :set prompt "ghci> ". Sau đây là một số phép toán đơn giản.

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

Giống như một số ngôn ngữ khác, ta có thể thực hiện phép tính trên một dòng và thứ tự tính toán giống như thông thườ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

Không có gì khác biệt so với các ngôn ngữ khác. Tuy nhiên, có một lỗi dễ mắc phải ở đây là số âm. Nếu chúng ta muốn tính toán với số âm thì phải luôn kẹp giữa cặp ngoặc đơn. Ví dụ, 5 * -3 thì ghci sẽ rống lên (5 * (-3) sẽ ổn hơn). Phép toán Boolean cũng tương đối rạch ròi.

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 bằng.

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

Nếu 5 + “abc” hay 5 == True thì sẽ ngay lập tức có thông báo lỗi:

* No instance for (Num [Char]) arising from a use of `+'
* In the expression: 5 + "abc"
In an equation for `it': it = 5 + "abc"

GHCi báo là “abc” không phải là số, vì vậy nó không thể cộng “abc”với 5. Nếu ta thử gõ True == 5, GHCI sẽ bảo kiểu của chúng không khớp nhau. Trong khi + chỉ làm việc được với số, thì == chỉ nhận hai thứ nào đó so sánh được với nhau (bạn không thể so sánh cam với táo). Lưu ý, bạn có thể gõ vào 5 + 4.0 vì 5 có thể là số nguyên lẫn số thực. 4.0 thì không thể làm số nguyên, vì vậy 5 sẽ được biến đổi để thích nghi.

Từ đầu đến giờ chúng ta 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. Chúng ta gọi hàm này bằng cách kẹp nó giữa hai số. Đây được gọi là kiểu hàm trung tố. Sau này, chúng ta sẽ gặp các kiểu hàm khác là tiền tố và hậu tố. Tiền tố là hàm mà khi gọi chúng ta sẽ để tên hàm phía trước tham số khi gọi hàm, ngược lại với hậu tố.

Vì hàm tiền tố rất phổ biến (trong toán lẫn các ngôn ngữ lập trình khác) nên chúng ta sẽ mặc định các hàm luôn là kiểu tiền tố. Thường thì các tham số của ham được để trong cặp ngoặc (tham số được phân biệt bởi dấu phẩy). Trong Haskell, cặp ngoặc lẫn dấu phẩy được bỏ vì nhiều lý do mà chúng ta sẽ tìm hiểu sau. Để 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àm succ nhận vào thứ gì đó mà có thể trả lại thứ đứng kế tiếp nó. Như 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 nhiều tham số cũng đơn giản.

Hàm min và max nhận vào những thứ có thể sắp xếp được. min trả lại thứ nhỏ nhất còn max trả lại thứ lớn nhất hơn.

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

Phép 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. Có nghĩa là hai câu lệnh sau tương đương.

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

Tuy nhiên, nếu 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ì nó sẽ lấy số liền sau của 9 (10) rồi đ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à nó đã rõ ràng hơn nhiều. Nhiều người trước đây quen với kiểu gọi như foo()bar(1) hay baz(3, "haha"). Như tôi đã nói, dấu cách được dùng cho việc sử dụng hàm trong Haskell. Vì vậy những hàm đó nếu trong Haskell sẽ được viết là foobar 1 và baz 3 "haha".Nếu thấy một dòng code nào đó như bar (bar 3), thì nó có nghĩa là đầu tiên ta gọi hàm bar với tham số 3 để nhận được một thứ gì đó rồi mới gọi bar một lần nữa với số vừa thu được. Nếu trong C, code sẽ là bar(bar(3)). .

Viết hàm đầu tiên

Ở 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ờ, hãy thử tạo hàm! Hãy mở editor rồi gõ vào hàm sau để nhận vào một 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ư tên gọi. Sau tên hàm là tham số được tách rời bởi các dấu cách, sau đó là dấu = và ta sẽ định nghĩa hàm thực hiện điều gì. Lưu file này lại với tên Lab1-Intro.hs, sau đó lưu file rồi chạy ghci. Một khi đã ở trong GHCI, hãy gõ vào :l Lab1-Intro.hs. Bây giờ khi code được compile, ta có thể sử dụng hàm vừa định nghĩa.

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

Hãy thử hàm phức tạp hơn là tính tổng double của hai số x và y.

doubleUs x y = x*2 + x*2
ghci> doubleUs 4 9
26
ghci> doubleUs 2.3 34.2
73.0
ghci> doubleUs 28 88 + doubleMe 123
478

Giống như thông thường, chúng ta có thể tái sử dụng các hàm. 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 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 phải đổi số 2 thành số 3. Bạn chỉ cần định nghĩa lại doubleMe thành x + x + x và vì doubleUs gọi doubleMe, nó sẽ tự động chạy đúng. 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ị 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

Ngay ở đây ta biết đến lệnh if của Haskell. Điểm khác biệt giữa lệnh if trong Haskell và if trong các ngôn ngữ khác là vế else luôn bắt buộc có. Trong Haskell, với mỗi biểu thức 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 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 code để 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 x và y. Vì vế else là bắt buộc, nên lệnh if luôn trả lại một thứ gì đó và do đó nó là một biểu thức. Nếu ta muốn cộng thêm 1 vào kết quả đượ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 thêm 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 ‘ 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. Tôi thường dùng ‘ để chỉ hoặc là một hàm viết theo kiểu chặt (tức là không có tính lazy) hoặc một phiên bản thứ hai của 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 chữ c vì hàm không thể bắt đầu bằng một chữ in hoa (ta sẽ biết lý do sau). Thứ hai là hàm này không nhận tham số. Khi một hàm không nhận tham số, ta thường nói nó là định nghĩa (definition) hay tên (name). Vì ta không thể thay đổi ý nghĩa của tên sau khi ta đã định nghĩa chúng.