markdown:
Trở về
[Mục lục](https://quangchien.wordpress.com/2012/04/16/ch1-learn-haskell#toc) cuốn sách
Ở những chương trước, ta đã tìm hiểu một số kiểu và lớp chứa kiểu sẵn có trong Haskell. Trong chương này, ta sẽ học cách tự lập nên các kiểu và lớp, đồng thời khiến chúng hoạt động!
### Giới thiệu những kiểu dữ liệu đại số
Tới giờ, chúng ta đã gặp nhiều kiểu dữ liệu:
Bool,
Int,
Char,
Maybe, v.v. Nhưng làm thế nào để tự mình tạo các kiểu dữ liệu riêng? Ồ, một cách làm là dùng từ khóa
*data* để định nghĩa kiểu. Ta hãy xem cách định nghĩa kiểu
Bool trong thư viện chuẩn.
data Bool = False | True
data nghĩa là ta đang định nghĩa một kiểu dữ liệu mới. Phần đứng trước
= để chỉ kiểu, ở đây là
Bool. Phần đứng sau là
= là
*constructor giá trị*. [Trong Haskell, [hàm constructor](http://en.wikibooks.org/wiki/Haskell/Type_declarations#data_and_constructor_functions) (“hàm dựng”) là khái niệm riêng, chắc không liên quan đến constructor thường thấy trong lập trình hướng đối tượng. Trong trường hợp đang xét không có mặt hàm, chỉ có các giá trị nên gọi là “constructor giá trị”.] Chúng chỉ định những giá trị khác nhau mà kiểu này có thể chứa. Dấu
| được đọc là
*hoặc*. Vì vậy ta có thể đọc câu lệnh này là: kiểu
Bool có thể nhận giá trị là
True hoặc
False. Cả tên kiểu và constructor giá trị đều phải được viết bắt đầu bằng chữ in.
Tương tự, ta có thể hình dung kiểu
Int được định nghĩa như sau:
data Int = -2147483648 | -2147483647 | ... | -1 | 0 | 1 | 2 | ... | 2147483647
Các constructor giá trị đầu và cuối là những giá trị nhỏ nhất và lớn nhất mà
Int có thể nhận. Thực ra thì kiểu này không được định nghĩa như vậy, những dấu ba chấm chỉ được dùng với mục đích minh họa, vì ta muốn tránh viết một loạt những con số.
Bây giờ, hãy hình dung xem ta có thể biểu diễn một hình phẳng trong Haskell thế nào. Một cách là dùng các bộ. Đường tròn có thể được kí hiệu bởi
(43.1, 55.0, 10.4) trong đó các trường thứ nhất và thứ hai là tọa độ tâm đường tròn còn trường thứ ba là bán kính. Nghe có vẻ ổn, nhưng cách viết này cũng có thể biểu diễn véc-tơ 3 chiều hoặc một thứ nào đó khác. Một cách làm tốt hơn là tự ta lập một kiểu dùng dể biểu diễn hình dạng. Giả sử rằng hình phẳng này có thể là đường tròn hoặc hình chữ nhật. Ở đây nó là:
data Shape = Circle Float Float Float | Rectangle Float Float Float Float
Giờ thì cái thì thế này? Hãy hình dung nó như sau: constructor giá trị
Circle có ba trường, nhận các số dấu chấm động. Vì vậy khi ta viết một constructor giá trị, ta được tự chọn việc thêm một số kiểu vào sau nó và những kiểu này sẽ định nghĩa các giá trị mà nó chứa. Ở đây, hai trường đầu tiên là các tọa độ của tâm hình, còn trường thứ ba là bán kính. constructor giá trị
Rectangle có bốn trường đều chấp nhận các số có dấu chấm động. Hai số đầu là các tọa độ của góc trái phía trên và hai giá trị sau là các tọa độ của góc phải phía dưới.
Ở đây khi tôi nói trường, ý của tôi muốn nói là tham số. Các constructor giá trị thực ra là những hàm mà cuối cùng trả lại một giá trị của một kiểu dữ liệu. Ta hãy xem dấu ấn kiểu của hai constructor giá trị này.
ghci> :t Circle Circle :: Float -> Float -> Float -> Shape ghci> :t Rectangle Rectangle :: Float -> Float -> Float -> Float -> Shape
Hay đấy, vậy là các constructor giá trị cũng là hàm như những thứ khác. Ai mà nghĩ được như vậy? Ta hãy lập một hàm nhận vào một hình phẳng và trả lại diện tích của hình đó.
surface :: Shape -> Float surface (Circle _ _ r) = pi * r ^ 2 surface (Rectangle x1 y1 x2 y2) = (abs $ x2 - x1) * (abs $ y2 - y1)
Điều thứ nhất đáng chú ý ở đây là khai báo kiểu. Nó nói rằng hàm nhận vào một hình dạng và trả lại một số kiểu chấm động. Ta không thể viết khai báo kiểu như
Circle -> Float vì
Circle không phải là một kiểu, mà ở đây
Shape mới là kiểu. Cũng như ta không thể viết một hàm với khai báo kiểu là
True -> Int. Điều kế tiếp mà ta nhận thấy ở đây là có thể khớp mẫu với các thành tố dựng. Ta khớp mẫu với các thành tố dựng trước đó (thật ra là mọi lục) khi ta khớp mẫu với những giá trị như
[] hoặc
False hoặc
5, chỉ khác là các giá trị đó không chứa trường nào cả. Ta chỉ cần viết một dựng kiểu rồi gắn các trường của nó với các tên gọi. Vì ta quan tâm đến bán kính nên thực ra chẳng cần để ý đến hai trường đầu, vốn chứa thông tin về vị trí đường tròn.
ghci> surface $ Circle 10 20 10 314.15927 ghci> surface $ Rectangle 0 0 100 100 10000.0
A, được rồi! Nhưng nếu ta thử chỉ in ra
Circle 10 20 5 từ dấu nhắc, ta sẽ gặp phải lỗi. Đó là vì Haskell không (đúng hơn là chưa) biết làm thế nào để hiển thị kiểu dữ liệu ta cần dưới dạng chuỗi. Hãy nhớ rằng khi ta thử in một giá trị từ dấ nhắc lệnh, ban đầu Haskell sẽ chạy hàm
show để lấy dạng hiển thị chuỗi của giá trị này, rồi in nó ra màn hình. Để làm cho kiểu
Shape của ta trở thành một phần của lớp
Show thì cần phải điều chỉnh lại như sau:
data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)
Bây giờ tạm thời ta không bận tâm suy luận nhiều. Hãy coi như là nếu ta thêm
deriving (Show) vào cuối lời khai báo
*data* thì Haskell sẽ tự động làm cho kiểu đó trở thành một phần của lớp
Show. Vì vậy bây giờ ta đã có thể viết:
ghci> Circle 10 20 5 Circle 10.0 20.0 5.0 ghci> Rectangle 50 230 60 90 Rectangle 50.0 230.0 60.0 90.0
constructor giá trị là các hàm, vì vậy ta có thể ánh xạ và áp dụng từng phần với mọi thứ. Nếu muốn có danh sách các đường tròn đồng tâm với bán kính khác nhau, ta có thể viết như sau.
ghci> map (Circle 10 20) [4,5,6,6] [Circle 10.0 20.0 4.0,Circle 10.0 20.0 5.0,Circle 10.0 20.0 6.0,Circle 10.0 20.0 6.0]
Kiểu dữ liệu vừa lập là tốt rồi, dù nó có thể còn tốt nữa. Ta hãy lập một kiểu dữ liệu trung gian để định nghĩa một điểm trong không gian hai chiều. Sau đó ta có thể sử dụng kiểu này để giúp cho các loại hình phẳng trở nên dễ hiểu với người đọc mã lệnh hơn.
data Point = Point Float Float deriving (Show) data Shape = Circle Point Float | Rectangle Point Point deriving (Show)
Lưu ý rằng khi định nghĩa một điểm, ta lấy cùng một tên để đặt cho cả kiểu dữ liệu lẫn constructor giá trị. Điều này không có ý nghĩa đặc biệt gì, mặc dù thông thường người ta lấy trùng tên như vậy khi chỉ có một constructor giá trị. Vì vậy, bây giờ
Circle có hai trường, một thuộc vào kiểu
Point và trường kia có kiểu
Float. Việc này giúp dễ dàng phân biệt được các thứ với nhau. Tương tự với đối tượng hình chữ nhật. Ta cần chỉnh lại hàm
surface vừa viết để phản ánh những thay đổi này.
surface :: Shape -> Float surface (Circle _ r) = pi * r ^ 2 surface (Rectangle (Point x1 y1) (Point x2 y2)) = (abs $ x2 - x1) * (abs $ y2 - y1)
Thứ duy nhất ta phải thay đổi là các mẫu. Ta không xét đến điểm trong mẫu hình tròn. Ở mẫu hình chữ nhật, ta chỉ dùng một dạng khớp mẫu lồng ghép để thu được trường các điểm. Nếu, vì một lý do nào đó, ta muốn tham chiếu đến bản thân các điểm đó thì có thể dùng mẫu “as”.
ghci> surface (Rectangle (Point 0 0) (Point 100 100)) 10000.0 ghci> surface (Circle (Point 0 0) 24) 1809.5574
Bạn nghĩ sao về một hàm để dịch chuyển hình? Hàm này nhận vào một hình, độ dịch chuyển theo trục x và độ dịch chuyển theo trục y, rồi trả lại một hình mới có cùng kích thước hình ban đầu nhưng nằm ở vị trí nào đó khác.
nudge :: Shape -> Float -> Float -> Shape nudge (Circle (Point x y) r) a b = Circle (Point (x+a) (y+b)) r nudge (Rectangle (Point x1 y1) (Point x2 y2)) a b = Rectangle (Point (x1+a) (y1+b)) (Point (x2+a) (y2+b))
Khá rõ ràng. Ta cộng các độ dịch chuyển vào các điểm chỉ định vị trí của hình.
ghci> nudge (Circle (Point 34 34) 10) 5 10 Circle (Point 39.0 44.0) 10.0
Nếu không muốn trực tiếp xử lý các điểm, ta có thể lập một số hàm phụ trợ để tạo các hình với kích thước nào đó đặt tại gốc tọa độ rồi dịch chuyển chúng.
baseCircle :: Float -> Shape baseCircle r = Circle (Point 0 0) r baseRect :: Float -> Float -> Shape baseRect width height = Rectangle (Point 0 0) (Point width height)
ghci> nudge (baseRect 40 100) 60 23 Rectangle (Point 60.0 23.0) (Point 100.0 123.0)
Dĩ nhiên là bạn có thể xuấ khẩu các kiểu dữ liệu trong module bạn viết. Để làm được điều này, chỉ cần viết kiểu dữ liệu đi theo các hàm được xuất khẩu, sau đó viết thêm cặp ngoặc đơn trong đó chỉ định những constructor giá trị nào cần được xuất khẩu, phân cách bởi dấu phẩy. Nếu bạn muốn xuất khẩu toàn bộ các constructor giá trị trong một kiểu nhât định thì chỉ cần viết
...
Nếu ta muốn xuất khẩu các hàm và kiểu giá trị được định nghĩa ở đây trong một module, ta có thể bắt đầu viết như sau:
module Shapes ( Point(..) , Shape(..) , surface , nudge , baseCircle , baseRect ) where
Bằng cách viết
Shape(..), ta đã xuất khẩu toàn bộ các constructor giá trị cho
Shape, vì vậy điều này nghĩa là bất cứ ai nhập module ta viết đều có thể tạo các hình bằng những constructor giá trị
Rectangle và
Circle. Nó giống như viết
Shape (Rectangle, Circle).
Ta cũng có thể chọn cách không xuất khẩu bất cứ constructor giá trị nào của
Shape bằng cách chỉ viết
Shape trong câu lệnh xuất khẩu. Bằng cách này, một người nhập module của ta chỉ có thể tạo các hình bằng những hàm phụ trợ
baseCircle và
baseRect.
Data.Map đã dùng phương pháp như vậy. Bạn không thể tạo lập ánh xạ bằng cách viết
Map.Map [(1,2),(3,4)] vì nó không xuất khẩu constructor giá trị đó. Tuy nhiên bạn có thể tạo một anash xạ bằng cách dùng một trong các hàm phụ trợ như
Map.fromList. Hãy nhớ rằng các constructor giá trị chỉ là các hàm nhận vào tham số là các trường và trả lại kết quả là một giá trị thuộc kiểu nào đó (như
Shape). Vì vậy khi ta quyết định không xuất khẩu các hàm đó, ta đã ngăn không cho người khác sau khi nhập module, có thể dùng nhứng hàm này; nhưng nếu có hàm nào đó khác đã xuất khẩu mà trả lại một kiểu dữ liệu, thì ta có thể dùng những hàm đó để lập nên các giá trị của kiểu dữ liệu mà ta tạo ra.
Việc không xuất khẩu các constructor giá trị của một kiểu dữ liệu sẽ làm cho chúng trở nên trừu tượng hơn, theo cách mà ta giấu cơ chế thực hiện của chúng. Hơn nữa, bất cứ người nào dùng module ta viết đều không thể khớp mẫu với các constructor giá trị được.
### Cú pháp dạng bản ghi
Vậy là ta đã được giao nhiệm vụ tạo một kiểu dữ liệu để miêu tả một người. Thông tin mà ta muốn lưu trữ về người đó bao gồm: họ, tên, tuổi, chiều cao, số điện thoại, và vị kem được ưa thích. Không rõ bạn nghĩ sao, nhưng đó là tất cả những gì tôi muốn biết ở một người. Ta hãy viết mã lệnh nào!
data Person = Person String String Int Float String String deriving (Show)
Được rồi. Trường thứ nhất là họ, trường thứ hai là tên, trường thứ ba là tuổi, và cứ như vậy. Ta hãy tạo ra dữ liệu về người mới.
ghci> let guy = Person "Buddy" "Finklestein" 43 184.2 "526-2928" "Chocolate" ghci> guy Person "Buddy" "Finklestein" 43 184.2 "526-2928" "Chocolate"
Vậy cũng được, dù hơi khó đọc. Sẽ thế nào nếu ta muốn tạo ra một hàm để lấy thông tin riêng từ một người? Một hàm lấy tên của người nào đó, một hàm lấy họ của người nào đó, v.v. À, ta sẽ phải định nghĩa các hàm đó kiểu như sau.
firstName :: Person -> String firstName (Person firstname _ _ _ _ _) = firstname lastName :: Person -> String lastName (Person _ lastname _ _ _ _) = lastname age :: Person -> Int age (Person _ _ age _ _ _) = age height :: Person -> Float height (Person _ _ _ height _ _) = height phoneNumber :: Person -> String phoneNumber (Person _ _ _ _ number _) = number flavor :: Person -> String flavor (Person _ _ _ _ _ flavor) = flavor
Phù! Thật sự tôi không thích viết mã lệnh như vậy! Tuy viết như vậy rất lủng củng và NHÀM CHÁN, song cách này hoạt động được.
ghci> let guy = Person "Buddy" "Finklestein" 43 184.2 "526-2928" "Chocolate" ghci> firstName guy "Buddy" ghci> height guy 184.2 ghci> flavor guy "Chocolate"
Chắc phải có cách hay hơn chứu nhỉ? Ồ, không có đâu, rất tiếc.
Đùa chút thôi, có đấy. Ha ha ha! Những người lập nên Haskell đều rất thông minh và đã lường trước tình huống này. Họ đã kèm theo một cách khác để viết những kiểu dữ liệu. Sau đây là cách để ta đạt được tính năng nói trên dùng cú pháp dạng bản ghi (record).
data Person = Person { firstName :: String , lastName :: String , age :: Int , height :: Float , phoneNumber :: String , flavor :: String } deriving (Show)
Như vậy thay vì chỉ đặt tên các kiểu của trường lần lượt và phân biệt bởi những dấu cách thì lần này ta dùng cặp ngoặc nhọn. Đầu tiên, ta viết tên của trường, chẳng hạn
firstName rồi viết hai dấu hai chấm
:: (còn được gọi là Paamayim Nekudotayim, ha ha) tiếp theo là chỉ định kiểu dữ liệu. Đối với kiểu dữ liệu của kết quả cũng tương tự. Ưu điểm chủ yếu của điều này là nó tạo ra các hàm để tra tìm các trường trong kiểu dữ liệu. Bằng cách dùng cú pháp kiểu bản ghi để tạo ra kiểu dữ liệu này, Haskell tự động lập ra các hàm:
firstName,
lastName,
age,
height,
phoneNumber và
flavor.
ghci> :t flavor flavor :: Person -> String ghci> :t firstName firstName :: Person -> String
Còn có một ích lợi khác khi dùng dạng cú pháp kiểu bản ghi. Khi ta suy diễn (derive)
Show cho kiểu dữ liệu thì nó sẽ hiển thị theo cách khác với khi ta dùng cú pháp bản ghi để định nghĩa và tạo kiểu. Chẳng hạn ta có một kiểu biểu diễn cho xe hơi. Ta cần phải theo dõi công ty sản xuất, mẫu mã và năm sản xuất xe hơi này. Hãy xem đây.
data Car = Car String String Int deriving (Show)
ghci> Car "Ford" "Mustang" 1967 Car "Ford" "Mustang" 1967
Nếu ta định nghĩa nó bằng cú pháp kiểu bản ghi, thì ta có thể tạo ra một xe mới như thế này.
data Car = Car {company :: String, model :: String, year :: Int} deriving (Show)
ghci> Car {company="Ford", model="Mustang", year=1967} Car {company = "Ford", model = "Mustang", year = 1967}
Khi tạo ra xe hơi mới, ta không nhất thiết phải xếp các trường theo đúng thứ tự, miễn là liệt kê đủ. Nhưng nếu không dùng cú pháp dạng bản ghi thì ta phải nêu các trường theo thứ tự.
Hãy dùng cú pháp dạng bản ghi khi constructor có nhiều trường mà không rõ ý nghĩa của từng trường. Nếu ta tạo một kiểu dữ liệu véc-tơ 3 chiều bằng cách viết
data Vector = Vector Int Int Int, thì khá dễ thấy rằng các trường đều là thành phần của véc-tơ. Nhưng ở hai kiểu
Person và
Car vừa rồi, điều đó không rõ ràng, và bằng việc dùng cú pháp dạng bản ghi thật là có lợi.
### Tham số kiểu
Constructor giá trị có thể nhận vài giá trị làm tham số và sau đó tạo ra một giá trị mới. Chẳng hạn, constructor
Car nhận vào ba giá trị và tạo ra một giá trị “xe hơi”. Tương tự,
*constructor kiểu* có thể nhận vào tham số là các kiểu dữ liệu để tạo ra những kiểu mới. Điều này thoạt nghe thì rất chung chung nhưng nó không đến nỗi phức tạp. Nếu bạn đã quen với các template trong C++, thì có thể liên hệ với chúng. Để có cái nhìn rõ rệt về cách hoạt động của tham số kiểu, ta hãy xét xem một kiểu dữ liệu ta đã gặp được lập ra sao.
data Maybe a = Nothing | Just a
Ở đây,
a là tham số kiểu. Và chính vì có sự tham gia của tham số kiểu mà ta gọi
Maybe là constructor kiểu. Tùy ý ta muốn kiểu dữ liệu này chứa thứ gì khi không phải
Nothing, constructor kiểu có thể kết thúc bằng việc tạo ra một kiểu
Maybe Int,
Maybe Car,
Maybe String, v.v. Không có giá trị nào có kiểu chỉ mỗi là
Maybe, và bản thân nó không phải là một kiểu, mà là một constructor kiểu. Để cho nó là một kiểu thực thụ và chứa các giá trị, thì tất cả những tham số kiểu của nó cần được điền đủ các giá trị vào.
Vì vậy nếu ta truyền
Char với vai trò tham số vào
Maybe,thì ta sẽ có kiểu
Maybe Char. Giá trị
Just 'a', chẳng hạn, sẽ có kiểu là
Maybe Char.
Có thể bạn chưa biết rằng ta đã dùng đến một kiểu có một tham số kiểu trước khi gặp
Maybe. Kiểu đó là danh sách. Tuy có mặt của cách viết tiện lợi (syntactic sugar), song cơ bản là kiểu danh sách nhận một tham số để tạo ra một kiểu cụ thể. Các giá trị có thể mang kiểu
[Int], một kiểu
[Char], một kiểu
[[String]], nhưng bạn không thể có một giá trị chỉ có kiểu là
[].
Ta hãy thử nghịch chơi kiểu
Maybe.
ghci> Just "Haha" Just "Haha" ghci> Just 84 Just 84 ghci> :t Just "Haha" Just "Haha" :: Maybe [Char] ghci> :t Just 84 Just 84 :: (Num t) => Maybe t ghci> :t Nothing Nothing :: Maybe a ghci> Just 10 :: Maybe Double Just 10.0
Tham số kiểu rất ó ích vì với chúng, ta có thể tạo ra những kiểu khác nhau tùy theo những dạng nào ta muốn bao gồm trong kiểu dữ liệu đang xét. Khi ta viết
:t Just "Haha", cơ chế suy luận kiểu sẽ hình dung ra được đó là kiểu
Maybe [Char], vì nếu
a có trong
Just a là một chuỗi, thì
a trong
Maybe a cũng phải là một chuỗi.
Lưu ý rằng kiểu của
Nothing là
Maybe a. Kiểu của nó có tính đa hình. Nếu một hàm nào đó yêu cầu tham số là một
Maybe Int thì ta có thể đưa cho nó
Nothing, vì
Nothing đằng nào cũng không chứa giá trị gì, do đó việc làm trên cũng không ảnh hưởng. Kiểu
Maybe a có thể đóng vài trò là
Maybe Int nếu cần, cũng như
5 có thể đóng vai trò
Int hoặc
Double. Tương tự, kiểu của danh sách rỗng là
[a]. Một danh sách rỗng có thể đóng vai trò của danh sách bất cứ thứ gì. Đó là lý do tại sao ta có thể viết
[1,2,3] ++ [] và
["ha","ha","ha"] ++ [].
Dùng tham số kiểu rất có lợi, nhưng chỉ khi việc dùng chúng có ý nghĩa. Thông thường ta dùng chúng khi kiểu dữ liệu đang dùng sẽ phát huy tác dụng bất kể giá trị nó chứa có kiểu gì, giống như ở
Maybe a. Nếu kiểu dữ liệu đang xét đóng vai trò như một cái hộp thì ta nên dùng chúng. Ta có thể sửa lại kiểu dữ liệu
Car từ thế này:
data Car = Car { company :: String , model :: String , year :: Int } deriving (Show)
Sang thế này:
data Car a b c = Car { company :: a , model :: b , year :: c } deriving (Show)
Nhưng thực sự là ta sẽ được lợi không? Câu trả lời là: có thể không, vì rồi ta sẽ phải định nghĩa các hàm chỉ hoạt động được với kiểu
Car String String Int. Chẳng hạn, với định nghĩa đầu tiên về
Car, ta có thể lập một hàm để hiển thị các thuộc tính của chiếc xe hơi dưới dạng một câu nói tóm tắt.
tellCar :: Car -> String tellCar (Car {company = c, model = m, year = y}) = "This " ++ c ++ " " ++ m ++ " was made in " ++ show y
ghci> let stang = Car {company="Ford", model="Mustang", year=1967} ghci> tellCar stang "This Ford Mustang was made in 1967"
Thật là một hàm xinh xắn! Lời khai báo kiểu rất đẹp và hoạt động tốt. Bây giờ nếu không phải là
Car mà là
Car a b c thì sao?
tellCar :: (Show a) => Car String String a -> String tellCar (Car {company = c, model = m, year = y}) = "This " ++ c ++ " " ++ m ++ " was made in " ++ show y
Ta đã phải buộc hàm này nhận vào một kiểu
Car là
(Show a) => Car String String a. Bạn có thể thấy rằng dấu ấn kiểu đã phức tạp hơn và ích lợi duy nhất mà ta thu được chỉ là giờ ta được phép lấy bất kì kiểu nào là thực thể trong lớp
Show dùng làm kiểu cho
c.
ghci> tellCar (Car "Ford" "Mustang" 1967) "This Ford Mustang was made in 1967" ghci> tellCar (Car "Ford" "Mustang" "nineteen sixty seven") "This Ford Mustang was made in \"nineteen sixty seven\"" ghci> :t Car "Ford" "Mustang" 1967 Car "Ford" "Mustang" 1967 :: (Num t) => Car [Char] [Char] t ghci> :t Car "Ford" "Mustang" "nineteen sixty seven" Car "Ford" "Mustang" "nineteen sixty seven" :: Car [Char] [Char] [Char]
Dù vậy trên thực tế thì đại đa số trường hợp ta sẽ dùng
Car String String Int, nên dường như việc tham số hóa kiểu
Car thật không bõ công. Ta thường dùng những tham số kiểu khi mà kiểu chứa trong những constructor kiểu không thật quan trọng đối với kiểu dữ liệu để có thể hoạt động được. Một danh sách các thứ, bản thân nó là nó, vẫn hoạt động được mà chẳng can hệ gì đến kiểu của các thứ trong đó. Nếu ta muốn tính tổng một danh sách các số, ta có thể làm rõ sau trong trong hàm tính tổng, là ta muốn cụ thể một danh sách số. Với
Maybe cũng tương tự như vậy.
Maybe biểu thị một lựa chọn hoặc là không có gì, hoặc là có một thứ gì đó. Còn “thứ gì đó” có thể thuộc kiểu dữ liệu gì cũng được.
Một ví dụ khác về kiểu được tham số hóa mà ta đã gặp là
Map k v trong
Data.Map. Ở đây
k là kiểu của các khóa trong ánh xạ còn
v là kiểu của các giá trị. Đây là một ví dụ hay, trong đó các tham số kiểu rất có ích. Ánh xạ khi được tham số hóa sẽ cho phép ta thực hiện ánh xạ từ kiểu này sang kiểu khác, miễn là kiểu của khóa thuộc về lớp
Ord. Nếu đang định nghĩa một kiểu ánh xạ, ta có thể thêm vào một ràng buộc về lớp kiểu trong lời khai báo
*data*:
data (Ord k) => Map k v = ...
Tuy vậy, một quy tắc phổ biến trong Haskell là
*không bao giờ thêm các ràng buộc lớp vào trong lời khai báo data. *Tại sao ư? À, bởi vì ta không được lợi nhiều, mà lại phải viết thêm các ràng buộc lớp ngay cả khi không cần đến chúng. Nếu ta đặt ràng buộc
Ord k vào trong lời khai báo
*data* của
Map k v, ta sẽ phải đặt ràng buộc vào các hàm giả sử khóa trong ánh xạ có thể xếp thứ tự. Nhưng nếu ta không đặt ràng buộc vào trong lời khai báo dữ liệu, thì ta sẽ không phải đặt
(Ord k) => trong lời khai báo kiểu của hàm mà không quan tâm đến việc liệu các khóa có thể sắp thứ tự hay không. Một ví dụ về hàm dạng này là
toList; nó nhận vào một phép ánh xạ rồi chuyển đổi ánh xạ này thành một danh sách liên kết. Dấu ấn kiểu của hàm là
toList :: Map k a -> [(k, a)]. Nếu
Map k v có ràng buộc kiểu trong lời khai báo
*data*, thì kiểu của
toList sẽ phải là
toList :: (Ord k) => Map k a -> [(k, a)], mặc dù hàm không thực hiện bất kì việc so sánh khóa nào theo thứ tự.
Bởi vậy, hãy đừng đưa ràng buộc kiểu vào trong lời khai báo
*data* ngay cả khi có vẻ như hợp lý, vì dù gì đi nữa bạn cũng sẽ phải đặt chúng vào trong khai báo kiểu hàm.
Ta hãy lập kiểu véc-tơ 3 chiều và thêm vào một vài phép toán. Ta sẽ dùng một kiểu tham số hóa vì mặc dù nó sẽ thường dùng để chứa những kiểu số, nó vẫn sẽ cho phép thao tác một số phép toán với chúng.
data Vector a = Vector a a a deriving (Show) vplus :: (Num t) => Vector t -> Vector t -> Vector t (Vector i j k) `vplus` (Vector l m n) = Vector (i+l) (j+m) (k+n) vectMult :: (Num t) => Vector t -> t -> Vector t (Vector i j k) `vectMult` m = Vector (i*m) (j*m) (k*m) scalarMult :: (Num t) => Vector t -> Vector t -> t (Vector i j k) `scalarMult` (Vector l m n) = i*l + j*m + k*n
vplus được dùng để cộng hai véc-tơ với nhau. Hai véc-tơ được cộng lại chỉ bằng cách cộng những thành phần tương ứng.
scalarMult được dùng cho tích vô hướng của hai véc-tơ còn
vectMult dùng để nhân một véc-tơ vwosi một số vô hướng. Các hàm này có thể hoạt động được với những kiểu
Vector Int,
Vector Integer,
Vector Float bất kì, miễn là
a trong
Vector a thuộc về lớp
Num. Đồng thời, nếu bạn kiểm tra lời khai báo kiểu của những hàm này, bạn sẽ thấy rằng chúng chỉ có thể hoạt động trên những véc-tơ có cùng kiểu và các số tham gia vào phép tính cũng phải cùng kiểu với những số trong véc-tơ. Lưu ý rằng ta không đưa ràng buộc lớp
Num vào trong khai báo
*data*, vì dù sao ta vẫn phải lặp lại nó trong các hàm.
Một lần nữa, cần phân biệt rõ constructor kiểu và constructor giá trị. Khi khai báo một kiểu dữ liệu, phần phía trước dấu
= là constructor kiểu và những constructor đi sau dấu bằng (có thể có dấu
| ngăn cách) là những constructor giá trị. Việc trao kiểu
Vector t t t -> Vector t t t -> t cho một hàm sẽ là sai, vì ta phải đặt kiểu vào trong lời khai báo kiểu và constructor
*kiểu* dành cho véc-tơ thì chỉ nhận mỗi một tham số, còn constructor giá trị thì nhận ba tham số. Ta hãy thử nghịch chơi với kiểu véc-tơ vừa được lập.
ghci> Vector 3 5 8 `vplus` Vector 9 2 8 Vector 12 7 16 ghci> Vector 3 5 8 `vplus` Vector 9 2 8 `vplus` Vector 0 2 3 Vector 12 9 19 ghci> Vector 3 9 7 `vectMult` 10 Vector 30 90 70 ghci> Vector 4 9 5 `scalarMult` Vector 9.0 2.0 4.0 74.0 ghci> Vector 2 9 3 `vectMult` (Vector 4 9 5 `scalarMult` Vector 9 2 4) Vector 148 666 222
### Các thực thể suy diễn
Trong Mục
[Lớp chứa kiểu (nhập môn)](https://quangchien.wordpress.com/2012/04/20/ch3-learn-haskell/#typeclasses-101), chúng tôi đã trình bày những điều cơ bản về lớp (chứa kiểu). Chúng tôi có giải thích rằng một lớp là một dạng giao diện trong đó định nghĩa một hành vi nào đó. Một kiểu dữ liệu có thể được lập thành *thực thể* của một lớp nếu như lớp này cho phép hành vi của kiểu dữ liệu đó. Lấy ví dụ: kiểu
Int là một thực thể của lớp
Eq vì lớp
Eq có định nghĩa hành vi của những thứ có thể so sánh ngang bằng được. [Ở đây chỉ xét so sánh bằng hoặc khác nhau chứ không kể so sánh hơn kém.] Và vì số nguyên có thể được so sánh ngang bằng nên
Int là một phần thuộc về lớp
Eq. Ích lợi thực sự đến từ các hàm đóng vai trò là giao diện cho
Eq, cụ thể là
== và
/=. Nếu có một kiểu dữ liệu thuộc về lớp
Eq, thì ta có thể dùng các hàm
== với những giá trị theo kiểu đó. Điều này lý giải tại sao những biểu thức như
4 == 4 và
"foo" /= "bar" lại kiểm tra về kiểu.
Chúng tôi cũng nhắc đến rằng lớp chứa kiểu thường hay bị nhầm với khái niệm lớp trong những ngôn ngữ như Java, Python, C++ và tương tự, vốn từng làm rối trí nhiều người. Ở những ngôn nguex này, lớp là một “bí kíp” từ đó ta chế ra các đối tượng có chứa trạng thái, các đối tượng này có thể kèm theo hành động. Lớp chứa kiểu thì lại giống các giao diện hơn. Ta không tạo ra dữ liệu từ lớp chứa kiểu [sau này, để cho gọn, ta sẽ gọi chúng là các lớp]. Mà trước hết, ta tạo ra kiểu dữ liệu rồi sau đó nghĩ xem nó có thể hoạt động thế nào. Nếu nó hoạt động được như thứ gì mà so sánh ngang bằng được, thì ta sẽ đặt nó là một thực thể của lớp
Eq. Nếu nó hoạt động được như thể một thứ có thể sắp thứ tự, ta sẽ đặt nó là một thực thể của lớp
Ord.
Trong mục kế tiếp, ta sẽ xem cách thủ công tạo ra các thực thể kiểu thuộc lớp, bằng cách lập những hàm định nghĩa bởi lớp. Nhưng ngay bây giờ, hãy xem Haskell có thể tự động tạo ra kiểu đang xét là một thực thể của bất kì lớp nào trong số sau đây:
Eq,
Ord,
Enum,
Bounded,
Show,
Read. Haskell có thể suy diễn ra hành vi của kiểu dữ liệu trong ngữ cảnh này nếu ta dùng từ khóa
*deriving* trong khi tạo ra kiểu dữ liệu.
Xét kiểu dữ liệu sau:
data Person = Person { firstName :: String , lastName :: String , age :: Int }
Kiểu dữ liệu này mô tả một người. Giả định rằng không có hai người nào cùng chung tổ hợp họ, tên, và tuổi. Bây giờ, nếu ta có hai bản ghi thì liệu có thể xét được rằng đó có phải cùng chỉ về một người không? Được chứ. Ta có thể thử so sánh ngang bằng xem chúng có bằng nhau không. Điều này lý giải tại sao kiểu dữ liệu trên lại thuộc về lớp
Eq. Ta sẽ suy diễn ra thực thể như sau.
data Person = Person { firstName :: String , lastName :: String , age :: Int } deriving (Eq)
Khi suy diễn thực thể
Eq đối với một kiểu và rồi thử so sánh hai giá trị thuộc kiểu đó bằng các phép
== hoặc
/=, Haskell sẽ xem liệu rằng các constructor giá trị có khớp không (mặc dù trong trường hợp hiện tại chỉ có một constructor giá trị) và sau đó sẽ kiểm tra xem liệu tất cả dữ liệu chứa bên trong có khớp nhau không, bằng cách kiểm tra từng cặp trường bằng phép
==. Tuy vậy, chỉ có một điều kiện cần lưu ý: kiểu của tất cả các trường bên trong cũng phải thuộc về lớp
Eq. Song vì cả
String và
Int đều thỏa mãn yêu cầu này, nên mọi việc sẽ ổn. Ta hãy kiểm tra thực thể
Eq đang xét.
ghci> let mikeD = Person {firstName = "Michael", lastName = "Diamond", age = 43} ghci> let adRock = Person {firstName = "Adam", lastName = "Horovitz", age = 41} ghci> let mca = Person {firstName = "Adam", lastName = "Yauch", age = 44} ghci> mca == adRock False ghci> mikeD == adRock False ghci> mikeD == mikeD True ghci> mikeD == Person {firstName = "Michael", lastName = "Diamond", age = 43} True
Dĩ nhiên, vì
Person hiện giờ ở trong
Eq, ta có thể dùng nó như là
a với mọi hàm có một ràng buộc lớp
Eq a trong dấu ấn kiểu của chúng, chẳng hạn như
elem.
ghci> let beastieBoys = [mca, adRock, mikeD] ghci> mikeD `elem` beastieBoys True
Các lớp
Show và
Read được lần lượt dùng cho những thứ có thể chuyển được thành, và chuyển từ chuỗi. Cũng như với
Eq, nếu một kiểu có các constructor chứa các trường, thì kiểu này phải thuộc về
Show hoặc
Read nếu ta muốn làm cho kiểu của ta trở thành thực thể của các lớp đó. Hãy làm cho kiểu dữ liệu
Person trên thuộc về cả
Show lẫn
Read.
data Person = Person { firstName :: String , lastName :: String , age :: Int } deriving (Eq, Show, Read)
Bây giờ ta có thể in thông tin của một người lên màn hình.
ghci> let mikeD = Person {firstName = "Michael", lastName = "Diamond", age = 43} ghci> mikeD Person {firstName = "Michael", lastName = "Diamond", age = 43} ghci> "mikeD is: " ++ show mikeD "mikeD is: Person {firstName = \"Michael\", lastName = \"Diamond\", age = 43}"
Giá như ta đã thử in thông tin về người lên mành hình trước khi làm cho kiểu dữ liệu
Person thuộc về
Show, Haskell hẳn đã “phàn nàn”, nói rằng nó không biết cách làm thế nào để biểu diễn thông tin về người dưới dạng chuỗi. Nhưng bây giờ khi ta đã suy diễn một thực thể
Show cho nó, thì Haskell đã biết cách biểu diễn này.
Read chính là lớp ngược với
Show.
Show được dùng để chuyển các giá trị từ kiểu dữ liệu ta lập sang kiểu chuỗi,
Read thì dùng để chuyển từ chuỗi sang kiểu dữ liệu ta lập. Tuy vậy cần nhớ rằng, khi sử dụng hàm
read, ta cần ghi chú cụ thể về kiểu để Haskell biết được ta muốn lấy kết quả là kiểu gì. Nếu ta không ghi rõ kiểu cần thu được là gì, Haskell sẽ không tự nhận biết được.
ghci> read "Person {firstName =\"Michael\", lastName =\"Diamond\", age = 43}" :: Person Person {firstName = "Michael", lastName = "Diamond", age = 43}
Nếu sau này ta dùng kết quả của hàm
read của ta theo cách Haskell suy diễn là cần phải hiểu như dữ liệu về một người, thì sẽ không phải viết chú thích về kiểu nữa.
ghci> read "Person {firstName =\"Michael\", lastName =\"Diamond\", age = 43}" == mikeD True
Ta cũng có thể đọc các kiểu được tham số hóa, nhưng phải điền vào những tham số kiểu. Vì vậy ta không thẻ viết
read "Just 't'" :: Maybe a, nhưng lại có thể viết
read "Just 't'" :: Maybe Char.
Ta có thể suy diễn những thực thể cho lớp
Ord, vốn dành cho những kiểu có giá trị xếp được theo thứ tự. Nếu ta so sánh hai giá trị thuộc cùng một kiểu nhưng lại được tạo thành từ các constructor khác nhau thì giá trị được tạo bởi constructor định nghĩa trước sẽ được coi là nhỏ hơn.
Chẳng hạn, hãy xét kiểu
Bool, vốn có thể có giá trị là
False hoặc
True. Nhằm mục đích xét xem hành vi của kiểu dữ liệu này khi được so sánh ra sao, ta có thể hình dung rằng kiểu Bool được lập như sau:
data Bool = False | True deriving (Ord)
Vì constructor giá trị
False được chỉ định trước và constructor giá trị
True được chỉ định sau đó, nên ta có thể coi như
True lớn hơn
False.
ghci> True `compare` False GT ghci> True > False True ghci> True < False False
Trong kiểu dữ liệu
Maybe a, constructor giá trị
Nothing được chỉ định trước constructor giá trị
Just, vì vậy một giá trị
Nothing luôn nhỏ hơn một giá trị
Just something, ngay cả khi cái thứ
*something* có bằng âm một tỷ tỷ đi nữa. Nhưng khi so sánh hai giá trị
Just, thì sẽ quy về việc so sánh các thứ bên trong hai giá trị đó.
ghci> Nothing < Just 100 True ghci> Nothing > Just (-49999) False ghci> Just 3 `compare` Just 2 GT ghci> Just 100 > Just 50 True
Nhưng ta không thể viết kiểu như
Just (*3) > Just (*2), vì
(*3) và
(*2) là các hàm, và không phải là thực thể của lớp
Ord.
Ta có thể dễ dàng dùng những kiểu dữ liệu đại số để thực hiện việc liệt kê; các lớp
Enum và
Bounded sẽ giúp ta thực hiện điều đó. Xết kiểu dữ liệu sau đây:
data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday
Vì tất cả những constructor giá trị đều không nhận tham số (tức là trường) nên ta có thể cho nó thuộc về lớp
Enum. Lớp
Enum dành cho những thứ có các giá trị liền trước và liền sau. Ta cũng có thể cho nó thuộc về lớp
Bounded, vốn dành cho những thứ có giá trị thấp nhất và giá trị cao nhất. Xong việc, hãy làm cho nó thành một thực thể thuộc về tất cả những lớp suy diễn khác và xem ta có thể thao tác gì với nó.
data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday deriving (Eq, Ord, Show, Read, Bounded, Enum)
Vì kiểu dữ liệu này đã nằm trong các lớp
Show và
Read nên ta có thể chuyển những giá trị thuộc kiểu này sang kiểu chuỗi và từ kiểu chuỗi.
ghci> Wednesday Wednesday ghci> show Wednesday "Wednesday" ghci> read "Saturday" :: Day Saturday
Vì kiểu dữ liệu nằm trong các lớp
Eq và
Ord, nên ta có thể so sánh hơn kém hoặc ngang bằng các ngày với nhau.
ghci> Saturday == Sunday False ghci> Saturday == Saturday True ghci> Saturday > Friday True ghci> Monday `compare` Wednesday LT
Nó cũng nằm trong lớp
Bounded, vì vậy ta có thể lấy ngày “thấp nhất” và “cao nhất”.
ghci> minBound :: Day Monday ghci> maxBound :: Day Sunday
Nó cũng là một thực thể của
Enum. Ta có thể lấy các giá trị liền trước và liền sau của một ngày và có thể tạo danh sách khoảng từ hai giá trị ngày!
ghci> succ Monday Tuesday ghci> pred Saturday Friday ghci> [Thursday .. Sunday] [Thursday,Friday,Saturday,Sunday] ghci> [minBound .. maxBound] :: [Day] [Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday]
Thật là tuyệt.
### Sự tương đồng kiểu dữ liệu
Trước đây, chúng tôi có nói rằng khi viết các kiểu dữ liệu thì
[Char] và
String là hai kiểu tương đương và dùng thay cho nhau được. Điều này được thực hiện bằng sự
*tương đồng kiểu*. Tương đồng kiểu dữ liệu thực ra chẳng thực hiện bất cứ điều gì, chúng chỉ đặt các tên khác nhau cho cùng một kiểu dữ liệu để ý nghĩa hơn đối với người đọc mã lệnh và tài liệu hướng dẫn. Sau đây là cách mà thư viện chuẩn định nghĩa
String là kiểu tương đồng với
[Char].
type String = [Char]
Chúng tôi đã giới thiệu từ khóa
*type*. Từ khóa này có thể khiến bạn đọc hiểu nhầm, vì thực ra ta không tạo ra thứ gì mới (ta tạo mới bằng từ khóa *data*), mà chỉ tạo một kiểu tương đồng với kiểu dữ liệu sẵn có.
Nếu ta tạo một hàm để chuyển đổi chuỗi sang dạng chữ in và gọi nó là
toUpperStringhoặc gì đó, thì ta có thể khai báo kiểu như sau:
toUpperString :: [Char] -> [Char] hay
toUpperString :: String -> String. Cả hai cách này đều như nhau, cách viết thứ hai chỉ dễ đọc hơn.
Khi thao tác với module
Data.Map, đầu tiên là ta biểu diễn danh bạ điện thoại bằng một danh sách liên kết trước khi chuyển đổi nó thành một ánh xạ. Như ta đã phát hiện từ trước, danh sách liên kết là một danh sách chứa các cặp khóa-trị. Hãy xem một danh bạ điện thoại mà ta đã có.
phoneBook :: [(String,String)] phoneBook = [("betty","555-2938") ,("bonnie","452-2928") ,("patsy","493-2928") ,("lucille","205-2928") ,("wendy","939-8282") ,("penny","853-2492") ]
Ta thấy rằng kiểu của
phoneBook là
[(String,String)]. Điều này cho thấy nó là một danh sách liên kết ánh xạ từ chuỗi đến chuỗi, không hơn không kém. Hãy tạo ra một tương đồng kiểu để truyền đạt thêm thông tin trong lời khai báo kiểu.
type PhoneBook = [(String,String)]
Bây giờ thì lời khai báo kiểu của danh bạ điện thoại mà ta lập đã là
phoneBook :: PhoneBook. Hãy tạo một kiểu tương đồng cho
String nữa.
type PhoneNumber = String type Name = String type PhoneBook = [(Name,PhoneNumber)]
Cho kiểu tương đồng với
String là cách mà lập trình viên Haskell thực hiện khi họ muốn truyền đạt thêm thông tin rằng những chuỗi trong hàm họ viết được dùng với vai trò gì và chúng biểu diễn những gì.
Vì vậy bây giờ, khi ta lập một hàm nhận vào một tên và một con số rồi xem nếu tổ hợp tên và số đó có nằm trong danh bạ không thì ta có thể viết lời khai báo kiểu rất đẹp và đủ ý nghĩa như sau.
inPhoneBook :: Name -> PhoneNumber -> PhoneBook -> Bool inPhoneBook name pnumber pbook = (name,pnumber) `elem` pbook
Nếu ta quyết định không dùng kiểu tương đồng thì hàm của ta sẽ có kiểu
String -> String -> [(String,String)] -> Bool. Trong trường hợp này, lời khai báo kiểu mà tận dụng được ưu thế của kiểu tương đồng thì sẽ dễ hiểu hơn. Tuy nhiên bạn không nên lạm dụng. Ta giới thiệu kiểu tương đồng để miêu tả những gì mà một kiểu có sẵn nào đó biểu thị trong hàm (và vì vậy lời khai báo kiểu trở nên lời hướng dẫn cụ thể hơn) hoặc khi một thứ nào đó có kiểu dài ngoằng được lặp lại rất nhiều (như
[(String,String)]) nhưng biểu thị cho thứ gì đó cụ thể hơn trong ngữ cảnh của hàm đang được sử dụng.
Kiểu tương đồng cũng có thể được tham số hóa. Nếu ta muốn một kiểu để biểu diễn cho danh sách liên kết nhưng vẫn muốn nó tổng quát để nó có thể dùng được các khóa và giá trị thuộc bất kì kiểu nào, thì ta có thể viết như sau:
type AssocList k v = [(k,v)]
Bây giờ, một hàm nhận vào giá trị bằng một khóa trong danh sách liên kết có thể mang kiểu
(Eq k) => k -> AssocList k v -> Maybe v.
AssocList là một constructor kiểu nhận vào hai kiểu dữ liệu và tạo ra một kiểu cụ thể, chẳng hạn như
AssocList Int String.
*Fonzie nói:*Ấy! Khi tôi nói về *kiểu cụ thể*, ý tôi là những kiểu được áp dụng trọn vẹn như
Map Int Stringhoặc nếu ta xử lý với mộ trong số các hàm đa hình,
[a]hoặc
(Ord a) => Maybe avà tương tự như vậy. Và đôi khi chúng tôi có nói
Maybelà một kiểu, nhưng không có ý như vậy, bởi ai cũng biết
Maybelà một constructor kiểu. Khi áp dụng một kiểu phụ thêm vào
Maybe, như
Maybe String, thì ta có một kiểu cụ thể. Bạn biết đó, các giá trị chỉ có thể thuộc những kiểu cụ thể! Vì vậy chốt lại là, hãy sống “gấp”, yêu nhiệt tình và đừng để ai dùng chung đồ của bạn! [don’t let anybody else use your comb!]
Cũng như việc ta có thể áp dụng hàm theo từng phần để thu được hàm mới, ta có thể áp dụng các tham số kiểu theo từng phần và từ đó thu được các constructor kiểu. Cũng như việc gọi một hàm với không đủ tham số nhằm tạo ra một hàm mới, ta có thể chỉ định một constructor kiểu với không đủ tham số kiểu và nhận lại một constructor kiểu áp dụng theo từng phần. Nếu muốn một kiểu biểu thị cho ánh xạ (từ
Data.Map) từ số nguyên đến một thứ nào đó, ta có thể viết thế này:
type IntMap v = Map Int v
hoặc thế này:
type IntMap = Map Int
Bằng cách nào trong số hai cách trên thì constructor kiểu
IntMap cũng sẽ nhận một tham số và đó là kiểu mà các số nguyên sẽ chỉ tới.
*Được rồi*. Nếu bạn định thử viết mã lệnh minh họa tính năng trên, bạn có thể sẽ phải nhập chọn lọc
Data.Map. Khi bạn viết lệnh nhập chọn lọc, các constructor kiểu cũng phải có tên module đứng phía trước. Vì vậy bạn cần viết
type IntMap = Map.Map Int.
Hãy đảm bảo chắc rằng bạn thật sự hiểu được khác biệt giữa constructor kiểu và constructor giá trị. Chỉ vì ta đã tạo nên một kiểu tương đồng có tên
IntMap hay
AssocList thì điều đó không có nghĩa rằng ta có thể viết kiểu như
AssocList [(1,2),(4,5),(7,9)]. Tất cả việc tạo kiểu tương đồng nghĩa là ta có thẻ tham chiếu đến kiểu của nó bằng những tên gọi khác nhau. Ta có thể viết
[(1,2),(3,5),(8,9)] :: AssocList Int Int; mã lệnh này sẽ làm cho các số bên trong giả định rằng chúng nhận kiểu
Int, nhưng ta vẫn có thể dùng danh sách đó như mọi danh sách bình thường chứa các cặp số nguyên. Kiểu tương đồng (và các kiểu dữ liệu nói chung) chỉ có thể được dùng trong phần kiểu của Haskell, tức là mỗi khi ta định nghĩa cá kiểu mới (trong các lời khai báo
*data* và *type*) hoặc khi được định vị sau dấu
::. Dấu
:: xuất hiện trong lời khai báo kiểu hoặc chú thích kiểu.
Một kiểu dữ liệu đẹp khác mà nhận vào tham số là hai kiểu chính là kiểu
Either a b. Nó được định nghĩa gần như sau:
data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show)
Nó có hai constructor giá trị. Nếu
Left được dùng đến thì nội dung của nó sẽ thuộc kiểu
a và nếu
Right được dùng đến thì nội dung của nó sẽ thuộc kiểu
b. Vì vậy ta có thể dùng kiểu này để bao bọc một giá trị thuộc kiểu này hoặc kiểu khác rồi sau đó khi ta nhận một giá trị thuộc kiểu
Either a b, ta thường khớp mẫu với cả
Left lẫn
Right và nhận các thứ khác nhau tùy theo nó là cái nào.
ghci> Right 20 Right 20 ghci> Left "w00t" Left "w00t" ghci> :t Right 'a' Right 'a' :: Either a Char ghci> :t Left True Left True :: Either Bool b
Đến giờ, ta đã thấy rằng
Maybe a chủ yếu được dùng để biểu thị cho kết quả tính toán thành hoặc bại. Nhưng đôi khi
Maybe a không vẫn là chưa đủ vì
Nothing thực sự chưa truyền đạt nhiều thông tin ngoài việc báo rằng có gì đã hỏng hóc. Điều này phù hợp với các hàm có thể thất bại theo một cách duy nhất, hoặc chỉ đơn giản là ta không quan tâm đến nguyên nhân hàm đó thất bại. Một hàm tra tìm
Data.Map thất bại chỉ khi các khóa được tìm không nằm trong ánh xạ, vì vậy ta biết chắc điều gì đã xảy ra. Tuy nhiên, khi ta muốn biết một hàm thất bại theo cách nào, hoặc tại sao, thì ta thường dùng kiểu kết quả là
Either a b, trong đó
a là một kiểu dữ liệu nào đó mà cho ta biết nguyên nhân thất bại có thể xảy ra còn
b là một dạng tính toán thành công. Vì vậy, các lỗi dùng đến constructor giá trị
Left còn kết quả thì dùng đến
Right.
Ví dụ: một trường học nọ có các ngăn khóa để cho học sinh để các poster “Guns’n’Roses”. Mỗi ngăn khóa có một tổ hợp mã số. Khi một học sinh muốn có ngăn khóa mới, học sinh đó phải báo cho thầy quản lý biết số ngăn khóa mong muốn và người thầy sẽ giao mã số cho học sinh. Tuy nhiên, nếu đã có học sinh khác dùng ngăn khóa này, thì không thể dùng mã số cũ mà phải lựa lấy một mã số khác đi. Ta sẽ dùng một ánh xạ từ
Data.Map để biểu thị các ngăn khóa. Nó sẽ ánh xạ từ số ngăn khóa đến một cặp gồm thông tin rằng ngăn khóa được dùng chưa và mã số của ngăn.
import qualified Data.Map as Map data LockerState = Taken | Free deriving (Show, Eq) type Code = String type LockerMap = Map.Map Int (LockerState, Code)
Đơn giản. Ta giới thiệu một kiểu dữ liệu mới biểu diễn cho việc ngăn kéo được dùng rồi hay còn trống và ta tạo một kiểu tương đồng cho mã số ngăn khóa. Ta cũng tạo kiểu tương đồng với kiểu ánh xạ từ số nguyên đến cặp tình trạng ngăn khóa và mã số. Còn bây giờ, ta sẽ lập một hàm tìm kiếm mã số trong ánh xạ. Ta sẽ dùng một kiểu
Either String Code để biểu diễn cho kết quả, vì việc tra tìm có thể thất bại theo hai cách — ngăn khóa có thể bị chiếm giữ rồi, trong trường hợp đó ta không thể nói mã số hoặc số ngăn khóa có thể hoàn toàn không tồn tại. Nếu việc tra tìm thất bại, ta sẽ chỉ dùng một
String để báo điều gì đã xảy ra.
lockerLookup :: Int -> LockerMap -> Either String Code lockerLookup lockerNumber map = case Map.lookup lockerNumber map of Nothing -> Left $ "Locker number " ++ show lockerNumber ++ " doesn't exist!" Just (state, code) -> if state /= Taken then Right code else Left $ "Locker " ++ show lockerNumber ++ " is already taken!"
Ta thực hiện việc tra tìm thông thường trong ánh xạ. Nếu nhận được một
Nothing, ta trả lại một giá trị có kiểu
Left String, nói rằng ngăn khóa hoàn toàn không tồn tại. Còn nếu ta tìm thấy thì ta sẽ kiểm tra thêm xem ngăn khóa bị chiếm giữ chưa. Nếu đúng, thì trả lại
Left nói rằng nó đã bị chiếm rồi. Nếu không, thì trả lại giá trị thuộc kiểu
Right Code, grong đó ta đưa học sinh mã số đúng của ngăn khóa. Thực ra đó là một
Right String, nhưng ta đã giới thiệu kiểu tương đồng đó để đưa ra những thông tin thêm vào lời khai báo kiểu. Sau đây là một ví dụ về ánh xạ này:
lockers :: LockerMap lockers = Map.fromList [(100,(Taken,"ZD39I")) ,(101,(Free,"JAH3I")) ,(103,(Free,"IQSA9")) ,(105,(Free,"QOTSA")) ,(109,(Taken,"893JJ")) ,(110,(Taken,"99292")) ]
Bây giờ hãy thử tra tìm một vài mã số ngăn khóa.
ghci> lockerLookup 101 lockers Right "JAH3I" ghci> lockerLookup 100 lockers Left "Locker 100 is already taken!" ghci> lockerLookup 102 lockers Left "Locker number 102 doesn't exist!" ghci> lockerLookup 110 lockers Left "Locker 110 is already taken!" ghci> lockerLookup 105 lockers Right "QOTSA"
Ta đã có thể dùng một
Maybe a để biểu thị cho kết quả nhưng khi đó ta sẽ không biết được lý do không nhận được mã số. Song bây giờ, ta có được thông tin về việc thất bại trong kết quả tra tìm.
### Cấu trúc dữ liệu đệ quy
Như ta đã thấy, một constructor trong một kiểu dữ liệu đại số có thể chứa vài (hoặc không có) trường và mỗi trường phải là kiểu dữ liệu cụ thể nào đó. Trên quan điểm này, ta có thể tạo ra các kiểu dữ liệu mà constructor có các trường cùng kiểu với nhau! Dùng kiểu này, ta có thể tạo ra những kiểu dữ liệu có tính đệ quy, trong đó mỗi giá trị thuộc kiểu nào đó sẽ chứa các giá trị cùng kiểu, đến lượt mình các giá trị con này lại chứa những giá trị khác cùng kiểu và cứ như vậy.
Hãy hình dung danh sách sau:
[5]. Đó là cách viết tiện dụng cho
5:[]. Vế trái của
: có một giá trị và vế phải có một danh sách. Và trong trường hợp này là danh sách rỗng. Bây giờ, danh sách
[4,5] thì thế nào? À, một cách viết tiện dụng khác mà ta phân tích được thành
4:(5:[]). Nhìn vào dấu
: thứ nhất, ta thấy rằng nó cũng có một phần tử ở vế trái và một danh sách (
5:[]) ở vế phải. Tương tự với một danh sách như
3:(4:(5:6:[])), vốn có thể viết hoặc như vậy hoặc là như
3:4:5:6:[] (vì phép toán
: có tính kết hợp về bên phải) hoặc
[3,4,5,6].
Ta có thể nói rằng một danh sách có thể là danh sách rỗng hoặc là một phần tử nối bằng một
: đến danh sách khác (bản thân danh sách này có thể rỗng hoặc không).
Ta hãy dùng kiểu dữ liệu đại số để lập ra danh sách cho riêng mình!
data List a = Empty | Cons a (List a) deriving (Show, Read, Eq, Ord)
Mã lệnh này đọc như lời định nghĩa ta viết với danh sách ở đoạn trên. Nó hoặc là danh sách rỗng, hoặc một tổ hợp của phần đầu gồm một giá trị và một danh sách. Nếu cảm thấy lúng túng về vấn đề này, có thể bạn sẽ dễ hiểu hơn theo cách bản ghi.
data List a = Empty | Cons { listHead :: a, listTail :: List a} deriving (Show, Read, Eq, Ord)
Bạn cũng có thể thấy lúng túng về constructor
Cons ở đây.
*cons* là một tên gọi khác của
:. Bạn thấy đấy, trong danh sách,
: thực ra chỉ là một constructor nhận vào một giá trị và một danh sách khác rồi trả lại một dánh ách. Ta có thể dùng được kiểu danh sách mới lập rồi! Nói cách khác, nó gồm có hai trường. Một trường có kiểu
a còn trường kia có kiểu
[a].
ghci> Empty Empty ghci> 5 `Cons` Empty Cons 5 Empty ghci> 4 `Cons` (5 `Cons` Empty) Cons 4 (Cons 5 Empty) ghci> 3 `Cons` (4 `Cons` (5 `Cons` Empty)) Cons 3 (Cons 4 (Cons 5 Empty))
Ta gọi constructor
Cons theo cách toán tử trung tố vì vậy bạn có thể thấy bằng cách nào mà nó chỉ giống như
:.
Empty thì giống
[] và
4 `Cons` (5 `Cons` Empty) thì giống
4:(5:[]).
Ta có thể định nghĩa hàm tự động theo kiểu trung tố bằng cách đạt tên nó chỉ bằng những kí tự đặc biệt. Ta cũng có thể làm điều tương tự với các constructor, vì chúng chỉ là các hàm trả về một kiểu dữ liệu. Vì vậy hãy kiểm tra đoạn mã lệnh này.
infixr 5 :-: data List a = Empty | a :-: (List a) deriving (Show, Read, Eq, Ord)
Trước hết, ta nhận thấy một cấu trúc cú pháp mới, đó là lời khai báo định tố. Khi ta định nghĩa các hàm dưới dạng toán tử, thì ta có thể dùng khai báo đó để gán cho hàm này một kiểu định tố (nhưng không nhất thiết phải làm điều này). Một định tố quy định xem toán tử có mức ưu tiên tính toán đến đâu và xem phép tính kết hợp về phía trái hay phải. Chẳng hạn, định tố của
* là
infixl 7 * và của
+ là
infixl 6. Điều này nghĩa là cả hai phép toán đều kết hợp trái (
4 * 3 * 2 bằng
(4 * 3) * 2) nhưng
* có mức độ ưu tiên cao hơn là
+, và vì vậy,
5 * 4 + 3 bằng
(5 * 4) + 3.
Quay về ví dụ hiện tại, ta chỉ cần viết
a :-: (List a) thay vì
Cons a (List a). Bây giờ, ta có thể viết các danh sách thuộc kiểu danh sách mới lập như sau:
ghci> 3 :-: 4 :-: 5 :-: Empty (:-:) 3 ((:-:) 4 ((:-:) 5 Empty)) ghci> let a = 3 :-: 4 :-: 5 :-: Empty ghci> 100 :-: a (:-:) 100 ((:-:) 3 ((:-:) 4 ((:-:) 5 Empty)))
Khi suy diễn
Show từ kiểu hiện tại, Haskell sẽ vẫn sẽ hiển thị nó như thể constructor là một hàm tiền tố, vì vậy có cặp ngoặc tròn quanh toán tử (hãy nhớ lại rằng,
4 + 3 cũng như
(+) 4 3).
Ta hãy tạo một hàm để nối hai danh sách ta vừa lập lại với nhau. Với các danh sách thường, ta có toán tử
++ đảm nhiệm việc này:
infixr 5 ++ (++) :: [a] -> [a] -> [a] [] ++ ys = ys (x:xs) ++ ys = x : (xs ++ ys)
Như vậy ta sẽ chỉ mượn mã lệnh đó để viết cho danh sách ta vừa lập. Ta sẽ đặt tên hàm mới là
.++.
infixr 5 .++ (.++) :: List a -> List a -> List a Empty .++ ys = ys (x :-: xs) .++ ys = x :-: (xs .++ ys)
Và xem nó làm việc ra sao …
ghci> let a = 3 :-: 4 :-: 5 :-: Empty ghci> let b = 6 :-: 7 :-: Empty ghci> a .++ b (:-:) 3 ((:-:) 4 ((:-:) 5 ((:-:) 6 ((:-:) 7 Empty))))
Rất đẹp. Nếu muốn, ta có thể lập tất cả các hàm hoạt động với danh sách thường để hoạt động với kiểu danh sách ta vừa tạo.
Lưu ý cách mà ta khớp mẫu với
(x :-: xs). Cách này phát huy tác dụng vì khớp mẫu thực ra là khớp các constructor. Ta có thể khớp với
:-: vì nó là một constructor dành cho kiểu danh sách của ta, đồng thời ta cũng có thể khớp với
: vì nó là constructor dành cho kiểu danh sách có sẵn. Tương tự với
[]. Bởi vì khớp mẫu (chỉ) làm việc với các constructor, nên ta có thể khớp với tất cả những thứ như vậy, các constructor tiền tố thông thường hoặc kiểu như
8 hoặc
'a', vốn về cơ bản lần lượt là các constructor đối với những kiểu số và kí tự.
Bây giờ, ta sẽ lập một
*cây tìm kiếm nhị phân*. Nếu bạn thạo dùng cây tìm kiếm nhị phân trong những ngôn ngữ như C, thì sau đây sẽ là lời giải thích: cây tìm kiếm gồm một phần tử chỉ đến hai phần tử khác, một bên trái nó và một bên phải nó. Phần tử bên trái thì nhỏ hơn, phần tử bên phải thì lớn hơn. Mỗi phần tử trong số này lại có thể chỉ đến hai phần tử khác (hoặc một phần tử, hoặc không chỉ đến phần tử nào). Hệ quả là mỗi phần tử có tới hai cây con. Và một điều hay về cây tìm kiếm nhị phân là ta biết rằng tất ccar những phần tử trên cây con bên trái của, chẳng hạn như, 5 thì sẽ nhỏ hơn 5. Các phần tử ở cây con bên phải sẽ lớn hơn. Vì vậy nếu ta cần tìm xem liệu 8 có trên cây đang xét không, thì ta có thể bắt đầu từ 5 và vì 8 thì lớn hơn 5, ta sẽ đi về phía phải. Bây giờ ta đang ở 7 và vì 8 vẫn lớn hơn 7 nên ta lại đi về phía phải. Và ta đã tìm thấy phần tử chỉ sau ba lần nhảy! Bây giờ, giả sử như việc tìm kiếm được thực hiện trên danh sách (hoặc một cây không cân đối) thì ta sẽ mất bảy lần nhảy thay vì chỉ ba lần để tìm xem liệu 8 có nằm trên cây không.
Tập hợp và ánh xạ từ
Data.Set và
Data.Map được lập bằng các cây, chỉ khác ở chỗ thay vì cây tìm kiếm nhị phân thông thường, chúng sử dụng các cây tìm kiếm nhị phân cân đối. Nhưng hiện giờ ta sẽ chỉ lập cây tìm kiếm nhị phân thông thường.
Sau đây là điều chúng tôi muốn nói: một cây hoặc là một cây rỗng, hoặc là một phần tử có chứa một giá trị nào đó cùng với hai cây. Nghe thật phù hợp với một kiểu dữ liệu đại số!
data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show, Read, Eq)
Được, điều này rất tốt. Thay vì dựng một cây bằng cách thủ công, ta sẽ tạo một hàm nhận vào một câ và một phần tử rồi gài phần tử lên cây. Ta có thể làm điều này bằng cách so sánh giá trị cần gài vào với giá trị tại điểm nút gốc cây và nếu nó nhỏ hơn, ta sẽ đi về phía trái; nếu lớn hơn, ta sẽ đi về phía phải. Tương tự với từng giá trị tiếp theo tới khi ta đạt đến cây rỗng. Một khi đến được cây rỗng, ta chỉ việc gài điểm nút với giá trị đó thay vào cây rỗng này.
Trong các ngôn ngữ như C, ta làm điều này bằng cách thay đổi con trỏ và giá trị của cây. Trong Haskell, thực ra thì ta không thể sửa đổi cây được, vì vậy ta phải tạo ra một cây con mới mỗi lần quyết định xem cần đi về phái trái hoặc phải và cuối cùng, hàm đảm nhiệm việc gài phần tử sẽ trả lại một cây hoàn toán mới, vì Haskell thực sự không có khái niệm con trỏ, mà chỉ có giá trị. Vì vậy kiểu của hàm gài sẽ có dạng như
a -> Tree a - > Tree a. Nó nhận một phần tử và một cầy rồi trả lại một cây mới có phần tử này ở trên đó. Điều này nghe có vẻ không hiệu quả nhưng tính lười biếng của Haskell sẽ phụ trách vấn đề.
Vậy sau đây là hai hàm cần viết. Một là hàm ứng dụng để tạo ra một cây đơn lẻ (cây chỉ có một điểm nút) và một hàm để gài một phần tử lên cây.
singleton :: a -> Tree a singleton x = Node x EmptyTree EmptyTree treeInsert :: (Ord a) => a -> Tree a -> Tree a treeInsert x EmptyTree = singleton x treeInsert x (Node a left right) | x == a = Node x left right | x < a = Node a (treeInsert x left) right | x > a = Node a left (treeInsert x right)
Hàm
singleton chỉ là một cách viết tắt để tạo ra một nút có chứa thứ gì đó và rồi trả lại hai cây con rỗng. Trong hàm gài, đầu tiên là ta có điều kiện biên viết dưới dạng mẫu. Nếu ta đã đạt tới một cây con rỗng, điều này có nghĩa là ta đã ở nơi cần đến và chèn một cây đơn lẻ với phần tử quan tâm thay vào chỗ cây rống. Nếu không chèn vào một cây rỗng, thì ta phải kiểm tra điều gì đó. Trước hết, nếu phần tử định chèn bằng đúng phần tử ở điểm gốc cây, thì trả lại đúng cây ban đầu. Nếu nó nhỏ hơn, thì trả lại một cây có cùng giá trị điểm gốc, cùng cây con bên phải nhưng thay vì cây con bên trái, thì gài giá trị quan tâm vào nó. Tương tự (nhưng theo chều ngược lại) nếu giá trị quan tâm lớn hơn giá trị phần tử gốc.
Tiếp theo, ta sẽ tạo một hàm kiểm tra xem nếu phần tử nào đó có trên cây hay không. Trước hết, hãy định nghĩa điều kiện biên. Nếu ta cần tìm một phần tử trên cây rỗng, thì rõ ràng không có. Được rồi. Ta nhận thấy cách này giống với điều kiện biên khi tìm kiếm phần tử trong danh sách. Nếu ta tìm moojot phần tử trong danh sách rỗng, nó sẽ không có ở đó. Dù sao chăng nữa, nếu ta không tìm kiếm một phần tử trên một cây rỗng thì ta sẽ kiểm tra một số thứ. Nếu phần tử ở điểm gốc cây chính là giá trị ta cần tìm thì quá tốt! Còn nếu không thì sao? À, ta có thể lợi dụng thông tin là các phần tử về phía trái đều nhỏ hơn phần tử ở điểm gốc. Vì vậy nếu phần tử ta cần tìm nhỏ hơn giá trị ở điểm gốc thì hãy kiểm tra để xem nó có ở cây con bên trái không. Nếu lớn hơn thì hãy kiểm tra xem nó có ở cây con bên phải không.
treeElem :: (Ord a) => a -> Tree a -> Bool treeElem x EmptyTree = False treeElem x (Node a left right) | x == a = True | x < a = treeElem x left | x > a = treeElem x right
Tất cả những điều ta phải làm là viết mã lệnh cho cách làm được trình bày ở trên. Hãy nghịch chơi với cây ta vừa dựng nên! Thay vì dựng theo cách thủ công (dù ta có thể làm được), ta sẽ dùng một hàm gấp để dựng cây từ một danh sách. Hãy nhớ rằng, gần như mọi thứ với duyệt danh sách theo phần tử rồi trả lại một giá trị đều có thể lập được bằng một hàm gấp! Ta sẽ bắt đầu với một cây rỗng và rồi tiếp cận một danh sách từ bên phải và chỉ việc liền tiếp cài những phần tử lên cây tích lũy.
ghci> let nums = [8,6,4,1,7,3,5] ghci> let numsTree = foldr treeInsert EmptyTree nums ghci> numsTree Node 5 (Node 3 (Node 1 EmptyTree EmptyTree) (Node 4 EmptyTree EmptyTree)) (Node 7 (Node 6 EmptyTree EmptyTree) (Node 8 EmptyTree EmptyTree))
Trong hàm
foldr đó,
treeInsert là hàm gấp (nó nhận vào một cây và một phần tử danh sách rồi tạo ra một cây mới) và
EmptyTree là cây tích lũy ban đầu.
nums dĩ nhiên là danh sách mà ta sẽ thực hiện gấp.
Khi ta in nội dung cây ra màn hình thì kết qủa sẽ không dễ đọc, nhưng nếu cố gắng, ta sẽ hình dung được cấu trúc của nó. Ta thấy rằng giá trị ở điểm gốc bằng 5 và nó có 2 cây con, trong đó một có điểm gốc bằng 3 và một có điểm gốc bằng 7, v.v.
ghci> 8 `treeElem` numsTree True ghci> 100 `treeElem` numsTree False ghci> 1 `treeElem` numsTree True ghci> 10 `treeElem` numsTree False
Việc kiểm tra xem phần tử có thuộc cây hay không cũng diễn ra trôi chảy. Tuyệt.
Vậy như bạn có thể thấy, các cấu trúc dữ liệu đại số thực sự là một khái niệm mạnh và hay trong Haskell. Ta có thể dùng chúng để tạo ra bất kì thứ gì từ những giá trị boole và liệt kê các ngày thứ trong tuần cho đến các cây tìm kiếm nhị phân và còn hơn thế nữa!
### Lớp chứa kiểu (nâng cao)
Đến giờ, ta đã học được một số lớp chuẩn của Haskell và đã thấy có những kiểu nào có trong lớp đó. Ta cũng học cách tự động tjao ra những thực thể kiểu riêng trong các lớp chuẩn bằng cách đề nghị Haskell giúp ta suy diễn các thực thể. Trong mục này, ta sẽ học cách tạo nên những lớp riêng và cách thủ công để lập ra các thực thể thuộc lớp đó.
Ôn lại về khái niệm lớp: Lớp (hay lớp chứa kiểu) giống như các giao diện. Một lớp định nghĩa hành vi nào đó (như so sánh ngang bằng, so sánh hơn kém/thứ tự, liệt kê) rồi đến những kiểu có hành vi như vậy được lập thành thực thể của lớp đó. Hành vi của lớp đạt được bằng cách định nghĩa hàm hoặc chỉ là định nghĩa kiểm mà chúng lập nên. Vì vậy khi ta nói một kiểu là thực thể của một lớp, ý ta muốn nói là có thể dùng các hàm mà lớp đó định nghĩa với kiểu nói trên.
Lớp trong Haskell chẳng có gì liên quan đến lớp trong những ngôn ngữ như Java hoặc Python. Điều này có thể khiến nhiều người nhầm lẫn, vì vậy lúc này tôi muốn bạn quên đi tất cả những gì bạn đã biết về lớp trong những ngôn ngữ mệnh lệnh.
Chẳng hạn, lớp
Eq dành cho những thứ có thể so sánh ngang bằng được. Lớp này định nghĩa các hàm
== và
/=. nếu ta có một kiểu (chẳng hạn như
Car [xe hơi]) và việc so sánh hai xe hơi bằng một hàm ngang bằng
== là có lý, thì việc coi
Car là thực thể của
Eq cũng là hợp lý.
Dưới đây là cách mà lớp
Eq được định nghĩa trong module Prelude chuẩn:
class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool x == y = not (x /= y) x /= y = not (x == y)
Oa! Ở đây có kiểu cú pháp và từ khóa mới! Đừng lo, tất cả sẽ rõ ngay thôi. Trước hết, khi viết
class Eq a where, điều này nghĩa là ta định nghĩa một lớp mới và nó có tên là
Eq. Ở đây
a là một biến kiểu và nó có nghĩa là
a sẽ đóng vai trò kiểu mà ta sẽ sớm khiến nó thành một thực thể của lớp
Eq. Nó không nhất thiết phải được gọi là
a, nó không nhất thiết phải là một chữ cái, chỉ cần là một từ viết chữ thường. Sau đó, ta định nghĩa một vài hàm. Không cần thiết phải viết mã lệnh cho phần thân hàm, mà ta chỉ cần chỉ định khai báo kiểu cho các hàm này.
class Eq equatable wherevà rồi viết những khai báo kiểu dữ liệu như
(==) :: equatable -> equatable -> Bool.
Dù sao chúng ta
*đã* lập các phần thân của các hàm mà lớp
Eq định nghĩa, chỉ khác là ta định nghĩa chúng dưới dạng đệ quy tương hỗ. Ta nói rằng hai thực thể của
Eq bằng nhau nếu chúng không khác và chúng sẽ khác nhau nếu không bằng. Thực ra không nhất thiết phải viết như vậy nhưng ta đã làm, và sẽ sớm thấy được ích lợi của cách viết này.
class Eq a wherevà rồi định nghĩa một khai báo kiểu bên trong lớp đó như
(==) :: a -> -a -> Bool, thì sau này khi ta kiểm tra kiểu của hàm trên, nó sẽ có kiểu
(Eq a) => a -> a -> Bool.
Vì vậy một khi ta đã có một lớp, thì ta có thể làm gì với nó? À, thực sự không làm được gì nhiều. Nhưng một khi ta bắt đầu tạo các thực thể kiểu thuộc lớp đó, thì ta bắt đầu thu nhận được những tính năng hay. Vì vậy, hãy kiểm tra kiểu sau đây:
data TrafficLight = Red | Yellow | Green
Nó xác định các trạng thái của một ngọn đèn giao thông. Có thể nhận thấy rằng cách mà ta không suy diễn bất kì thực thể kiểu gì từ nó. Đó là vì ta sẽ tự dựng nên một số các thực thể, mặc dù có thể suy diễn ra chúng như trong trường hợp các kiểu
Eq và
Show. Sau đây là cách mà ta khiến nó trở thành thực thể của lớp
Eq.
instance Eq TrafficLight where Red == Red = True Green == Green = True Yellow == Yellow = True _ == _ = False
Ta thực hiện điều này bằng cách dùng từ khóa
*instance* keyword. Như vậy *class* được dùng để định nghĩa các lớp mới và *instance* dùng để khiến cho các kiểu ta lập ra trở thành thực thể của lớp. Khi định nghĩa
Eq, ta đã viết
class Eq a where và nói rằng
a đóng vai trò của bất cứ kiều gì sẽ được trở thành thực thể về sau này. Ở đây, ta có thể thấy điều đó rõ hơn, vì khi tạo ra một thực thể, ta viết
instance Eq TrafficLight where. Ta thay thế
a bằng kiểu dữ liệu thực sự.
Vì
== được định nghĩa theo
/= và ngược lại trong lời khai báo
*lớp*, nên ta chỉ phải thay đổi một trong hai định nghĩa trên trong lời khai báo thực thể. Việc này được gọi là định nghĩa trọn vẹn tối thiểu cho lớp đang xét — một số tối thiểu các hàm mà ta cần lập nên sao cho kiểu được thiết lập có hành vi giống như đã được lớp giới thiệu. Để hoàn thành định nghĩa trọn vẹn tối thiểu cho
Eq, ta phải viết mã lệnh thay thế cho một trong hai
== hoặc
/=. Nếu
Eq đơn giản được định nghĩa như sau:
class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool
thì ta sẽ phải viết mã lệnh cho cả hai hàm này khi làm một kiểu trở thành thực thể của lớp, vì nếu không thì Haskell sẽ không biết là hai hàm nói trên có liên hệ ra sao. Cách định nghĩa trọn vẹn tối thiểu sẽ là: cả
== lẫn
/=.
Bạn có thể thấy rằng chúng ta viết lệnh cho
== chỉ đơn giản là khớp mẫu. Vì còn nhiều trường hợp nữa trong đó hai ngọn đèn giao thông không sáng giống nhau, ta đã chỉ định những trường hợp nào thì bằng nhau và sau đó đơn giản là một mẫu “bắt tất cả”, nói rằng nếu không thuộc vào các tổ hợp trên thì hai ngọn đèn sẽ không như nhau.
Đồng thời bằng cách thủ công, ta hãy làm cho kiểu này trở thành thực thể của
Show. Để thỏa mãn định nghĩa trọn vẹn tối thiểu cho
Show, ta chỉ phải viết mã lệnh cho hàm
show của nó, hàm này nhận một giá trị và biến nó thành chuỗi.
instance Show TrafficLight where show Red = "Red light" show Yellow = "Yellow light" show Green = "Green light"
Một lần nữa, ta đã đạt được mục đích bằng cách dùng khớp mẫu. Hãy xem nó hoạt động ra sao:
ghci> Red == Red True ghci> Red == Yellow False ghci> Red `elem` [Red, Yellow, Green] True ghci> [Red, Yellow, Green] [Red light,Yellow light,Green light]
Tuyệt. Đáng ra có thể chỉ cần suy diễn
Eq và rồi nó cũng có hiệu quả tương tự (song vì để bạn tự học nên chúng tôi không làm vậy). Tuy nhiên, việc suy diễn
Show đã trực tiếp chuyển đổi constructor giá trị thành chuỗi. Nhưng nếu ta muốn các ngọn đèn được biểu diễn như
"Red light", thì ta phải tự tay khai báo thực thể.
Bạn cũng có thể tạo các lớp là lớp con của một lớp khác. Lời khai báo
*lớp* cho
Num hơi dài, nhưng đây là phần đầu của nó:
class (Eq a) => Num a where ...
Như ta đã đề cập từ trước, có nhiều chỗ mà ta có thể gài ràng buộc vào trong lớp. Vì vậy, điều này chẳng qua là giống như viết
class Num a where, chỉ khác là ta nói rằng kiểu dữ liệu mới lập
a phải là thực thể của
Eq. Điều cốt yếu ta nói là cần phải làm cho kiểu trở thành thực thể của
Eq trước khi ta có thể làm nó trở thành thực thể của
Num. Trước khi một kiểu nào đó có thể được coi là một con số thì ta phải xác định được là các giá trị thuộc kiểu đó có thể so sánh bằng/khác được không đã: điều này hoàn toàn hợp lý. Đó là tất cả nội dung của việc tạo lớp con, chỉ là một ràng buộc lớp trong lời khai báo
*lớp*! Khi định nghĩa phần thân hàm trong lời khai báo *lớp* hoặc khi định nghĩa chúng trong lời khai báo *thực thể*, ta được phép giả thiết rằng
a thuộc về
Eq và do đó có thể dùng
== đối với các giá trị thuộc kiểu này.
Nhưng còn về
Maybe hay những kiểu danh sách được làm cho trở thành thực thể của lớp thì sao? Điều khiến cho
Maybe trở nên khác so với, chẳng hạn,
TrafficLight là ở chỗ bản thân
Maybe không phải là một kiểu cụ thể, nó là constructor kiểu vốn nhận vào một tham số kiểu (như
Char hoặc thứ gì đó) và tạo ra một kiểu cụ thể (như
Maybe Char). Ta hãy xem xét lại lớp
Eq:
class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool x == y = not (x /= y) x /= y = not (x == y)
Từ các lời khai báo kiểu, ta thấy rằng
a được dùng như kiểu cụ thể vì tất cả các kiểu trong hàm phải là cụ thể (nhớ lại rằng bạn không thể có hàm thuộc kiểu
a -> Maybe nhưng bạn có thể có hàm
a -> Maybe a hoặc
Maybe Int -> Maybe String). Điều này lý giải tại sao ta không thể viết được như
instance Eq Maybe where ...
Vì như ta đã thấy, cái
a phải là kiểu cụ thể nhưng ở đây
Maybe lại không phải kiểu cụ thể. Nó là một constructor kiểu nhận vào một tham số và tạo ra một kiểu cụ thể. Cũng sẽ rất nhàm chán nếu viết
instance Eq (Maybe Int) where,
instance Eq (Maybe Char) where, v.v. với mọi kiểu khác nhau. Vì vậy ta có thể viết như sau:
instance Eq (Maybe m) where Just x == Just y = x == y Nothing == Nothing = True _ == _ = False
Điều này giống như nói rằng ta muốn làm cho tất cả những kiểu có dạng
Maybe something trở thành thực thể của
Eq.Thực chất, ta đã có thể viết
(Maybe something), nhưng ta thường chọn dùng một chữ cái để hợp với phong cách Haskell. Ở đây,
(Maybe m) đóng vai trò của
a từ
class Eq a where. Dù
Maybe không phải là một kiểu cụ thể, nhưng
Maybe m thì đúng vậy. Bằng cách chỉ định một tham số kiểu (
m, được viết chữ thường), ta nói rằng ta muốn tất cả những kiểu có dạng
Maybe m, trong đó
m là kiểu bất kì, trở thành thực thể của
Eq.
Tuy vậy, còn một vấn đề với cách làm trên. Bạn có phát hiện được không? Ta đã dùng
== đối với nội dung của
Maybe mà không hề có đảm bảo rằng thứ chứa trong
Maybe dùng được với
Eq! Đó là lý do mà ta phải sửa lại lời khai báo
*thực thể* như sau:
instance (Eq m) => Eq (Maybe m) where Just x == Just y = x == y Nothing == Nothing = True _ == _ = False
Ta đã phải thêm vào một ràng buộc lớp! Với lời khai báo
*thực thể* này, ta đã nói rằng: ta muốn tất cả những kiểu có dạng
Maybe m thuộc về lớp
Eq, nhưng chỉ những kiểu nào mà
m (thứ chứa trong
Maybe) cũng thuộc về
Eq. Thực tế nếu để Haskell đã suy diễn thực thể.
Trong đa số các trường hợp, ràng buộc lớp trong lời khai báo
*lớp* được dùng để làm cho một lớp trở thành lớp con của một lớp khác và và ràng buộc lớp trong lời khai báo *thực thể* được dùng để thể hiện yêu cầu về nội dung một kiểu nào đó. Chẳng hạn, ở đây ta yêu cầu nội dung của
Maybe cũng phải thuộc về lớp
Eq.
Khi tạo ra các thực thể, nếu bạn thấy rằng một kiểu được dùng như kiểu cụ thể trong lời khai báo kiểu (như
a trong
a -> a -> Bool), bạn phải cung cấp những tham số liểu và thêm cặp ngoặc tròn để thu được một kiểu cụ thể.
*lớp*. Cái
atrong
class Eq a wheresẽ được thay thế bằng một kiểu thực sự khi bạn tạo ra một thực thể, vì vậy cố gắng hình dung đặt cả kiểu đang xét vào trong một lời khai báo kiểu hàm.
(==) :: Maybe -> Maybe -> Boolkhông hợp lý lắm nhưng còn
(==) :: (Eq m) => Maybe m -> Maybe m -> Boolthì được. Tuy nhiên đây chỉ là điều cần tính đến, vì
==sẽ luôn có kiểu
(==) :: (Eq a) => a -> a -> Bool, bất kể các thực thể ta tạo ra có là gì đi nữa.
Ô, còn một điều nữa cần nhớm kiểm tra! Nếu bạn muốn biết các thực thể của một lớp là gì, chỉ cần gõ vào
:info YourTypeClass trong GHCI. Vì vậy, việc gõ
:info Num sẽ cho thấy những hàm mà lớp đã định nghĩa và nó sẽ cho bạn một danh sách các kiểu trong lớp.
:info cũng hoạt động được với các kiểu và constructor kiểu. Nếu bạn gõ vào
:info Maybe, GHCI sẽ cho bạn thấy tất cả những lớp mà
Maybe là một thực thể trong đó. Ngoài ra,
:info còn cho bạn thấy lời khai báo kiểu của một hàm. Tôi nghĩ rằng nó rất hay.
### Một lớp yes-no
Trong JavaScript và một số ngôn ngữ kiểu yếu khác, gần như là bạn có thể đặt bất kì thứ gì vào trong một biểu thức if. Chẳng hạn, bạn có thể viết tất cả những mã lệnh sau:
if (0) alert("YEAH!") else alert("NO!"),
if ("") alert ("YEAH!") else alert("NO!"),
if (false) alert("YEAH") else alert("NO!), v.v. và chúng đều phát ra thông báo
NO!. Nếu bạn viết
if ("WHAT") alert ("YEAH") else alert("NO!"), thì nó sẽ phát thông báo
"YEAH!" vì JavaScript coi rằng chuỗi không rỗng sẽ có djang một giá trị đúng (true).
Dù việc chỉ dùng
Bool trong ngữ cảnh cần biểu thức logic sẽ tốt hơn đối với Haskell, ta hãy thử lập theo hành vi này của JavaScript. Để cho vui thôi! Ta hãy bắt đàu bằng một lời khai báo
*lớp*.
class YesNo a where yesno :: a -> Bool
Thật đơn giản. Lớp
YesNo chỉ định nghĩa một hàm. Hàm này nhận vào một giá trị có kiểu được coi là mang khái niệm đúng-sai và báo cho ta biết cụ thể là giá trị này đúng hay sai. Có thể nhận thấy rằng, từ cách dùng
a trong hàm, thì
a phải là một kiểu cụ thể.
Tiếp theo, ta hãy định nghĩa một vài thực thể. Đối với các con số, ta sẽ giả sử rằng (như ở JavaScript) một số khác 0 bất kì là đúng và 0 là sai.
instance YesNo Int where yesno 0 = False yesno _ = True
Các danh sách rỗng (và mở rộng ra là chuỗi) là giá trị sai, còn danh sách không rỗng là giá trị đúng.
instance YesNo [a] where yesno [] = False yesno _ = True
Lưu ý cách ta đặt chỉ một tham số kiểu
a vào đó để làm cho danh sách trở thành kiểu cụ thể, mặc dù ta không giả định gì
về kiểu của các thành phần chứa trong danh sách. Còn gì nữa nhỉ, hừm, tôi biết rồi, bản thân
Bool có giá trị đúng-sai và điều này thì hiển hiên.
instance YesNo Bool where yesno = id
Hử? Cái
id là gì? Đó chỉ là một hàm trong thư viện chuẩn; hàm này nhận vào một tham số rồi trả lại chính thứ đó, vốn là thứ mà dù sao ta cũng viết vào đây.
Ta hãy làm cho
Maybe a cũng là một thực thể.
instance YesNo (Maybe a) where yesno (Just _) = True yesno Nothing = False
Ta không cần đến một ràng buộc về lớp vfi ta không giả thiết gì về nội dung của
Maybe. Ta chỉ nói rằng nó đúng nếu nó là một giá trị
Just và sai nếu nó là
Nothing. Ta vẫn sẽ phải viết rõ
(Maybe a) thay vì chỉ
Maybe vì nếu bạn thử nghĩ xem, một hàm
Maybe -> Bool không thể tồn tại (vì
Maybe không phải là kiểu cụ thể), còn
Maybe a -> Bool thì lại được. Dù vậy thì vẫn rất hay bởi bây giờ, bất kì kiểu nào có dạng
Maybe something đều thuộc về
YesNo và không phụ thuộc vào
something đó là cái gì.
Trước đây, ta có định nghĩa một kiểu
Tree a biểu diễn cho cây tìm kiếm nhị phân. Ta có thể nói rằng một cây rỗng là “sai” và bất kì cây nào không rỗng đều “đúng”.
instance YesNo (Tree a) where yesno EmptyTree = False yesno _ = True
Liệu một ngọn đèn giao thông có tương ứng với giá trị đúng hoặc sai không? Được chứ. Nếu đèn đỏ, bạn dừng. Nếu đèn xanh, bạn đi. Còn đèn vàng? À, với tôi thì thường là phóng tiếp vì tôi thuộc kiểu người thích adrenaline.
instance YesNo TrafficLight where yesno Red = False yesno _ = True
Được rồi, bây giờ ta khi đã có một vài thực thể, ta hãy nghịch chơi!
ghci> yesno $ length [] False ghci> yesno "haha" True ghci> yesno "" False ghci> yesno $ Just 0 True ghci> yesno True True ghci> yesno EmptyTree False ghci> yesno [] False ghci> yesno [0,0,0] True ghci> :t yesno yesno :: (YesNo a) => a -> Bool
Được rồi, nó đã hoạt động! Ta hãy lập một hàm phỏng theo câu lệnh if, nhưng hoạt động với các giá trị
YesNo.
yesnoIf :: (YesNo y) => y -> a -> a -> a yesnoIf yesnoVal yesResult noResult = if yesno yesnoVal then yesResult else noResult
Khá là dễ. Nó nhận vào một giá trị kiểu đúng-sai và hai thứ. Nếu cái đúng-sai kia nghiêng về đúng thì nó trả lại cái thứ nhất, còn không thì trả lại cái thứ hai.
ghci> yesnoIf [] "YEAH!" "NO!" "NO!" ghci> yesnoIf [2,3,4] "YEAH!" "NO!" "YEAH!" ghci> yesnoIf True "YEAH!" "NO!" "YEAH!" ghci> yesnoIf (Just 500) "YEAH!" "NO!" "YEAH!" ghci> yesnoIf Nothing "YEAH!" "NO!" "NO!"
### Lớp Functor
Đến giờ, ta đã bắt gặp nhiều lớp trong thư viện chuẩn. Ta đã nghịch với
Ord, được dùng với những thứ xếp thứ tự được. Ta đã nghịch với
Eq, vốn dành cho những thứ so sánh ngang bằng được. Ta đã thấy
Show, vốn biểu diễn một giao diện cho những kiểu có thể hiển thị được dưới dạng chuỗi. Người bạn tốt của ta,
Read luôn có mặt mỗi khi ta cần chuyển đổi một chuỗi sang thành giá trị thuộc một kiểu gì đó. Và bây giờ, ta sắp sửa xét đến lớp
Functor, về cơ bản vốn là dành cho những thứ có thể được ánh xạ trên đó. Có lẽ bạn sẽ nghĩ về danh sách, vì ánh xạ trên danh sách là “tuyệt chiêu” của Haskell. Và bạn đã đúng, kiểu danh sách thuộc về lớp
Functor.
Để tìm hiểu về lớp
Functor thì còn gì hay hơn là xem nó được thiết lập thế nào? Ta hãy thử ghé xem qua.
class Functor f where fmap :: (a -> b) -> f a -> f b
Được rồi. Ta thấy rằng nó định nghĩa một hàm,
fmap, và không cho ta bất kì cách thực hiện mặc định nào cho nó. Kiểu của
fmap rất thú vị. Trong số các định nghĩa lớp ta gặp đến giờ, biến kiểu đóng vai trò là kiểu trong lớp luôn là kiểu cụ thể, như
a trong
(==) :: (Eq a) => a -> a -> Bool. Nhưng giờ đây,
f không phải là một kiểu cụ thể (một kiểu mà giá trị có thể nắm giữ, như
Int,
Bool hoặc
Maybe String), mà là một constructor kiểu nhận vào một tham số kiểu. Một ví dụ nhanh chóng để ôn lại:
Maybe Int là một kiểu cụ thể, còn
Maybe là một constructor kiểu chỉ nhận vào một kiểu với vai trò tham số. Dù sao thì ta cũng thấy rằng
fmap nhận vào một hàm từ một kiểu này sang một kiểu khác và một functor áp dụng với một kiểu rồi trả lại một functor được áp dụng với một kiểu khác.
Đừng lo nếu diều này nghe có vẻ dễ nhầm lẫn. Tất cả sẽ sớm được sáng tỏ khi ta điểm qua một vài ví dụ. Hừm, lời khai báo kiểu vừa rồi với
fmap gợi cho tôi nhớ về điều gì đó. Dấu ấn kiểu của
map là
map :: (a -> b) -> [a] -> [b], trong trường hợp bạn không biết.
A, hay đấy! Nó nhận vào một hàm từ một kiểu này sang một kiểu khác và một danh sách chứa một kiểu rồi trả lại một danh sách chứa kiểu khác. Bạn ơi, tôi nghĩ rằng ta đã có một functor! Thực ra,
map chỉ là một
fmap vốn chỉ làm việc trên danh sách. Đây là cách trong đó danh sách đóng vai trò thực thể của lớp
Functor.
instance Functor [] where fmap = map
Như vậy đó! Lưu ý rằng ta đã không viết
instance Functor [a] where, vì từ
fmap :: (a -> b) -> f a -> f b, ta thấy được rằng
f phải là một constructor kiểu, nhận vào một kiểu.
[a] đã sẵn là một kiểu cụ thể (của một danh sách với kiểu bất kì trong đó), còn
[] là một constructor kiểu nhận vào một kiểu và tạo ra các kiểu dữ liệu như
[Int],
[String] hoặc thậm chí cả
[[String]].
Bởi vì đối với danh sách,
fmap chỉ đơn thuần là
map nên ta nhận được kết quả hệt như vậy khi dùng chúng với danh sách.
map :: (a -> b) -> [a] -> [b] ghci> fmap (*2) [1..3] [2,4,6] ghci> map (*2) [1..3] [2,4,6]
Điều gì sẽ xảy ra khi ta
map hoặc
fmap lên một danh sách rỗng? Ồ, dĩ nhiên là ta thu được danh sách rỗng. Chỉ có điều là danh sách rỗng có kiểu
[a] biến thành danh sách rỗng có kiểu
[b].
Những kiểu dữ liệu nào đóng vai trò như cái hộp thì có thể thành functor. Bạn có thể hình dung một danh sách như một cái hộp có vô vàn những ngăn nhỏ và chúng có thể tất cả đều trống rỗng, hoặc là một ngăn đầy còn lại rỗng, hoặc là nhiều ngăn đầy. Như vậy, có những thuộc tinh nào khác ở một cái hộp như vậy? Một thuộc tính là kiểu
Maybe a. Ở một chừng mực nào đó, cũng giống như một cái hộp có thể không chứa gì, tương ứng với giá trị
Nothing, hoặc chỉ chứa một thứ, như
"HAHA", tương ứng với giá trị
Just "HAHA". Sau đây là cách mà
Maybe đóng vai trò functor.
instance Functor Maybe where fmap f (Just x) = Just (f x) fmap f Nothing = Nothing
Một lần nữa, hãy chú ý cách ta viết
instance Functor Maybe where thay vì
instance Functor (Maybe m) where, cũng như ta đã làm với
Maybe và
YesNo.
Functor cần một constructor kiểu, constructor này nhận vào một kiểu chứ không phải kiểu cụ thể. Nếu bạn thử tưởng tượng việc thay thế các
f với các
Maybe thì
fmap sẽ đóng vai trò của
(a -> b) -> Maybe a -> Maybe b ở kiểu cụ thể này; và cách này có vẻ ổn. Nhưng nếu bạn thay thế
f bằng
(Maybe m) thì dường như nó sẽ đóng vai trò của
(a -> b) -> Maybe m a -> Maybe m b, tức là chẳng có ý nghĩa gì bởi
Maybe chỉ nhận duy nhất một tham số kiểu.
Dù sao đi nữa, cách thiết lập
fmap là khá đơn giản. Nếu nó là một giá trị rỗng:
Nothing, thì chỉ cần trả lại một
Nothing. Nếu ta ánh xạ trên một hộp rỗng thì ta thu được một hộp rỗng. Có lý. Cũng như nếu ta ánh xạ trên một danh sách rỗng, ta sẽ nhận được danh sách rỗng. Nếu nó không phải một giá trị rỗng, mà là một giá trị đơn lẻ đặt trong
Just, thì ta áp dụng hàm kèm theo đối với nội dung của
Just.
ghci> fmap (++ " HEY GUYS IM INSIDE THE JUST") (Just "Something serious.") Just "Something serious. HEY GUYS IM INSIDE THE JUST" ghci> fmap (++ " HEY GUYS IM INSIDE THE JUST") Nothing Nothing ghci> fmap (*2) (Just 200) Just 400 ghci> fmap (*2) Nothing Nothing
Một thứ khác có thể được đặt qua ánh xạ và tạo một thực thể
Functor là kiểu
Tree a mà ta đã lập. Nó có thể được hình dung như một cái hộp (theo khía cạnh có thể giữ nhiều giá trị hoặc không giữ thứ gì) và constructor kiểu
Tree nhận vào đúng một tham số kiểu. Nếu bạn xét
fmap như thể nó là một hàm để dành riêng cho
Tree thì dấu ấn kiểu của nó sẽ là
(a -> b) -> Tree a -> Tree b. Ta sẽ dùng phép đệ quy đối với kiểu dữ liệu này. Việc ánh xạ trên một cây rỗng sẽ tạo ra một cây rỗng. Ánh xạ trên cây không rỗng sẽ tạo ra một cây trong đó hàm kèm theo sẽ áp dụng cho giá trị ở gốc cây, còn các cây con trái và cây con phải vẫn là những cây con trước đây, chỉ khác là sẽ được hàm ánh xạ lên chúng.
instance Functor Tree where fmap f EmptyTree = EmptyTree fmap f (Node x leftsub rightsub) = Node (f x) (fmap f leftsub) (fmap f rightsub)
ghci> fmap (*2) EmptyTree EmptyTree ghci> fmap (*4) (foldr treeInsert EmptyTree [5,7,3,2,1,7]) Node 28 (Node 4 EmptyTree (Node 8 EmptyTree (Node 12 EmptyTree (Node 20 EmptyTree EmptyTree)))) EmptyTree
Hay thật! Thế còn
Either a b thì sao? Có thể cho nó thành functor được không? Lớp
Functor cần một constructor kiểu chỉ nhận vào một tham số kiểu nhưng
Either lại nhận hai tham số. Hừm! Tôi biết rồi, chúng ta sẽ áp dụng
Either từng phần bằng cách chỉ cấp cho nó một tham số để nó còn một tham số tự do. Đây là cách làm cho
Either a là một functor trong thư viện chuẩn:
instance Functor (Either a) where fmap f (Right x) = Right (f x) fmap f (Left x) = Left x
Nào nào, chúng ta đã làm gì ở đây vậy? Bạn có thể thấy cách làm cho
Either a thành một thực thể thay vì chỉ là
Either. Đó là vì
Either a là một constructor kiểu nhận vào một tham số, trong khi
Either thì nhận vào hai tham số. Nếu
fmap được dành riêng cho
Either a thì dấu ấn kiểu đã là
(b -> c) -> Either a b -> Either a c vì như vậy thì gống với
(b -> c) -> (Either a) b -> (Either a) c. Trong đoạn mã lệnh thiết lập trên, ta đã ánh xạ trong trường hợp có constructor kiểu là
Right, nhưng lại không ánh xạ trong trường hợp
Left. Tại sao vậy? À, nếu nhìn trở lại cách định nghĩa
Either a b, ta sẽ thấy nó có dạng:
data Either a b = Left a | Right b
Như vậy, nếu ta muốn ánh xạ một hàm lên cả hai vế, thì
a và
b phải có cùng kiểu. Ý của tôi là nếu ta thử ánh xạ một hàm có nhận vào một chuỗi và trả lại một chuỗi, trong khi
b là chuỗi mà
a lại là con số thì sẽ không có tác dụng. Hơn nữa, từ việc thấy được kiểu của
fmap sẽ là gì khi nó chỉ hoạt động với các giá trị
Either, ta thấy rằng tham số thứ nhất phải giữ nguyên còn tham số thứ hai thay đổi được; và tham số thứ nhất thực tế là constructor giá trị
Left.
Điều này rất hợp với cách so sánh “cái hộp” nếu ta hình dung phần
Left có dạng như một hộp rỗng với một lời thông báo lỗi ghi bên cạnh để cho ta biết tại sao nó lại rỗng.
Các ánh xạ tạo bởi
Data.Map cũng có thể được thành functor vì chúng giữ giá trị (hoặc không giữ thứ gì!). Trong trường hợp
Map k v,
fmapsẽ ánh xạ một hàm
v -> v' trên một phép ánh xạ kiểu
Map k v rồi trả lại một ánh xạ kiểu
Map k v'.
'không có ý nghĩa gì đặc biệt đối với kiểu, tương tự như khi ta đặt tên các giá trị. Dấu này thường dùng để chỉ những thứ gần giống nhau, chỉ hơn thay đổi chút ít.
Hãy cố gắng tự hình dung xem bằng cách nào mà ta có thể làm cho
Map k trở thành thực thể của
Functor!
Với lớp
Functor, ta đã thấy bằng cách nào mà các lớp có thể biểu diễn khá hay những khái niệm cấp cao. Ta cũng đã thực hành thêm một chút với các kiểu dữ liệu áp dụng từng phần và với cách tạo thực thể. Trong một chương sách sau này, ta cũng sẽ xét đến một vài định luật áp dụng cho functor.
*Còn một điều nữa!*Functor cần tuân theo một số định luật để chúng được phép có những thuộc tính mà ta có thể dựa vào mà không phải nghĩ nhiều. Nếu dùng
fmap (+1)đối với danh sách
[1,2,3,4], ta sẽ dự tính kết quả là
[2,3,4,5]chứ không phải ngược lại,
[5,4,3,2]. Nếu ta dùng
fmap (\a -> a)(hàm đồng nhất, trả lại đúng các tham số nhận vào) đối với một danh sách nào đó, ta sẽ dự định nhận về đúng danh sách đó. Chẳng hạn, nếu ta đưa nhầm thực thể functor cho kiểu
Tree, dùng
fmapđối với một cây trong đó cây con bên trái của một điểm nút chỉ có những phần tử nhỏ hơn nút và cây con bên phải điểm nút chỉ có những phần tử lớn hơn điểm nút đó thì có thể sẽ tạo nên một cây mà tính chất này không còn đảm bảo nữa. Trong chương sau này của quyển sách ta sẽ đề cập chi tiết đến các định luật áp dụng cho functor.
### Dạng và kiểu kiếc nào đó
Constructor kiểu nhận vào tham số là những kiểu khác để rồi tạo ra kiểu cụ thể. Điều này làm tôi nhớ đến các hàm, chúng nhận vào tham số là những giá trị để tạo ra giá trị. Ta đã thấy rằng constructor kiểu có thể áp dụng được từng phần (
Either String là một kiểu, nó nhận vào một kiểu và tạo ra một kiểu cụ thể, như
Either String Int), giống như hàm. Điều đó thật là hay. Trong mục này, ta sẽ xét định nghĩa chính thức cách áp dụng kiểu cho constructor kiểu, cũng như ta xét định nghĩa chính thức cách áp dụng giá trị cho hàm bằng cách khai báo kiểu.
*Bạn không cần phải đọc mục này mới tiếp tục chinh phục được Haskell* và nếu bạn không hiểu nó thì đừng lo. Tuy nhiên, nắm được vấn đề này sẽ cho bạn hiểu biết tường tận về hệ thống kiểu.
Vì vậy những giá trị như
3,
"YEAH" hoặc
takeWhile (các hàm cũng là giá trị, vì ta có thể truyền chúng thoải mái) mỗi cái đều có kiểu riêng. Kiểu là những nhãn hiệu nhỏ nhắn mà mỗi giá trị mang theo để dựa vào đó ta có thể suy luận về giá trị của chúng. Nhưng bản thân kiểu lại còn có những nhãn hiệu nhỏ của riêng chúng, gọi là
*dạng* (kind). Một dạng thì ít nhiều giống như kiểu của kiểu. Điều này nghe hơi kì quặc và dễ gây nhầm lẫn, song thực ra đó là một khái niệm rất hay.
Dạng là gì và chúng dùng được vào việc gì? À, ta hãy xét dạng của một kiểu bằng cách dùng lệnh
:k trong GHCI.
ghci> :k Int Int :: *
Một dấu sao ư? Kì thật. Nó nghĩa là gì? Dấu
* nghĩa là kiểu dữ liệu là một kiểu cụ thể. Một kiểu cụ thể là kiểu mà nó không nhận bất kì tham số kiểu nào và giá trị chỉ có thể có kiểu nằm trong các kiểu cụ thể. Nếu tôi buộc phải đọc hẳn dấu
* ra (việc mà đến giờ tôi chưa làm), tôi sẽ đọc là
*sao* hoặc đơn giản chỉ là *kiểu*.
Được rồi, bây giờ hãy xem dạng của
Maybe là gì.
ghci> :k Maybe Maybe :: * -> *
Constructor kiểu
Maybe nhận vào một kiểu cụ thể (chẳng hạn,
Int) rồi trả lại một kiểu cụ thể, chẳng hạn
Maybe Int. Và đó chính là điều mà dạng này cho ta biết. Cũng như
Int -> Int có nghĩa là một hàm nhận vào một
Int và trả lại một
Int,
* -> * có nghĩa là constructor kiểu nhận vào một kiểu cụ thể và trả lại một kiểu cụ thể. Ta hãy áp dụng tham số kiểu cho
Maybe và để xem dạng của kiểu đó là gì.
ghci> :k Maybe Int Maybe Int :: *
Đúng như tôi dự đoán! Chúng ta áp dụng tham số kiểu cho
Maybe và thu lại một kiểu cụ thể (đó là điều mà
* -> * muốn nó). Một cách so sánh (mặc dù không tương đương, vì kiểu và dạng là khác nhau) với điều này là nếu ta viết
:t isUpper và
:t isUpper 'A'.
isUpper có kiểu là
Char -> Bool và
isUpper 'A' có kiểu là
Bool, vì giá trị của nó về cơ bản là
True. Tuy vậy cả hai kiểu nó trên đều có dạng là
*.
Ta đã dùng
:k đối với một kiểu để thu được dạng của nó, cũng như khi ta dùng
:t đối với một giá trị để thu được kiểu của nó. Như đã nói, kiểu là các nhãn hiệu gắn lên giá trị còn dạng là các nhãn hiệu gắn lên kiểu và có sự tương đồng giữa hai thứ.
Ta hãy xem một dạng khác.
ghci> :k Either Either :: * -> * -> *
À ha, điều này cho ta biết
Either nhận vào hai kiểu cụ thể làm tham số kiểu, để tạo ra một kiểu cụ thể. Nó cũng trông giống như một lời khai báo kiểu của một hàm nhận vào hai giá trị và trả lại một thứ gì đó. Các constructor kiểu được curry (cũng như đối với hàm), vì vậy ta có thể áp dụng chúng từng phần.
ghci> :k Either String Either String :: * -> * ghci> :k Either String Int Either String Int :: *
Khi ta muốn làm cho
Either thuộc về lớp
Functor, ta phải áp dụng nó từng phần vì
Functormuốn kiểu chỉ nhận một tham số trong khi
Either thì nhận hai. Nói cách khác,
Functor muốn dạng
* -> * và vì vậy ta phải áp dụng
Either từng phần để thu được một kiểu có dạng
* -> * thay vì kiểu ban đầu của nó,
* -> * -> *. Nếu ta nhìn vào định nghĩa của
Functor lần nữa
class Functor f where fmap :: (a -> b) -> f a -> f b
có thể thấy rằng biến kiểu
f được dùng như là kiểu nhận vào một kiểu cụ thể và tạo ra một kiểu cụ thể. Ta biết rằng nó phải tạo ra một kiểu cụ thể vì nó được dùng làm kiểu của một giá trị trong hàm. Và từ đó, ta có thể suy diễn rằng những kiểu nào muốn là bạn với
Functor thì phải có dạng
* -> *.
Bây giờ, ta hãy viết một kiểu linh tinh. Hãy nhìn vào lớp sau, mà tôi sắp chuẩn bị thiết lập:
class Tofu t where tofu :: j a -> t a j
Chà, trông có vẻ lạ. Bằng cách nào mà ta có thể tạo ra một kiểu là thực thể của lớp kì lạ này? À, hãy nhìn xem dạng của nó phải là gì. Bởi
j a được dùng làm kiểu của một giá trị mà hàm
tofu nhận vào làm tham số, nên
j a phải có dạng là
*. Ta giả sử rằng
a có dạng
* và từ đó ta có thể suy luận rằng
j phải có dạng
* -> *. Ta thấy rằng
t cũng phải tạo ra một giá trị cụ thể và rằng nó phải nhận hai kiểu. Đồng thời biết rằng
a có dạng
* và
j có dạng
* -> *, ta suy ra rằng
t phải có dạng
* -> (* -> *) -> *. Vì vậy nó nhận một kiểu cụ thể (
a), một constructor kiểu nhận vào một kiểu cụ thể (
j) rồi tạo ra một kiểu cụ thể. Ôi.
Được rồi, giờ ta hãy tạo một kiểu có dạng là
* -> (* -> *) -> *. Sau đây là một cách giải.
data Frank a b = Frank {frankField :: b a} deriving (Show)
Bằng cách nào mà ta biết được rằng kiểu này có dạng
* -> (* -> *) - > *? À, các trường trong kiểu trừu tượng (ADT) sinh ra là để giữ các giá trị, vì vậy hiển nhiên là chúng phải có dạng
*. Ta giả sử
a có dạng
*, điều này có nghĩa là
b nhận vào một tham số kiểu và vì vậy dạng của nó là
* -> *. Bây giờ khi đã biết dạng của cả hai
a và
b, và bởi vì chúng là các tham số cho
Frank, ta thấy rằng
Frank có dạng
* -> (* -> *) -> * Dấu
* thứ nhất biểu diễn cho
a còn
(* -> *) biểu diễn cho
b. Ta hãy tạo một số giá trị
Frank rồi kiểm tra kiểu của chúng.
ghci> :t Frank {frankField = Just "HAHA"} Frank {frankField = Just "HAHA"} :: Frank [Char] Maybe ghci> :t Frank {frankField = Node 'a' EmptyTree EmptyTree} Frank {frankField = Node 'a' EmptyTree EmptyTree} :: Frank Char Tree ghci> :t Frank {frankField = "YES"} Frank {frankField = "YES"} :: Frank Char []
Hừm. Bởi vì
frankField có kiểu theo hình thức là
a b, nên những giá trị của nó phải có kiểu cũng với hình thức tương tự. Vì vậy chúng có thể là
Just "HAHA", vốn có kiểu
Maybe [Char] hoặc nó có thể có giá trị
['Y','E','S'], vốn có kiểu
[Char] (còn nếu ta dùng kiểu danh sách tự lập ra, thì sẽ là
List Char). Và ta thấy rằng kiểu của các giá trị
Frank tương ứng với dạng của
Frank.
[Char] có dạng là
* còn
Maybe có dạng là
* -> *. Vì để có được một giá trị, nó phải là một kiểu cụ thể và vì vậy phải được áp dụng đầy đủ, mỗi giá trị của
Frank blah blaah đều có dạng
*.
Việc làm cho
Frank trở thành thực thể của
Tofu là rất đơn giản. Ta thấy rằng
tofu nhận vào một
j a (một kiểu ví dụ là
Maybe Int) rồi trả lại một
t a j. Vì vậy nếu ta thay thế
Frank bằng
j, kiểu của kết quả sẽ là
Frank Int Maybe.
instance Tofu Frank where tofu x = Frank x
ghci> tofu (Just 'a') :: Frank Char Maybe Frank {frankField = Just 'a'} ghci> tofu ["HELLO"] :: Frank [Char] [] Frank {frankField = ["HELLO"]}
Không có ích lắm, nhưng ta đã cho thấy sức mạnh của kiểu mới lập được. Ta hãy lập thêm một kiểu linh tinh khác. Ta có kiểu dữ liệu này:
data Barry t k p = Barry { yabba :: p, dabba :: t k }
Và bây giờ ta muốn làm cho nó trở thành thực thể của
Functor.
Functor cần các kiểu có dạng
* -> * nhưng dường như
Barry không có dạng này. Dạng của
Barry là gì? À, ta thấy nó nhận vào ba tham số kiểu, vì vậy nó sẽ là
something -> something -> something -> *. An toàn nhất là nói rằng
p là một kiểu cụ thể và do đó có dạng
*. Đối với
k, ta giả định là nó có dạng
* và suy ra,
t có dạng
* -> *. Bây giờ hãy thay thế các dạng này vào chỗ các
*something* mà ta đã tạm viết ở trên; ta thấy được rằng nó có dạng là
(* -> *) -> * -> * -> *. Hãy dùng GHCI để kiểm tra.
ghci> :k Barry Barry :: (* -> *) -> * -> * -> *
À, ta đã nhận định đúng. Thật là thỏa mãn. Bây giờ, để khiến cho kiểu này thuộc về
Functor ta phải áp dụng một cách từng phần hai tham số kiểu đầu để cho còn lại
* -> *. Điều đó có nghĩa rằng chỗ bắt đầu khai báo thực thể sẽ là:
instance Functor (Barry a b) where. Nếu ta coi
fmap như thể nó được tạo riêng dành cho
Barry, thì nó sẽ có kiểu là
fmap :: (a -> b) -> Barry c d a -> Barry c d b, vì ta chỉ thay thế
f của
Functor bằng
Barry c d. Tham số kiểu thứ ba từ
Barry sẽ phải thay đổi và ta thấy rằng nó thuận tiện như ở nhà.
instance Functor (Barry a b) where fmap f (Barry {yabba = x, dabba = y}) = Barry {yabba = f x, dabba = y}
Được rồi đấy! Ta vừa mới ánh xạ
f lên trường thứ nhất.
Trong mục này, ta đã xét kĩ về cách hoạt động của tham số kiểu và phần nào đã giải thích về mặt hình thức cùng với khái niệm “dạng”, như ta đã hình thức hóa tham số hàm với các lời khai báo hàm. Tuy nhiên, chúng là hai thứ hoàn toàn khác nhau. Thực tế, khi làm việc với Haskell, bạn chẳng cần bận tâm với kiểu và suy diễn kiểu một cách thủ công như ta đã làm. Thông thường, bạn chỉ cần áp dụng từng phần kiểu dữ liệu của bạn cho
* -> * hoặc
* khi làm cho nó thành một thực thể của một trong các lớp chuẩn; nhưng biết được nguyên nhân và cách thức hoạt động như vậy là một điều hay. Cũng thú vị khi thấy rằng bản thân kiểu lại có kiểu nhỏ hơn. Một lần nữa, bạn không cần phải hiểu hết toàn bộ nội dung ở đây trước khi tiếp tục; song nếu bạn đã hiểu cơ chế hoạt động của các dạng thì dường như bạn đã nắm vững hệ thống kiểu của Haskell.
## 3 responses to “
Chương 8: Tự lập nên các kiểu và lớp riêng”
Pingback: Tự học lấy Haskell | Blog của Chiến
Pingback: Chương 9: Đầu vào và đầu ra | Blog của Chiến
Pingback: Chương 11: Functor, Functor áp dụng và Monoid | Blog của Chiến

Link: <http://wp.me/p1kFN-iN>; rel=shortlink


markdown:
Hãy học cách vui thích với những gì bạn có trong khi theo đuổi tất cả những gì bạn muốn. (Learn how to be happy with what you have while you pursue all that you want. )Jim Rohn
Tôi chưa bao giờ học hỏi được gì từ một người luôn đồng ý với tôi. (I never learned from a man who agreed with me. )Dudley Field Malone
Hãy dang tay ra để thay đổi nhưng nhớ đừng làm vuột mất các giá trị mà bạn có.Đức Đạt-lai Lạt-ma XIV
Trong sự tu tập nhẫn nhục, kẻ oán thù là người thầy tốt nhất của ta. (In the practice of tolerance, one's enemy is the best teacher.)Đức Đạt-lai Lạt-ma XIV
Cách tốt nhất để tiêu diệt một kẻ thù là làm cho kẻ ấy trở thành một người bạn. (The best way to destroy an enemy is to make him a friend.)Abraham Lincoln
Kẻ hung dữ hại người cũng như ngửa mặt lên trời mà phun nước bọt. Nước bọt ấy chẳng lên đến trời, lại rơi xuống chính mình.Kinh Bốn mươi hai chương
Chúng ta có lỗi về những điều tốt mà ta đã không làm. (Every man is guilty of all the good he did not do.)Voltaire
Đừng chọn sống an nhàn khi bạn vẫn còn đủ sức vượt qua khó nhọc.Sưu tầm
Khởi đầu của mọi thành tựu chính là khát vọng. (The starting point of all achievement is desire.)Napoleon Hill
Vết thương thân thể sẽ lành nhưng thương tổn trong tâm hồn sẽ còn mãi suốt đời. (Stab the body and it heals, but injure the heart and the wound lasts a lifetime.)Mineko Iwasaki
Học vấn của một người là những gì còn lại sau khi đã quên đi những gì được học ở trường lớp. (Education is what remains after one has forgotten what one has learned in school.)Albert Einstein
[Trang chủ](Home.html) »»
[Danh mục](danh-muc_thu-vien_tongquat_rong-mo-tam-hon.html) »»
[Đại từ điển Hán Việt](tu-dien_han-viet_none_rong-mo-tam-hon.html) »»
Đang xem mục từ: thông - 匆 »»
**Chú ý**: Nếu quý vị copy chữ Hán dán (paste) vào đây để tra thì kết quả sẽ chính xác hơn rất nhiều.
Quý vị cũng có thể bôi đen (select) những chữ Hán đã hiển thị trên màn hình rồi dùng chuột kéo thả vào khung để tra.
Như chữ Thông 悤. (
*Từ điển Nguyễn Quốc Hùng*) **Quý vị đang truy cập từ IP 54.198.165.74 (918988106) và chưa ghi danh hoặc đăng nhập trên máy tính này. **Nếu là thành viên, quý vị chỉ cần đăng nhập một lần duy nhất trên thiết bị truy cập, bằng email và mật khẩu đã chọn.
Chúng tôi khuyến khích việc
**ghi danh thành viên **,để thuận tiện trong việc chia sẻ thông tin, chia sẻ kinh nghiệm sống giữa các thành viên, đồng thời quý vị cũng sẽ nhận được sự hỗ trợ kỹ thuật từ Ban Quản Trị trong quá trình sử dụng website này.
** Việc ghi danh là hoàn toàn miễn phí và tự nguyện**.
Ghi danh hoặc đăng nhập

WARC-Type: response

Content-Type: application/http; msgtype=response

WARC-Truncated: length

WARC-Identified-Payload-Type: text/html

Server: nginx/1.10.0 (Ubuntu)

Content-Type: text/html; charset=UTF-8

Connection: close


markdown:
THIẾT KẾ CƠ ĐIỆN GIÁ RẺ
Thứ Ba, 22 tháng 12, 2015
So sánh hàn hóa nhiệt và kẹp tiếp địa
So sánh hàn hóa nhiệt và kẹp tiếp địa
Ths.ks bùi kiến hòa _ 097.777.2018 _ chuyên gia tư vấn thiết kế điện nước - chia sẻ với các bạn vấn đề này.
Xem bài viết đầy đủ tại đây :
http://sdme.com.vn/news/so-sanh-han-cadwel-va-kep-tiep-dia-06112015.html
Được đăng bởi
sdme_ 097.777.2018_hcm
vào lúc
23:22
Gửi email bài đăng này
BlogThis!
Chia sẻ lên Twitter
Chia sẻ lên Facebook
Chia sẻ lên Pinterest
Nhãn:
sdme
,
thiet ke co dien
,
thiet ke dien nuoc
,
thiet ke me
Không có nhận xét nào:
Đăng nhận xét
Bài đăng Mới hơn
Bài đăng Cũ hơn
Trang chủ
Đăng ký:
Đăng Nhận xét (Atom)
## Không có nhận xét nào:
## Đăng nhận xét

Server: GSE

X-Content-Type-Options: nosniff

Cache-Control: private, max-age=0

X-XSS-Protection: 1; mode=block


markdown:
### Vô địch thế giới
Sau khi ghi hai bàn giúp đội tuyển Việt Nam đá bại đội Hàn Quốc với tỉ số 2-1 ở vòng bán kết, Công Phượng tiếp tục tỏa sáng với ba siêu phẩm cháy lưới đội tuyển Thái Lan ở trận chung kết, lần đầu tiên đem về chức vô địch cho đội tuyển Việt Nam. Nhóm phóng viên chúng tôi đã có mặt ở sân vận động Mỹ Đình để phỏng vấn các nhân vật liên quan.
Ông Đoàn Nguyên Đức, chủ tịch tập đoàn Hoàng Anh Gia Lai, hồ hởi nói với báo giới, "Cuối cùng cũng đến ngày được nhìn thấy lứa Công Phượng vô địch thế giới. Chưa bao giờ tôi thấy hạnh phúc như hôm nay. Hồi xưa nhìn tụi nhỏ chân không đá banh đâu có ai tin tôi khi tôi nói bọn chúng sẽ làm nên chuyện. Từ lúc bọn nó vô địch giải U21 Báo Thanh Niên năm nào tôi đã âm thầm chuẩn bị tất cả cho ngày hôm nay rồi. Tôi đã phải đàm phán với FIFA rất căng để đem giải vô địch thế giới về Việt Nam".
Chủ tịch VFF, ông Trần Quốc Tuấn, phát biểu, "Kính thưa các anh chị phóng viên. Kính thưa bạn đọc. Không phải tôi giành công, nhưng chiến thắng hôm nay của đội tuyển có sự góp sức rất lớn của VFF. Chúng tôi đã có tầm nhìn rất xa, khi xây dựng sân Mỹ Đình từ mấy chục năm trước để ngày hôm nay đội tuyển chúng ta có ngày đăng quang ngay trên sân nhà".
Anh Nguyễn Công Queo, tự nhận là một cổ động viên trung thành của Công Phượng, cho biết, "Trời ơi sướng quá mấy anh chị phóng viên ơi. Tôi đã đợi ngày này mấy chục năm nay rồi. Từ hồi coi lứa Công Phượng khi còn là U19 tôi đã biết thế nào cũng có ngày này mà. Hồi xưa mình đá với người Thái với người Hàn lúc nào cũng bị dưới cơ, bây giờ coi thấy rõ mình đá trên cơ họ mấy bậc. Đúng là quân tử trả thù mấy chục năm chưa muộn".
Ông Đoàn Nguyên Đức, chủ tịch tập đoàn Hoàng Anh Gia Lai, hồ hởi nói với báo giới, "Cuối cùng cũng đến ngày được nhìn thấy lứa Công Phượng vô địch thế giới. Chưa bao giờ tôi thấy hạnh phúc như hôm nay. Hồi xưa nhìn tụi nhỏ chân không đá banh đâu có ai tin tôi khi tôi nói bọn chúng sẽ làm nên chuyện. Từ lúc bọn nó vô địch giải U21 Báo Thanh Niên năm nào tôi đã âm thầm chuẩn bị tất cả cho ngày hôm nay rồi. Tôi đã phải đàm phán với FIFA rất căng để đem giải vô địch thế giới về Việt Nam".
Chủ tịch VFF, ông Trần Quốc Tuấn, phát biểu, "Kính thưa các anh chị phóng viên. Kính thưa bạn đọc. Không phải tôi giành công, nhưng chiến thắng hôm nay của đội tuyển có sự góp sức rất lớn của VFF. Chúng tôi đã có tầm nhìn rất xa, khi xây dựng sân Mỹ Đình từ mấy chục năm trước để ngày hôm nay đội tuyển chúng ta có ngày đăng quang ngay trên sân nhà".
Anh Nguyễn Công Queo, tự nhận là một cổ động viên trung thành của Công Phượng, cho biết, "Trời ơi sướng quá mấy anh chị phóng viên ơi. Tôi đã đợi ngày này mấy chục năm nay rồi. Từ hồi coi lứa Công Phượng khi còn là U19 tôi đã biết thế nào cũng có ngày này mà. Hồi xưa mình đá với người Thái với người Hàn lúc nào cũng bị dưới cơ, bây giờ coi thấy rõ mình đá trên cơ họ mấy bậc. Đúng là quân tử trả thù mấy chục năm chưa muộn".
*Nhóm Phóng Viên (giải U50 Thế Giới, báo Người Cao Tuổi)*
## Comments


markdown:
Thảo luận trong 'Electro House' bắt đầu bởi Siêu Chuối, 9/1/17.
Loading the player...
Dãn cách tên bằng dấu phẩy(,).

Server: Apache/2.4.18 (Ubuntu)

X-Frame-Options: SAMEORIGIN

Set-Cookie: xf_session=428616a1b341e7120ae99ba447a5b230; path=/; secure; HttpOnly

Vary: Accept-Encoding


markdown: Loại xăng này chỉ thích hợp đối với các loại xe hơi đời mới. Xe gắn máy thông thường không cần thiết sử dụng. Trên thị trường xăng dầu vừa xuất hiện thêm một loại xăng mới, xăng không chì RON 95 (tên thương mại Mogas 95), giá bán lẻ 5.600 đồng/lít. Đây là sản phẩm do Petrolimex nhập khẩu và là một trong ba loại xăng không chì được Nhà nước chấp nhận được phép kinh doanh tại VN trong chương trình chuyển đổi sử dụng xăng không chì thay thế xăng pha chì kể từ thời điểm 1-7-2001. Tuy nhiên, do tính toán nhu cầu thị trường nên đến nay xăng Mogas 95 mới chỉ bắt đầu được bán ra tại thị trường Hà Nội và TPHCM (ở TPHCM có gần chục cây xăng thuộc Petrolimex có bán xăng này). Về tính năng, theo các nhà chuyên môn, đây là loại xăng có trị số octane cao (tối thiểu bằng 95, tính theo phương pháp RON). Vì vậy loại xăng này thuộc nhóm xăng chất lượng cao, sử dụng rất hiệu quả cho các loại xe hơi có tỉ số nén lớn hơn 9,5/1 như các loại xe hơi đời mới đắt tiền, các loại xe thể thao, xe đua v.v... nhất là các loại xe đời mới có bộ chuyển đổi xúc tác, bộ kiểm soát động cơ điện tử... Ngoài ra, theo kỹ sư Nguyễn Đức Minh, Trưởng Phòng Kỹ thuật hàng hóa Petrolimex Saigon, loại xăng này cũng rất thích hợp đối với các loại xe gắn máy đời mới sử dụng tay ga số tự động từ 125cc trở lên. Cũng theo các nhà chuyên môn, đối với các loại xe gắn máy thông thường, kể cả xe đời mới loại 100cc và 110cc đều không cần thiết sử dụng loại xăng này. Mặc dù nếu sử dụng, xe không bị ảnh hưởng gì nhưng xét về hiệu quả kinh tế là không có lợi vì phải tốn thêm 500 đồng/lít (so với xăng Mogas 90) và 300 đồng/lít (so với xăng Mogas 92) trong khi hiệu suất máy cũng không mạnh hơn. Petrolimex có chủ trương dần dần sẽ hướng tới chỉ kinh doanh 2 loại xăng không chì là xăng Mogas 90 và Mogas 95. Loại Mogas 95 sẽ chuyên dùng cho các loại xe đời mới nói trên, các loại xe còn lại chỉ cần sử dụng xăng Mogas 90, vừa hiệu quả kinh tế (giá rẻ hơn) mà vẫn đảm bảo các tính năng kỹ thuật, kể cả đối với các loại xe lâu nay người tiêu dùng vẫn có thói quen sử dụng xăng không chì Mogas 92.

Vary: Accept-Encoding,User-Agent

Set-Cookie: xf_session=0bc3405ddbc3e3bbf8633379dd9b9d5e; path=/; secure; httponly