Bài 08 - Tạo Kiểu không tham số hóa
Chúng ta đã đề cập đến Kiểu là gì và tại sao chúng hữu ích trong các bài học trước. Vì vậy, trong phần này, chúng ta sẽ học cách tạo Kiểu của riêng mình.
- Kiểu tương đồng (Synonyms)
- Làm thế nào để định nghĩa chúng?
- Tại sao cần sử dụng chúng
- Kiểu mới với
data
- Tạo Kiểu
- Sử dụng Kiểu
- Thông số giá trị
- Cú pháp Bản ghi
Video bài giảng
Kiểu tương đồng (Synonyms)
Ngay từ đầu, khi tìm hiểu về Strings
Haskell, bạn đã phát hiện ra rằng đó String
là đường cú pháp cho [Char]
. Điều này có nghĩa là String
và [Char]
tương đương nhau và bạn có thể sử dụng chúng thay thế cho nhau .
Đó là bởi vì String
là một Kiểu tương đồng với [Char]
.
Cách định nghĩa Kiểu tương đồng
Để định nghĩa một Kiểu tương đồng, bạn sử dụng từ khóa type
, theo sau là tên mới cho Kiểu và Kiểu tồn tại trước đó tương đương.
type String = [Char]
Bạn có thể đặt tên cho Kiểu tương đồng theo cách bạn muốn, miễn là nó bắt đầu bằng chữ in hoa.
Khi bạn định nghĩa một Kiểu tương đồng, bạn sẽ không tạo một Kiểu mới! Bạn chỉ nói với Haskell rằng một Kiểu hiện có được gọi bằng một tên khác (một từ tương đồng)!
Tại sao nên sử dụng Kiểu tương đồng
Tại sao bạn lại làm phức tạp hơn mà không thêm nhiều tính năng hơn?
Bởi vì Kiểu tương đồng cho phép chúng ta truyền đạt nhiều thông tin hơn! Hãy xem một ví dụ.
Hãy tưởng tượng bạn bắt đầu làm việc với một văn phòng cho phép bạn tạo các giao dịch tiền tệ.
Bạn muốn tạo một giao dịch mới, vì vậy bạn hãy xem khai báo Kiểu của hàm mà bạn cần sử dụng:
generateTx :: String -> String -> Int -> String
Không phải là một khai báo hữu ích cho lắm. Bạn có thể suy luận rằng đó Int
là giá trị cần chuyển, nhưng đó là những thông tin Strings
gì? Và cái String
mà nó trả về chứa gì?
Bây giờ, hãy so sánh khai báo Kiểu đó với khai báo này:
generateTx :: Address -> Address -> Value -> Id
Rõ ràng, khai báo thứ hai truyền tải ngữ cảnh tốt hơn! Hai tham số đầu tiên là địa chỉ, tham số thứ ba là giá trị của giao dịch và có vẻ như nó trả về id của giao dịch.
Tất cả điều đó chỉ từ khai báo Kiểu. Sự khác biệt? Chỉ cần một vài Kiểu tương đồng.
Hãy xem những gì chúng ta đã làm để cải thiện bối cảnh rất nhiều. Bắt đầu bằng cách tạo lại hàm được gọi generateTx
sẽ lấy địa chỉ và giá trị của giao dịch và tạo id cho giao dịch đó:
generateTx :: String -> String -> Int -> String
generateTx from to value = from ++ to ++ show value
Bây giờ, chúng ta chỉ cần thêm một số Kiểu tương đồng và thay thế chúng trong khai báo:
type Address = String
type Value = Int
type Id = String
generateTx :: Address -> Address -> Value -> Id
generateTx from to value = from ++ to ++ show value
Siêu dễ dàng! Và nếu bạn muốn kiểm tra xem Address
, Value
, hoặc Id
Kiểu là gì, bạn có thể mở GHCi, tải tệp và kiểm tra thông tin của nó:
:i Address
type Address :: *
type Address = String -- Defined at :1:1
Và, tất nhiên, chúng ta có thể xây dựng trên các Kiểu tương đồng trước đó để tạo Kiểu phức tạp hơn. Đây là một ví dụ:
type Name = String
type Address = (String, Int)
type Person = (Name, Address)
bob = ("Bob Smith", ("Main St.", 555)) :: Person
:t bob
:t fst bob
bob :: Person
fst bob :: Name
Các Kiểu tương đồng là điều tuyệt vời, nhưng chúng chỉ là các tên khác nhau cho cùng một thứ. Nhưng điều gì sẽ xảy ra nếu chúng ta cần tạo một Kiểu hoàn toàn mới? Từ khóa data
là giải pháp!
Định nghĩa Kiểu mới với data
Chúng ta có thể tạo Kiểu mới như thế này:
data PaymentMethod = Cash | Card | Cryptocurrency
data Color = Red | Green | Blue
data Bool = True | False -- Real definition of Bool
data Ordering = LT | EQ | GT -- Real definition of Ordering
Chúng ta bắt đầu với từ khóa data
. Sau đó, phần trước dấu bằng là tên Kiểu mới của chúng ta và phần sau dấu bằng là các hàm tạo giá trị (value constructors).
Các hàm tạo giá trị định nghĩa các giá trị khác nhau mà Kiểu có thể có.
Trong ngữ cảnh này, dấu |
(dấu gạch đứng) được đọc là "hoặc". Vì vậy, chúng ta có thể đọc Kiểu đầu tiên là:
Kiểu
PaymentMethod
có thể có giá trị làCash
,Card
hoặcCryptocurrency
.
Tên Kiểu và Hàm tạo giá trị (value constructors) phải bắt đầu bằng một chữ cái viết hoa!
Sử dụng Kiểu mới của chúng ta
Và bây giờ, làm thế nào chúng ta có thể sử dụng Kiểu mới này?
Bằng cách sử dụng các giá trị của nó! Ví dụ: hãy thêm phương thức thanh toán cho người của chúng ta:
type Name = String
type Address = (String, Int)
data PaymentMethod = Cash | Card | Cryptocurrency deriving (Show)
type Person = (Name, Address, PaymentMethod)
bob = ("Bob Smith", ("Main St.", 555), Cash) :: Person
bob
("Bob Smith",("Main St.",555),Cash)
Chúng ta sẽ thêm deriving (Show)
vào cuối phần khai báo dữ liệu của mình.
Bằng cách thêm phần này, Haskell sẽ tự động biến Kiểu đó thành một Instance của Lớp Kiểu Show
. Cho phép chúng ta in chúng trên thiết bị đầu cuối.
Chúng ta sẽ giải thích chi tiết cách thức hoạt động của tính năng này trong bài học "Tạo Lớp Kiểu và Instance".
Và, tất nhiên, chúng ta có thể kiểm tra các thuộc tính của nó bằng lệnh :i
trong ghci:
:i PaymentMethod
type PaymentMethod :: *
data PaymentMethod = Cash | Card | Cryptocurrency
-- Defined at :3:1
instance [safe] Show PaymentMethod -- Defined at :3:61
Chúng ta có thể khớp mẫu cho các giá trị của nó:
howItPays :: Person -> String
howItPays (_, _, Cash) = "Pays in cash"
howItPays (_, _, Card) = "Pays with card"
howItPays (_, _, Cryptocurrency) = "Pays with cryptocurrency"
howItPays bob
"Pays in cash"
Và sử dụng nó như bất kỳ Kiểu nào khác.
Nhưng đó chỉ là phần nổi của tảng băng chìm. Chúng ta nên làm gì nếu cần nhiều hơn một vài giá trị?
Ví dụ, nếu tôi muốn một Kiểu đại diện cho một hình có thể là bất kỳ hình tròn hoặc hình chữ nhật nào thì sao?
Chúng ta có thể bắt đầu bằng cách định nghĩa một cái gì đó như:
data Shape = Circle | Rectangle
Nhưng vấn đề là, điều này không được sử dụng nhiều.
Tôi muốn có thể thực hiện các công việc với các giá trị này, chẳng hạn như tính chu vi và diện tích. Và tôi không thể làm điều đó nếu không có các thuộc tính thực tế của Shape!
Không có vấn đề gì cả! Chúng ta chỉ có thể truyền một số tham số cho hàm tạo!
Tham số giá trị (Value Parameters)
Hãy suy nghĩ về những gì chúng ta cần để đại diện cho bất kỳ hình tròn hoặc hình chữ nhật nào:
- Để tính diện tích một hình tròn, chúng ta chỉ cần bán kính của nó. Vì vậy, chỉ cần một giá trị số.
- Để tính diện tích một hình chữ nhật, chúng ta cần độ dài của hai cạnh của nó. Vì vậy, hai giá trị số.
Để dịch các yêu cầu đó thành mã, điều duy nhất chúng ta cần làm là thêm Kiểu khác làm đối số cho hàm tạo giá trị của chúng ta khi định nghĩa Kiểu, như sau:
data Shape = Circle Float | Rectangle Float Float
Và đây là một ví dụ về Kiểu dữ liệu đại số (algebraic data types) nổi tiếng mà mọi người nói đến. Một trong nhiều thuộc tính của Haskell.
Chúng được gọi là "Đại số" vì chúng ta có thể tạo Kiểu mới bằng cách kết hợp Kiểu trước đó bằng cách xen kẽ (A | B
có nghĩa A
hoặc B
nhưng không phải cả hai) hoặc bằng cách kết hợp ( A B
, có nghĩa A
và B
cùng nhau).
Và làm thế nào để sự kết hợp này hoạt động? Nếu chúng ta kiểm tra Kiểu Circle
hàm tạo:
-- data Shape = Circle Float | Rectangle Float Float
:t Circle
Hình tròn :: Float -> Shape
Chúng ta thấy đó Circle
là một hàm!!. Hàm này nhận một giá trị Kiểu Float
và trả về một giá trị Kiểu Shape
! Vì vậy, để có được một giá trị của Kiểu Shape
, tất cả những gì chúng ta phải làm là truyền vào bán kính của nó:
smallCircle = Circle 3
hugeCircle = Circle 100
:t smallCircle
smallCircle :: Shape
Và nó giống nhau đối với Rectangle
các giá trị:
-- data Shape = Circle Float | Rectangle Float Float
:t Rectangle
Rectangle
là một hàm nhận hai giá trị Kiểu Float
và trả về một giá trị Kiểu Shape
. Vì vậy, để có được một hình chữ nhật Kiểu Shape
, tất cả những gì chúng ta phải làm là chuyển độ dài các cạnh của nó:
rect1 = Rectangle 10 5
rect2 = Rectangle 256 128
:t rect1
rect1 :: Shape
Vậy đấy! Chúng ta đã tạo một số giá trị của Kiểu mới Shape
của chúng ta. Bây giờ chúng ta hãy sử dụng chúng!
Chúng ta có thể định nghĩa một hàm tính diện tích của bất kỳ giá trị Kiểu Shape
nào như sau:
area :: Shape -> Float
area (Circle r) = pi * r^2 -- We pattern match on value constructors
area (Rectangle l1 l2) = l1 * l2
area smallCircle
area rect2
28.274334
32768.0
Chúng ta vừa tạo ra một Kiểu thực sự hữu ích! Nhưng Tôi muốn nhiều hơn nữa! Tôi muốn thêm màu sắc! Và các điểm trong không gian 2D cho bạn biết vị trí tâm của hình!
Đối với điều đó, chúng ta có thể làm điều gì đó quái dị như thế này:
data Shape
= Circle (Float, Float) Float String
| Rectangle (Float, Float) Float Float String
Nơi chúng ta thêm các điểm trong không gian dưới dạng bộ giá trị Float
và màu sắc làm giá trị String
.
Chúng ta có thể dễ dàng định nghĩa lại area
hàm cho Kiểu mới này như sau:
area :: Shape -> Float
area (Circle _ r _) = pi * r^2
area (Rectangle _ l1 l2 _) = l1 * l2
Nhưng sau đó, nếu chúng ta muốn trích xuất các trường cụ thể của Kiểu Shape
, chúng ta phải tạo một hàm tùy chỉnh cho từng trường trong số chúng:
color :: Shape -> String
color (Circle _ _ c) = c
color (Rectangle _ _ _ c) = c
point :: Shape -> (Float, Float)
point (Circle p _ _) = p
point (Rectangle p _ _ _) = p
--- Etc...
type Point = (Float,Float)
type Radius = Float
type Width = Float
type Height = Float
type Color = String
data Shape
= Circle Point Radius Color
| Rectangle Point Width Height Color
Kiểu thực tế này là cách dễ đọc hơn. Tôi sẽ đưa bạn cái đó.
Nhưng đó là rất nhiều Kiểu tương đồng để cải thiện sự hiểu biết về khai báo. Và trên hết, nó không giải quyết được các vấn đề khác—cấp bách hơn—!
Nhưng đừng lo lắng, Haskell có sự hỗ trợ của chúng ta! Nhập cú pháp bản ghi!
Cú pháp ghi (Record Syntax)
Cú pháp bản ghi là một cách khác để định nghĩa Kiểu dữ liệu (data types) đi kèm với một số đặc quyền.
Chúng ta sẽ bắt đầu với một ví dụ dễ dàng hơn và sau đó chúng ta sẽ sửa Kiểu Shape
của mình.
Giả sử chúng ta muốn tạo một Kiểu dữ liệu Employee
chứa tên và số năm kinh nghiệm của nhân viên.
Nếu không có cú pháp bản ghi, chúng ta sẽ tạo nó như thế này:
data Employee = Employee String Float
Trong trường hợp này, vì Kiểu chỉ có một hàm tạo giá trị, nên thường sử dụng cùng tên với tên của Kiểu. Nó không giống như có gì đặc biệt về nó, nó chỉ là quy ước.
Nhưng với cú pháp bản ghi, chúng ta có thể tạo nó như sau:
data Employee = Employee { name :: String, experienceInYears :: Float } deriving (Show)
- Các hàm tạo giá trị cú pháp bản ghi có các tham số của chúng, mà chúng ta gọi là các trường, được bao quanh bởi các dấu ngoặc nhọn
}
. - Mỗi trường có một tên bắt đầu bằng một chữ cái viết thường theo sau là Kiểu của nó.
- Và các trường được phân tách bằng dấu phẩy.
Ok, chúng ta có Kiểu mới Employee
của chúng ta. Bây giờ chúng ta hãy sử dụng nó.
Chúng ta có thể tạo các giá trị như thế này:
richard = Employee { name = "Richard", experienceInYears = 7.5 }
:t richard
richard
richard :: Employee
Employee {name = "Richard", experienceInYears = 7.5}
Chúng ta cung cấp hàm tạo và giữa các dấu ngoặc nhọn }
của nó, chúng ta chỉ định tên của từng trường với giá trị tương ứng của nó. Có thể theo bất kỳ thứ tự nào!
Ngay lập tức, Kiểu dữ liệu kết quả dễ hiểu hơn và Show
Instance rõ ràng hơn khi chúng ta in nó. Nhược điểm duy nhất là chúng ta cần phải viết tất cả mã bổ sung đó.
matt = Employee "Matt" 5
matt
sẽ tương tự như Employee {name = "Matt", experienceInYears = 5.0}
Bạn cũng có thể tạo các giá trị mới của Kiểu Employee
bằng cách chuyển các tham số của các hàm tạo giá trị theo cùng thứ tự như định nghĩa của nó để có được kết quả cuối cùng giống nhau! Không cần thêm mã.
Và điều đó thậm chí còn không lọt vào top 3 đặc quyền tốt nhất! Một cách khác là chúng ta có thể cập nhật giá trị của bản ghi bằng cách tạo một giá trị mới từ giá trị trước đó và chỉ định nghĩa các trường đã thay đổi, như sau:
newMatt = matt { experienceInYears = 6 }
newMatt
Employee {name = "Matt", experienceInYears = 6.0}
Một điều tuyệt vời hơn nữa là nó tự động tạo các hàm để tra cứu các trường trong Kiểu dữ liệu!
:t name
name richard
:t experienceInYears
experienceInYears richard
name :: Employee -> String
"Richard"
experienceInYears :: Employee -> Float
7.5
Bởi vì chúng ta có hai trường (trường name
và trường experienceInYears
), chúng ta nhận được miễn phí hai hàm cùng tên lấy một giá trị của Kiểu Employee
và trả về giá trị của trường.
Bây giờ, nếu chúng ta muốn, ví dụ, để tính toán kinh nghiệm kết hợp của nhóm của bạn, bạn có thể làm điều gì đó như:
team = [Employee "John" 4, Employee "Josh" 2, Employee "Matthew" 7]
combinedExp :: [Employee] -> Float
combinedExp = foldr (\e acc -> experienceInYears e + acc) 0
combinedExp team
13.0
Thực sự tiện lợi! Và có nhiều hơn nữa! Nhưng trước khi tiết lộ thuộc tính tuyệt vời cuối cùng của cú pháp bản ghi, hãy sử dụng sức mạnh mới này và định nghĩa lại Kiểu khó hiểu Shape
.
Như bạn nhớ lại, không có cú pháp bản ghi, định nghĩa Kiểu dữ liệu là thế này:
data Shape
= Circle (Float, Float) Float String
| Rectangle (Float, Float) Float Float String
Chà, với cú pháp bản ghi là cú pháp này:
data Shape
= Circle
{ position :: (Float, Float)
, radius :: Float
, color :: String
}
| Rectangle
{ position :: (Float, Float)
, width :: Float
, height :: Float
, color :: String
}
deriving (Show)
Như bạn có thể thấy, tất cả những gì chúng ta phải làm là thay thế các tham số của hàm tạo bằng các trường bản ghi và chúng ta có thể sử dụng Kiểu dữ liệu giống như chúng ta đã làm với Kiểu Employee
.
Chúng ta có thể tạo các giá trị bằng cách sử dụng cú pháp thông thường và bản ghi và chúng ta có thể cập nhật các giá trị bằng cách chỉ định nghĩa các trường chúng ta cần thay đổi:
circ = Circle { position = (1, 2), radius = 6, color = "Green" }
:t circ
circ
rect1 = Rectangle (9, 3) 7 3 "Yellow"
:t rect1
rect1
rect2 = rect1 {width = 12}
:t rect2
rect2
circ :: Shape
Circle {position = (1.0,2.0), radius = 6.0, color = "Green"}
rect1 :: Shape
Rectangle {position = (9.0,3.0), width = 7.0, height = 3.0, color = "Yellow"}
rect2 :: Shape
Rectangle {position = (9.0,3.0), width = 12.0, height = 3.0, color = "Yellow"}
Và, tất nhiên, chúng ta có thể dễ dàng trích xuất các giá trị mà chúng ta cần bằng các hàm mới được định nghĩa tự động:
position circ
color rect2
(1.0,2.0)
"Yellow"
Tôi sẽ cho bạn thấy điều tương tự một lần nữa nhưng với một Kiểu khác. Tôi không muốn làm bạn chán, vì vậy hãy xem thứ gì khác đi kèm với hồ sơ.
Hãy sử dụng khớp mẫu (pattern matching) để định nghĩa lại hàm tính diện tích hình cho Kiểu dữ liệu bản ghi mới của chúng ta.
Ngay cả khi chúng ta đang sử dụng cú pháp bản ghi, chúng ta vẫn có thể so khớp mẫu như chúng ta vẫn thường làm:
area :: Shape -> Float
area (Circle _ r _) = pi * r ^ 2
area (Rectangle _ w h _) = w * h
area circ
area rect1
113.097336
21.0
Nhờ các bản ghi, giờ đây chúng ta có một cú pháp khớp mẫu đặc biệt!:
area :: Shape -> Float
area Circle {radius=r} = pi * r^2
area Rectangle {width=w,height=h} = w * h
area circ
area rect1
113.097336
21.0
Chúng ta khớp mẫu trên các hàm tạo giá trị cú pháp bản ghi bằng cách viết các trường của hàm tạo giữa các dấu ngoặc nhọn và liên kết chúng với một biến ở bên phải dấu bằng của trường.
Điều thú vị là chúng ta chỉ khớp các mẫu của các trường mà chúng ta cần sử dụng. Và điều này mang lại cho chúng ta một lợi ích tuyệt vời khác của cú pháp bản ghi. Nếu chúng ta thêm một trường khác vào Kiểu dữ liệu, chúng ta không cần thay đổi bất kỳ hàm nào trước đó! Bởi vì chúng ta không tính đến các trường không sử dụng khi đối sánh mẫu của chúng ta!
Tuyệt vời, phải không?
Cú pháp bản ghi đặc biệt hữu ích khi bạn có một Kiểu dữ liệu có thể có hàng chục trường. Giống như một Kiểu chứa các cài đặt của một ứng dụng. Hoặc một trong đó có tất cả các thông tin của một cuộc khảo sát.
Nó cho phép bạn sử dụng Kiểu mà không cần nhớ giá trị nào là gì (vì tất cả chúng đều được đặt tên) và cho phép bạn cập nhật và tham chiếu các trường cụ thể, bỏ qua phần còn lại. Vì vậy, nếu bạn thay đổi Kiểu của mình trong tương lai, chỉ các giá trị và hàm sử dụng trường đã thay đổi bị ảnh hưởng.
Ok, đó là nó cho ngày hôm nay. Trong bài học tiếp theo, chúng ta sẽ dựa trên bài học này để tạo Kiểu phức tạp hơn. Vì vậy, hãy đảm bảo làm bài tập về nhà, và tôi sẽ gặp bạn ở bài tiếp theo!
Nguồn bài viết tại đây