Chương 3: Kiểu và lớp chứa kiểu

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

Hãy tin vào kiểu

Trước đây ta đã đề cập rằng Haskell có một hệ thống kiểu tĩnh. Kiểu của mỗi biểu thức đều được biết ngay từ lúc biên dịch, vì vậy khiến cho mã lệnh được an toàn hơn. Nếu bạn viết một chương trình trong đó thử tính chia một kiểu boole cho một số nào đó, thì chương trình thậm chí sẽ không biên dịch được. Điều này có lợi vì ta nên bắt những lỗi như vậy lúc biên dịch thì hơn là để chương trình bị đổ vỡ. Mọi thứ trong Haskell đều có kiểu, vì vậy trình biên dịch có thể suy luận được rất nhiều từ chương trình bạn viết trước khi biên dịch nó.

moo

Khác với Java hay Pascal, Haskell có suy luận kiểu. Nếu ta viết một con số, ta không cần phải bảo Haskell đó là một số. Nó sẽ tự suy luận được, vì thế ta không phải viết rõ ra kiểu các hàm và biểu thức để đảm bảo mọi thứ hoạt động được. Chúng tôi đã trình bày một số điều cơ bản về Haskell chỉ với một cái nhìn thoáng qua về kiểu. Tuy vậy, việc hiểu được hệ thống kiểu là phần rất quan trọng trong quá trình học Haskell.

Một kiểu cũng như một nhãn “dán” cho tất cả các biểu thức. Nó cho chúng ta biết thể loại của biểu thức đó là gì.Biểu thức True có kiểu boole, "hello" là một chuỗi, v.v.

Bây giờ ta hãy dùng GHCI để kiểm tra kiểu của một số biểu thức. Ta sẽ làm điều đó bằng cách dùng lệnh :t tiếp ttheo là bất kì một biểu thức hợp lệ nào, để cho ta biết kiểu của nó. Hãy thử xem sao.

ghci> :t 'a'
'a' :: Char
ghci> :t True
True :: Bool
ghci> :t "HELLO!"
"HELLO!" :: [Char]
ghci> :t (True, 'a')
(True, 'a') :: (Bool, Char)
ghci> :t 4 == 5
4 == 5 :: Bool

bomb
Ở đây ta thấy rằng viết :t trước một biểu thức sẽ in ra biểu thức đó, tiếp theo là dấu :: rồi đến kiểu của nó. :: được đọc là “có kiểu”. Những kiểu tường minh luôn được viêt với tên có chữ cái đầu in hoa. 'a', như ta đoán được, có kiểu Char. Không khó gì để nhận ra nó là chữ viết tắt cho character (kí tự). True thuộc về kiểu Bool. Như thế cũng có nghĩa. Nhưng còn cái gì đây?. Kiểm tra kiểu của "HELLO!", ta nhận được [Char]. Cặp ngoặc vuông biểu thị cho một danh sách. Như vậy ta đọc nó là một danh sách các kí tự. Khác với danh sách, các bộ dài ngắn khác nhau thì có kiểu riêng. Vì vậy, biểu thức (True, 'a') có kiểu (Bool, Char), trong khi một biểu thức như ('a','b','c') sẽ có kiểu (Char, Char, Char). 4 == 5 luôn trả lại False, vì vậy kiểu của nó là Bool.

Các hàm cũng có kiểu. Khi viết ra các hàm, ta có thể lựa chọn có hoặc không khai báo cụ thể kiểu của hàm đó. Việc khai báo thường là quy tắc tốt trừ trường hợp bạn viết hàm rất ngắn. Từ nay trở đi, tất cả những hàm ta viết sẽ có khai báo kiểu. Bạn còn nhớ rằng dạng gộp danh sách trước đây ta viết để lọc một chuỗi, chỉ giữ lại những chữ cái in hoa chứ? Cùng với khai báo kiểu, nó sẽ trông như sau:

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

removeNonUppercase có kiểu [Char] -> [Char], nghĩa là nó ánh xạ từ một chuỗi đến một chuỗi. Đó là vì nó nhận vào một chuỗi và một tham số rồi trả lại kết quả là một chuỗi khác. Kiểu [Char] thì tương đồng với String vì vậy sẽ rõ hơn nếu ta viết removeNonUppercase :: String -> String. Ta không cần phải khai báo kiểu cho hàm này vì trình biên dịch có thể tự suy luận rằng nó là một hàm từ chuỗi đến chuỗi, tuy nhiên dù sao ta vẫn làm công việc này. Nhưng bằng cách nào ta có thể viết kiểu của một hàm nhận vào nhiều tham số? Sau đây là một hàm đơn giản nhận vào 3 số nguyên rồi cộng chúng lại:

addThree :: Int -> Int -> Int -> Int
addThree x y z = x + y + z

Các tham số được ngăn cách bởi dấu -> và không có sự phân biệt nào riêng giữa tham số và kiểu được trả lại. Ở đây, kiểu được trả lại là thứ cuối cùng trong lời khai báo còn các tham số là ba thứ đầu. Sau này ta sẽ thấy được tại sao tất cả chúng đều được phân tách bởi -> thay vì có một sự phân biệt rõ ràng hơn giữa kiểu trả về và các tham số, chẳng hạn Int, Int, Int -> Int hay một cách nào khác.

Nếu bạn muốn khai báo kiểu cho hàm vừa viết nhưng không chắc chắn khai báo thế nào, bạn luôn có thể chỉ viết hàm mà bỏ qua khai báo, sau đó kiểm tra nó với :t. Hàm cũng là biểu thức, vì vậy :t làm việc được với chúng mà không có vấn đề gì xảy ra.

Sau đây là cái nhìn khái quát về những kiểu thường gặp.

Int là viết tắt của integer, số nguyên. 7 có thể là một Int nhưng 7.2 thì không. Int bị chặn, có nghĩa là nó có một giá trị nhỏ nhất và một giá trị lớn nhất. Thường thì ở các máy tính 32 bit, số lớn nhất mà Int có thể nhận là 2147483647 và số nhỏ nhất là -2147483648.

Integer à … cũng là số nguyên. Sự khác biệt chủ yếu giữa nó và Int là ở chỗ Integer không bị chặn nên nó có thể dùng để biểu diễn số thật lớn. Ý tôi là rất lớn. Dù vậy thì dùng Int có thể hiệu quả [tiết kiệm tài nguyên máy tính] hơn.

factorial :: Integer -> Integer
factorial n = product [1..n]
ghci> factorial 50
30414093201713378043612608166064768844377641568960512000000000000

Float là số có dấu phẩy động, với độ chính xác đơn.

circumference :: Float -> Float
circumference r = 2 * pi * r
ghci> circumference 4.0
25.132742

Double là số có dấu phẩy động với độ chính xác kép!

circumference' :: Double -> Double
circumference' r = 2 * pi * r
ghci> circumference' 4.0
25.132741228718345

Bool là kiểu boole. Nó chỉ có hai giá trị: TrueFalse.

Char biểu diễn một kí tự. Nó được kí hiệu bởi cặp dấu nháy đơn. Một danh sách các kí tự hợp thành một chuỗi.

Bộ cũng là kiểu nhưng nó lại tùy thuộc và độ dài cũng như từng kiểu của thành phần, cho nên về lý thuyết sẽ có vô số kiểu bộ khác nhau, và ta không thể đề cập đến trong quyển hướng dẫn này. Lưu ý rằng một bộ rỗng () cũng là một kiểu nhưng chỉ có một giá trị duy nhất: ()

Biến kiểu

Theo bạn thì kiểu của hàm head là gì? VÌ head nhận vào một danh sách phần tử có kiểu bất kì rồi trả lại phần tử thứ nhất, cho nên nó sẽ có kiểu gì đây? Ta hãy cùng kiểm tra!

ghci> :t head
head :: [a] -> a

box
Hmmm! Cái a này là gì đây? Có phải nó là một kiểu không? Hãy nhớ rằng trước đây chúng tôi chỉ nói rằng tên kiểu được viết in hoa chữ cái đầu, vì vậy nó không hẳn là một kiểu. Không bắt đầu bằng chữ cái in hoa, thực ra nó là một biến kiểu. Điều này có nghĩa là a có thể là kiểu bất kì. Nó cũng giống như những generic (“đại diện chung”) trong các ngôn ngữ lập trình khác; chỉ trong Haskell nó mạnh hơn rất nhiều vì cho phép ta dễ dàng viết những hàm rất tổng quát nếu không động đến những hành vi đặc biệt của các kiểu có trong hàm. Những hàm có chứa biến kiểu được gọi là hàm đa hình. Lời khai báo kiểu của head nói rằng nó nhận vào một danh sách có kiểu bất kì rồi trả lại một phần tử có kiểu đó.

Mặc dù biến kiểu có thể được đặt tên dài hơn một kí tự, nhưng ta thường đặt tên cho chúng là a, b, c, d …

Bạn còn nhớ fst? Nó trả lại thành phần đầu tiên của một cặp. Ta hãy cùng kiểm tra kiểu này.

ghci> :t fst
fst :: (a, b) -> a

Có thể thấy rằng fst nhận vào một bộ bao gồm hai kiểu rồi trả lại một phần tử có cùng kiểu với thành phần thứ nhất trong cặp. Đó là lý do tại sao ta có thể dùng fst cho một cặp chứa hai kiểu bất kì. Lưu ý răng dù ab là hai biến kiểu khác nhau, nhưng như vậy không nhất thiết là chúng phải khác kiểu nhau. Chỉ có thể khẳng định rẳng thành phần thứ nhất và giá trị trả lại có cùng kiểu với nhau.

Lớp chứa kiểu 101

class

Lớp chứa kiểu là một dạng giao diện để định nghĩa một hành vi nào đó. Nếu một kiểu thuộc về một lớp kiểu nhất định, điều đó có nghĩa là nó trợ giúp và thực hiện hành vi của mình theo sự chỉ định của lớp kiểu. Nhiều người từ trường phái hướng đối tượng (OOP) bị các lớp chứa kiểu gây nhầm lẫn vì cứ nghĩ rằng chúng giống như các lớp trong ngôn ngữ hướng đối tượng. Không phải như vậy. Bạn nên hình dung chúng như kiểu giao diện Java (interface), chỉ có điều là tốt hơn.

Dấu ấn kiểu (type signature) của hàm == là gì?

ghci> :t (==)
(==) :: (Eq a) => a -> a -> Bool
Lưu ý: toán tử đẳng thức, == là một hàm. Các toán tử +, *, -, /, cũng vậy, như đa số các toán tử khác. Nếu một hàm được đặt tên chỉ bằng những kí tự đặc biệt, thì nó được mặc định là hàm trung tố. Nếu ta muốn kiểm tra kiểu của nó, hãy truyền nó đến một hàm khác hoặc gọi nó dưới dạng hàm tiền tố, khi đó nhớ bọc tên hàm trong cặp ngoặc tròn.

Rất thú vị. Ta đã thấy một điều mới mẻ ở đây, dấu =>. Mọi thứ đặt trước dấu => được gọi là một ràng buộc theo lớp. Ta có thể đọc lời khai báo kiểu trên như sau: hàm đẳng thức nhận vào hai giá trị bất kì có cùng kiểu và trả về một Bool. Kiểu của hai giá trị đó phải thuộc lớp Eq (đây chính là ràng buộc theo lớp).

Lớp Eq cho ta một giao diện để kiểm tra sự bằng nhau. Bất kì kiểu nào mà ta có thể so sánh ngang bằng giữa hai giá trị trong kiểu đó sẽ thuộc lớp Eq. Tất cả những kiểu tiêu chuẩn của Haskell ngoại trừ kiểu IO (kiểu liên quan tới nhập-xuất) cùng các hàm đều thuộc về lớp Eq.

Hàm elem có kiểu (Eq a) => a -> [a] -> Bool vì nó dùng == duyệt theo một danh sách để kiểm tra xem liệu có giá trị nào cần tìm trong danh sách không.

Một số lớp chứa kiểu cơ bản:

Eq được dùng cho các kiểu mà phép kiểm tra ngang bằng áp dụng được với chúng. Các kiểu thuộc lớp này cung cấp hai hàm ==/=. Vì vậy nếu có một ràng buộc theo lớp Eq đối với một biến kiểu trong một hàm, thì nó sẽ dùng == hoặc /= ở đâu đó trong lời định nghĩa của mình. Tất cả những kiểu chúng ta đã đề cập trước đây, trừ các hàm, đều thuộc về Eq, vì vậy chúng có thể được kiểm tra sự ngang bằng.

ghci> 5 == 5
True
ghci> 5 /= 5
False
ghci> 'a' == 'a'
True
ghci> "Ho Ho" == "Ho Ho"
True
ghci> 3.432 == 3.432
True

Ord được dùng cho kiểu có thứ tự.

ghci> :t (>)
(>) :: (Ord a) => a -> a -> Bool

Tất cả những kiểu ta đã đề cập đến giờ, trừ các hàm, đều thuộc về Ord. Ord gồm tất cả những hàm so sánh tiêu chuẩn như >, <, >= and <=. Hàm compare nhận vào hai đối tượng cùng kiểu thuộc Ord rồi trả lại thứ tự giữa chúng. Ordering là một kiểu, nó có thể là GT, LT or EQ, lần lượt nghĩa là greater than (lớn hơn), lesser than (nhỏ hơn) và equal (bằng).

Để thuộc lớp Ord, một kiểu phải có tư cash thành viên trong một “câu lạc bộ” độc nhất và uy tín, đó là Eq.

ghci> "Abrakadabra" < "Zebra"
True
ghci> "Abrakadabra" `compare` "Zebra"
LT
ghci> 5 >= 2
True
ghci> 5 `compare` 3
GT

Các thành viên của Show có thể được biểu diễn dưới dạng chuỗi. Tất cả các kiểu mà ta đã đề cập đến, trừ hàm, đều thuộc Show. Hàm hay được dùng nhất với lớp Showshow. Hàm này nhận một giá trị là thành viên của Show rồi biểu diễn nó cho ta xem dưới dạng chuỗi.

ghci> show 3
"3"
ghci> show 5.334
"5.334"
ghci> show True
"True"

Read là một lớp hàm, nói nôm na thì nó đối ngược với Show. Hàm read nhận vào một chuỗi kí tự rồi trả lại một kiểu là thành viên của Read.

ghci> read "True" || False
True
ghci> read "8.2" + 3.8
12.0
ghci> read "5" - 2
3
ghci> read "[1,2,3,4]" ++ [3]
[1,2,3,4,3]

Đến giờ mọi việc vẫn ổn. Một lần nữa, tất cả các kiểu được trình bày đều nằm trong lớp này. Nhưng điều gì sẽ xảy ra nếu ta chỉ viết read "4"?

ghci> read "4"
<interactive>:1:0:
    Ambiguous type variable `a' in the constraint:
      `Read a' arising from a use of `read' at <interactive>:1:0-7
    Probable fix: add a type signature that fixes these type variable(s)

Điều mà GHCI báo cho ta là nó không biết chúng ta đang muốn trả về thứ gì. Lưu ý rằng các lần trước khi dùng read ta đã thực hiện ngay thao tác gì đó đối với kết quả. Bằng cách đó thì GHCI sẽ suy được ra là chúng ta muốn kết quả của read theo kiểu gì. Nếu ta dùng nó như boole thì GHCI biết rằng nó cần trả lại một Bool. Nhưng ở đây, nó biết rằng ta cần một kiểu nào đó thuộc về lớp Read, nhưng không biết là cụ thể kiểu nào. Ta hãy xem dấu ấn kiểu của read.

ghci> :t read
read :: (Read a) => String -> a

Thấy chưa? Nó trả lại một kiểu thuộc về Read nhưng nếu sau này ta không thử dùng nó bằng cách này hay cách khác, ta sẽ không thể biết được kiểu nào. Đó là lý do tại sao chúng ta cần dùng chú thích kiểu một cách tường minh. Chú thích kiểu là cách nói rõ rằng kiểu của một biểu thức cần phải là gì. Ta làm điều này bằng cách thêm dấu :: vào cuối biểu thức rồi chỉ định một kiểu. Hãy xem này:

ghci> read "5" :: Int
5
ghci> read "5" :: Float
5.0
ghci> (read "5" :: Float) * 4
20.0
ghci> read "[1,2,3,4]" :: [Int]
[1,2,3,4]
ghci> read "(3, 'a')" :: (Int, Char)
(3, 'a')

Đa số các biểu thức đều là thể loại mà trình biên dịch có thể tự suy luận ra kiểu của chúng. Nhưng đôi khi, trình biên dịch không thể biết phải trả lại một giá trị thuộc kiểu Int hay Float cho một biểu thức kiểu như read "5". Để xem kiểu đó là gì, thực chất Haskell phải định giá read "5". Nhưng vì Haskell là ngôn ngữ kiểu tĩnh nên nó phải biết tất cả các kiểu trước khi biên dịch mã lệnh (hoặc trước khi định giá, trong trường hợp với GHCI). Vì vậy, ta phải bảo Haskell: “Biểu thức phải có kiểu này, trong trường hợp cậu chưa biết!”.

Các thành viên của Enum là những kiểu được đánh thứ tự lần lượt — chúng có thể đếm được (enumerate). Ưu điểm chính của lớp Enum là chúng ta có thể dùng các kiểu của nó làm dãy danh sách. Chũng cũng định nghĩa sẵn các đối tượng liền sau và liền trước, mà ta có thể nhận được lần lượt bằng cách gọi hàm succpred. Các kiểu trong lớp này gồm: (), Bool, Char, Ordering, Int, Integer, FloatDouble.

ghci> ['a'..'e']
"abcde"
ghci> [LT .. GT]
[LT,EQ,GT]
ghci> [3 .. 5]
[3,4,5]
ghci> succ 'B'
'C'

Các thành viên thuộc Bounded có một giới hạn trên và một giới hạn dưới.

ghci> minBound :: Int
-2147483648
ghci> maxBound :: Char
'\1114111'
ghci> maxBound :: Bool
True
ghci> minBound :: Bool
False

minBoundmaxBound rất đáng quan tâm vì chúng có một kiểu (Bounded a) => a. Theo nghĩa nào đó, chúng là hằng số đa hình [theo nghĩa có thể biến hình vào kiểu nào cũng được].

Tất cả các bộ cũng là thành viên của Bounded nếu như các thành phần trong bộ là thành viên thuộc lớp này.

ghci> maxBound :: (Bool, Int, Char)
(True,2147483647,'\1114111')

Num là một lớp kiểu số. Các thành viên của nó có thuộc tính là khả năng giữ vai trò của con số. Ta hãy xét kiểu của một con số.

ghci> :t 20
20 :: (Num t) => t

Dường như là các số nguyên cũng là hằng số đa hình. Chúng có thể đóng vài trò của bất kì kiểu nào thuộc lớp Num.

ghci> 20 :: Int
20
ghci> 20 :: Integer
20
ghci> 20 :: Float
20.0
ghci> 20 :: Double
20.0

Đó là các kiểu có trong lớp Num. Nếu kiểm tra kiểu của *, ta sẽ thấy rằng nó chấp nhận tất cả các con số.

ghci> :t (*)
(*) :: (Num a) => a -> a -> a

Nó nhận vào hai số cùng kiểu rồi trả lại một số cũng kiểu đó. Điều này lý giải tại sao (5 :: Int) * (6 :: Integer) sẽ gây ra lỗi về kiểu trong khi 5 * (6 :: Integer) thì lại được và ra kết quả là một Integer5 có thể đóng vai trò là một Integer hoặc Int.

Để gia nhập lớp Num, một kiểu phải là “bạn” sẵn với ShowEq.

Integral cũng là một lớp kiểu số. Num bao gồm tất cả các số, kể cả số thực lẫn số nguyên, Integral chỉ bao gồm số nguyên, trong lớp này có IntInteger.

Floating chỉ bao gồm số thực, tức là gồm các kiểu FloatDouble.

Một hàm rất có ích dùng đối với các số là fromIntegral. Nó có dạng khai báo fromIntegral :: (Num b, Integral a) => a -> b. Từ dấu ấn về kiểu này ta thấy rằng nó nhận một số nguyên rồi biến nó sang một số có dạng “tổng quát” hơn. Điều này có ích khi bạn muốn các kiểu số nguyên và kiểu dấu phẩy động tính toán được với nhau. Chẳng hạn, hàm length có lời khai báo kiểu là length :: [a] -> Int thay vì có một kiểu tổng quát hơn là (Num b) => length :: [a] -> b. Tôi nghĩ rằng có lí do lịch sử hay một điều gì đó lý giải cho việc này, song theo tôi, cách khai báo trên khá ngốc nghếch. Dù sao, nếu ta thử lấy chiều dài một danh sách rồi đem cộng nó với 3.2, ta sẽ nhận được lỗi vì ta đã thử cộng một Int với một số có phần thập phân (dấu phẩy động). Để khắc phục điều này, ta có thể viết fromIntegral (length [1,2,3,4]) + 3.2 và nó hoạt động bình thường.

Lưu ý rằng fromIntegral có vài ràng buộc theo lớp trong dấu ẩn kiểu của nó. Điều này hoàn toàn hợp lệ và như bạn sẽ thấy, các ràng buộc theo lớp được phân cách bởi các dấu phẩy bên trong cặp ngoặc tròn.

3 phản hồi

Filed under Haskell

3 responses to “Chương 3: Kiểu và lớp chứa kiểu

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

  2. Pingback: Chương 8: Tự lập nên các kiểu và lớp riêng | Blog của Chiến

  3. Mon

    cảm ơn a Chiến

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