Skip to main content

Bài 10 - Tạo Lớp Kiểu và các thực thể

:::tip TÓM TẮT

  • Overloading
  • Các bước để tạo Lớp Kiểu (Type Classes) và (Thực thể) Instances
  • Lớp Kiểu Eq
    • Định nghĩa Lớp Kiểu
    • Định nghĩa đa thực thể
    • Nâng cấp Lớp kiểu Eq của chúng ta với đệ quy lẫn nhau (và MCD)
    • Định nghĩa một Thực thể cho một Kiểu được tham số hóa (parameterized type).
  • Lớp Kiểu WeAccept
  • Lớp Kiểu Container
  • Khám phá Lớp Kiểu Ord (Subclassing)
  • Deriving
    • Deriving có thể đi sai
Video bài giảng
Chúng tôi đang dịch thuyết minh bài giảng sang tiếng Việt

:::

Overloading

Trước khi tìm hiểu Overloading là gì, chúng ta hãy tìm hiểu nghĩa của từ "date".

NGÀY (DATE):

"date" có nghĩa là gì? Nếu tôi nói rằng bạn chỉ có một cơ hội để trả lời và tôi sẽ cho bạn 100 đô la nếu bạn trả lời đúng, thì câu trả lời trực quan là: "Còn tùy!"

  1. Nếu bạn đang nói: "Ngày sinh của bạn là ngày nào?", thì điều đó có nghĩa là:

    • Thời gian mà một sự kiện xảy ra.
  2. Nếu bạn đang nói: "Joe đã đưa Laura đi hẹn hò.", thì điều đó có nghĩa là:

    • Một sự tham gia xã hội thường có tính cách lãng mạn (trừ khi Joe được khoanh vùng bạn bè).
  3. Nếu bạn đang nói: "Tôi muốn hẹn hò với một hóa thạch", tôi muốn tin rằng bạn không đề cập đến một cuộc hẹn hò lãng mạn mà là:

    • Hành động ước tính hoặc tính toán một ngày hoặc niên đại.

Và nếu bạn tra cứu từ này, "date" cũng là tên của một Kiểu trái cây và thậm chí còn có nhiều định nghĩa hơn!

Trong lập trình, chúng ta sẽ nói rằng từ "ngày" bị Overloading. Bởi vì nó có nhiều định nghĩa cho cùng một tên.

Bản thân từ "Overloading" là Overloading.

:::tip OVERLOADING:

  1. Trong giao tiếp hàng ngày, từ Overloading thường có nghĩa là: Đặt một tải trọng quá lớn lên hoặc vào (cái gì đó).

  2. Trong bối cảnh lập trình thông thường, nó có nghĩa là: Có nhiều ứng dụng của một hàm có cùng tên.

    Làm thế nào điều này làm việc trong thực tế phụ thuộc vào ngôn ngữ. Ví dụ: một số ngôn ngữ, chẳng hạn như JavaScript, không hỗ trợ nạp chồng. Vì vậy, bạn không thể làm điều đó. Và trong các hàm khác, như C++, bạn có thể tạo nhiều hàm có cùng tên và trình biên dịch sẽ chọn định nghĩa sẽ sử dụng dựa trên các Kiểu đối số.

  3. Trong Haskell, "Overloading" có nghĩa là: Có nhiều ứng dụng của một hàm hoặc giá trị có cùng tên. :::

Tất nhiên, Haskell phải vượt qua vấn đề này. Trong Haskell, Overloading không bị hạn chế đối với các hàm. Các giá trị cũng có thể bị Overloading. Ví dụ:

  • Các chữ 12, v.v. bị Overloading vì chúng có thể được hiểu là bất kỳ Kiểu số nào (IntIntegerFloat, v.v.)
  • Giá trị minBound bị Overloading bởi vì, ví dụ: khi được sử dụng dưới dạng một Char, có thể sẽ có giá trị '\NUL' trong khi dưới dạng một Int, giá trị đó là -2147483648.
  • Toán tử đẳng thức (==) hoạt động với nhiều kiểu, mỗi kiểu có cách thực hiện riêng.
  • Chức năng max cũng hoạt động với nhiều Kiểu, mỗi Kiểu có cách thực hiện riêng.

Hai giá trị đầu tiên là các giá trị bị Overloading và giá trị cuối cùng là các hàm bị Overloading. Vì vậy, chúng ta đã và đang sử dụng các hàm và giá trị Overloading. Câu hỏi đặt ra là: Làm thế nào để chúng ta biết được những thứ đó theo thứ tự ưu tiên? Chà, cơ chế cho phép Overloading trong Haskell là Type Classes (Lớp Kiểu).

Các bước để tạo Type Classes và Instances

Trong bài "Giới thiệu về Lớp Kiểu", chúng ta đã thấy tiện ích của Lớp Kiểu. Về cơ bản, nó tập trung vào việc có các hàm có thể được sử dụng bởi nhiều Kiểu khác nhau trong khi vẫn đảm bảo an toàn rằng chúng chỉ sử dụng những hàm mà chúng có thể làm việc cùng. Vì vậy, nếu bạn tạo một hàm lấy hai số và cộng chúng lại với nhau, thì hàm đó hoạt động với tất cả các Kiểu số đồng thời khiến trình biên dịch dừng bạn khi cố gắng cung cấp cho nó một Kiểu không phải là số.

Các Lớp Kiểu là một tính năng khá độc đáo – không nhiều ngôn ngữ lập trình có chúng. Nhưng điều tốt là chúng rất dễ sử dụng!

:::tip Khi tạo các Lớp Kiểu của riêng mình, chúng ta chỉ cần hai thứ.

  1. Tạo một Lớp Kiểu nêu rõ một số hành vi.
  2. Tạo một Loại một thực thể của Loại Loại đó với việc triển khai các hành vi đó cho Kiểu cụ thể đó. :::

Thực hành tạo nên sự hoàn hảo, vì vậy hãy học bằng cách thực hành. Chúng ta sẽ bắt đầu bằng cách xác định lại Lớp Kiểu Eq.

Lớp Kiểu Eq

Lớp Kiểu Eq được đi kèm với Haskell, vì vậy bạn không cần phải định nghĩa nó. Nhưng giả sử rằng chúng ta đang sống trong một thế giới Eq không tồn tại Lớp Kiểu và mỗi Kiểu có hàm riêng để kiểm tra sự bình đẳng. Do đó, bạn phải học một loạt các hàm khác nhau mà tất cả đều thực hiện giống nhau: Kiểm tra sự bằng nhau.

Nhưng, như Lennon đã nói, hãy tưởng tượng. Khi sống trong thế giới khủng khiếp đó, hãy tưởng tượng tất cả các Kiểu sống trong hòa bình và sử dụng cùng một hàm. Thật dễ dàng nếu bạn cố gắng. Bạn có thể nói tôi là một kẻ mơ mộng, nhưng hãy cứ làm đi!

Chúng ta có thể định nghĩa Lớp Kiểu Eq như thế này:

class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool

Trong dòng đầu tiên, chúng ta bắt đầu với class từ khóa để cho biết chúng ta đang tạo một Lớp Kiểu. Tiếp theo là cách Lớp Kiểu sẽ được gọi (Eq). Sau đó, chúng ta viết một biến kiểu (a) đại diện cho bất kỳ kiểu nào sẽ được tạo thành một thực thể của Lớp Kiểu này trong tương lai. Vì vậy, nó giống như một trình giữ chỗ. Và cuối cùng, chúng ta sử dụng where từ khóa để bắt đầu khối nơi chúng ta xác định các hành vi của Lớp Kiểu mới được tạo.

Và bây giờ đến phần thú vị. Chúng ta phải xác định các hành vi. Để làm điều đó, chúng ta viết tên và Kiểu hàm hoặc giá trị mà chúng ta cần. Trong trường hợp này, chúng ta xác định các hành vi là hàm ==–để kiểm tra xem hai giá trị có bằng nhau hay không và  hàm /=–để kiểm tra xem hai giá trị có khác nhau không.

Chúng ta cũng chỉ ra rằng cả hai đều nhận hai giá trị của Kiểu a mà chúng ta đã chỉ định làm tham số của Lớp Kiểu và trả về a BoolTrue nếu điều kiện vượt qua và False nếu không.

Và thực hiện! Chúng ta có Lớp kiểu Eq của chúng ta đã sẵn sàng để dùng! Điều này có nghĩa là chúng ta có tên và kiểu của hai hàm mà Lớp Kiểu Eq cung cấp. Chúng ta không có các định nghĩa ở đây vì mỗi Kiểu sẽ có các định nghĩa riêng. Và những định nghĩa đó được cung cấp khi định nghĩa một thực thể (Instance) cho Lớp Kiểu.

Định nghĩa một thực thể cho Lớp kiểu Eq

Trước tiên, chúng ta cần một Kiểu, vì vậy hãy xác định một Kiểu cho các phương thức thanh toán mà khách hàng có thể sử dụng trong ứng dụng của chúng ta:

data PaymentMethod = Cash | Card | CC -- CC stands for Cryptocurrency

type User = (String, PaymentMethod)

Và nếu chúng ta muốn, ví dụ, để kiểm tra xem hai người dùng có cùng một phương thức thanh toán hay không, chúng ta có thể viết một hàm như sau:

samePM :: User -> User -> Bool
samePM (_, pm1) (_, pm2) = pm1 == pm2 -- Won't work!

<interactive>:2:28: error:
• No instance for (Eq PaymentMethod) arising from a use of ‘==’
• In the expression: pm1 == pm2
In an equation for ‘samePM’: samePM (_, pm1) (_, pm2) = pm1 == pm2

Tuy nhiên, trình biên dịch sẽ không cho phép bạn sử dụng mã này! Và nó cho chúng ta biết tại sao:

No instance for (Eq PaymentMethod) arising from a use of ‘==’
In the expression: pm1 == pm2

Chúng ta đang sử dụng == trong biểu thức pm1 == pm1. Nhưng, bởi vì == là một hành vi của Lớp Kiểu Eq và PaymentMethod kiểu mới của chúng ta không phải là một thực thể của Lớp Kiểu Eq! Vì vậy, chúng ta không thể sử dụng == và /=. Để khắc phục điều này, chúng ta sẽ biến nó thành một ví dụ!

-- class Eq a where
-- ...

instance Eq PaymentMethod where
-- Implementations for Eq behaviors specific to PaymentMethod

Để tạo một thực thể, chúng ta sử dụng instance từ khóa theo sau là tên của Lớp Kiểu mà chúng ta muốn tạo một thực thể, Kiểu sẽ là một thực thể của Lớp Kiểu đó và từ khóa where. Sau đó, bên trong khối đó, chúng ta triển khai các hàm được định nghĩa trong Lớp Kiểu đó.

Như bạn có thể thấy, bởi vì bây giờ chúng ta đang tạo một thực thể cho một kiểu, nên chúng ta thay thế biến kiểu (a) mà chúng ta có trong định nghĩa Lớp Kiểu bằng kiểu cụ thể (PaymentMethod).

Và bởi vì chúng ta đang tạo một thực thể cho Lớp Kiểu Eq, nên chúng ta cần triển khai các hàm == và /=. Vì vậy, chúng ta sẽ làm điều đó:

-- class Eq a where
-- (==) :: a -> a -> Bool
-- (/=) :: a -> a -> Bool

-- data PaymentMethod = Cash | Card | CC

instance Eq PaymentMethod where
-- Implementation of ==
Cash == Cash = True
Card == Card = True -- Same as: (==) Card Card = True
CC == CC = True
_ == _ = False

-- Implementation of /=
Cash /= Cash = False
Card /= Card = False
CC /= CC = False
_ /= _ = True

Và thế là xong! Đó là cách bạn định nghĩa một Lớp Kiểu và biến một kiểu thành một thực thể của nó! Bây giờ, PaymentMethod có thể tự do sử dụng Eq với các hành vi (== và /=):

Card == Cash
CC /= Card

False

True

Và hàm trước đó sẽ hoạt động ngay bây giờ:

samePM :: User -> User -> Bool
samePM (_, pm1) (_, pm2) = pm1 == pm2 -- It's alive!

samePM ("Rick", CC) ("Marta", CC)

True

Nâng cấp Lớp kiểu Eq của chúng ta với Đệ quy lẫn nhau

Công việc của chúng ta được thực hiện về mặt kỹ thuật. Chúng ta có Lớp Kiểu của chúng ta và ví dụ của chúng ta. Nhưng có một thuộc tính của các hàm mà chúng ta vừa định nghĩa mà chúng ta không tận dụng được.

Nếu hai giá trị bằng nhau, điều đó có nghĩa là chúng không khác nhau và nếu chúng khác nhau, điều đó có nghĩa là chúng không bằng nhau. Vì vậy, chúng ta biết rằng đối với mỗi cặp giá trị == và /= sẽ luôn cho chúng ta Bool giá trị ngược lại.

Chúng ta đang trên đường trở thành những nhà phát triển Haskell vĩ đại và những nhà phát triển Haskell vĩ đại có thể làm tốt hơn thế. Vì vậy, hãy sử dụng kiến ​​thức này để cải thiện Lớp Kiểu và thực thể của chúng ta! Bắt đầu bằng cách định nghĩa lại Lớp Kiểu Eq như thế này:

class Eq a where
(==), (/=) :: a -> a -> Bool
x /= y = not (x == y)
x == y = not (x /= y)

Đó là cách Eq thực sự được xác định trong Haskell!

Hãy phân tích mã này. Vì cả hai hàm đều có cùng kiểu nên chúng ta có thể chỉ định chúng trong một dòng. Và vâng, chúng ta cũng đang viết các định nghĩa hàm bên trong Lớp Kiểu. Chúng ta có thể làm điều đó miễn là chúng không phân biệt Kiểu vì chúng phải làm việc với tất cả các Kiểu.

Xem xét các định nghĩa chi tiết hơn, chúng ta thấy mình đang sử dụng hàm not. Hàm not nhận một giá trị boolean và trả về giá trị ngược lại của nó.

Vì vậy, trong dòng thứ ba, chúng ta đang nói rằng kết quả của việc áp dụng /= cho x và y là ngược lại (not) kết quả của việc dùng == cho cùng x và y. Và ở dòng thứ tư, chúng ta đang nói rằng kết quả của việc dùng == cho x và y là ngược lại (not) kết quả của việc dùng /= cho cùng x và y.

Điều này được gọi là đệ quy lẫn nhau vì cả hai hàm được xác định theo thuật ngữ của nhau. Bằng cách định nghĩa == và /= đối lập với nhau, Haskell có thể suy ra hành vi của cái này từ cái kia.

Và, tất nhiên, giống như bất kỳ đệ quy nào khác, nó cần một trường hợp cơ sở để biết khi nào nên dừng đệ quy! Và đó là những gì chúng ta cung cấp khi triển khai một phiên bản! Ví dụ: hãy xác định lại đối tượng PaymentMethod cho Lớp Kiểu mới này:

instance Eq PaymentMethod where
Cash == Cash = True
Card == Card = True
CC == CC = True
_ == _ = False

Vậy đấy! Bởi vì bây giờ trình biên dịch có thể suy ra giá trị của hàm này với hàm kia, nên chúng ta không cần triển khai cả hai == và /=. Chúng ta có thể thực hiện một cách thuận tiện hơn.

Điều này được gọi là định nghĩa đầy đủ tối thiểu. Bởi vì đó là mức tối thiểu bạn phải triển khai để có được phiên bản đầy đủ hàm. Bạn có thể tận dụng điều này bằng cách kiểm tra định nghĩa đầy đủ tối thiểu của bất kỳ Lớp Kiểu nào bằng cách sử dụng :i <type class> và chỉ thực hiện các hành vi đó. Ví dụ: nếu bạn chạy :i Eq trong GHCi, bạn sẽ nhận được:

type Eq :: * -> Constraint -- Eq takes a concrete type and returns a Constraint
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
{-# MINIMAL (==) | (/=) #-}

-- ... and a bunch of instances.

Trong dòng này:

{-# MINIMAL (==) | (/=) #-}

Nó nói rằng để có định nghĩa đầy đủ tối thiểu của Lớp Kiểu, bạn phải triển khai == OR /=.

Trong thế giới thực, hầu hết tất cả các kiểu đều là thực thể của Lớp Kiểu Eq. Nhưng hãy nhớ rằng, chúng ta đang ở trong một vũ trụ song song, nơi bạn là người có tầm nhìn xa tạo ra Eq lớp nhân vật để biến thế giới thành một thứ tốt đẹp hơn. Vì vậy, nếu chúng ta dừng ở đây, hàm == and /= sẽ không bị Overloading! Bởi vì chúng sẽ chỉ có định nghĩa cho PaymentMethod.

Nhưng có một lý do khiến bạn quyết định tạo Lớp Kiểu Eq này. Và lý do là bạn nghĩ rằng các hành vi mà nó cung cấp là hữu ích cho nhiều Kiểu. Ví dụ như Kiểu Blockchain:

-- Create data type
data Blockchain = Cardano | Ethereum | Bitcoin

-- Create instance of Eq
instance Eq Blockchain where
Cardano == Cardano = True
Ethereum == Ethereum = True
Bitcoin == Bitcoin = True
_ == _ = False


-- Test
Cardano /= Cardano

False

Bây giờ, == và /= thực sự bị Overloading vì chúng có nhiều hơn một định nghĩa tùy thuộc vào Kiểu giá trị mà chúng được áp dụng.

Chúng ta làm được rồi!! Và chúng ta đang trên đà phát triển, vì vậy hãy tiếp tục!

Cho đến nay, chúng ta đã tạo hai thực thể của Lớp kiểu Eq. Cả hai cho các Kiểu không tham số. Hãy tìm hiểu cách chúng ta có thể định nghĩa một thực thể cho một Kiểu được tham số hóa.

Định nghĩa một thực thể cho một Kiểu được tham số hóa (parameterized type)

Để tạo một thực thể cho kiểu được tham số hóa, trước tiên, chúng ta cần kiểu được tham số hóa:

data Box a = Empty | Has a

Bây giờ chúng ta có thể tạo ví dụ của mình. Nhưng chúng ta không thể làm điều đó như thế này:

instance Eq Box where
-- ...

Tại sao? Chà, nếu chúng ta xem Lớp Kiểu bằng :i lệnh:

type Eq :: * -> Constraint -- Eq takes a concrete type and returns a Constraint
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
{-# MINIMAL (==) | (/=) #-}

-- ... and a bunch of instances.

Chúng ta được nhắc nhở rằng biến kiểu a là một kiểu cụ thể. Chúng ta có thể thấy điều này ở hai nơi:

  • Nếu chúng ta kiểm tra các Kiểu hàm, chúng ta sẽ thấy rằng biến Kiểu a nằm một mình giữa các mũi tên, do đó, nó đại diện cho một Kiểu cụ thể.
  • Và do đó, Kiểu Eqtype Eq :: * -> Constraint) nói rõ rằng nó lấy một Kiểu cụ thể và tạo ra một Constraint (Ràng buộc).

Các Lớp Kiểu luôn có một Kiểu trả về một Constraint vì các Lớp Kiểu không tạo ra một Kiểu. Chúng tạo ra một ràng buộc cho các giá trị đa hình. Vì vậy, nếu chúng ta thấy một Kiểu kết thúc bằng Constraint, chúng ta biết đó là một Lớp Kiểu và nó nằm ở bên trái của mũi => tên để hạn chế các Kiểu đa hình.

Trên hết, chúng ta không cần kiểm tra các hàm để biết Lớp Kiểu sử dụng biến kiểu a như thế nào. Loại (kind) đã cho chúng ta biết nếu nó cần một Kiểu cụ thể hoặc một hàm tạo Kiểu cụ thể.

Vì vậy, vì Eq :: * -> Constraint, chúng ta biết rằng a trong Eq a là một Kiểu cụ thể. Nhưng nếu chúng ta kiểm tra Kiểu Box:

:k Box

Box :: * -> *

Chúng ta thấy rằng nó không phải là một kiểu cụ thể mà là một hàm tạo kiểu lấy một kiểu làm tham số và trả về một kiểu cụ thể.

Vậy ta phải làm sao? Chúng ta có thể áp dụng Box cho một Kiểu khác để có được một Kiểu cụ thể, như thế này:

:k Box Int

Hộp Int :: *

Về mặt kỹ thuật, điều đó mang lại cho chúng ta một Kiểu cụ thể, vì vậy chúng ta có thể tạo các phiên bản như thế này:

instance Eq (Box Int) where
-- ...

instance Eq (Box String) where
-- ...

instance Eq (Box PaymentMethod) where
-- ...

--- etc

Và nó sẽ hoạt động hoàn hảo. Nhưng, Hmm, đây là rất nhiều công việc. Và chúng ta đã trải qua điều này khi định nghĩa các hàm và giải quyết nó bằng các biến kiểu. Thời gian này là không khác nhau!:

instance Eq (Box a) where
-- ...

Bằng cách định nghĩa thực thể này, tất cả các kiểu được tạo bằng cách sử dụng hàm Box tạo kiểu (như Box String hoặc Box Int) sẽ là một thực thể của Eq.

Bây giờ, đợi một chút. Làm cách nào để chúng ta xác định thực thể nếu chúng ta không biết Kiểu giá trị bên trong hộp? Chà, nếu chúng ta quyết định rằng:

  • Hai hộp chứa các phần tử bằng nhau thì bằng nhau.
  • Hai ô trống bằng nhau.
  • Và mọi thứ khác là khác nhau.

Chúng ta có thể định nghĩa các hành vi như thế này:

instance Eq (Box a) where
Has x == Has y = x == y
Empty == Empty = True
_ == _ = False

<interactive>:2:20: error:
• No instance for (Eq a) arising from a use of ‘==’
Possible fix: add (Eq a) to the context of the instance declaration
• In the expression: x == y
In an equation for ‘==’: Has x == Has y = x == y
In the instance declaration for ‘Eq (Box a)’

Trong công thức đầu tiên, chúng ta xác định == cho Kiểu Box a bằng cách áp dụng == cho Kiểu a mà nó chứa. Bởi vì Has x là Kiểu Box ax là Kiểu a. Tương tự cho các giá trị còn lại. Vì vậy, nếu cả hai hộp chứa cùng một phần tử, thì bản thân các hộp đều giống nhau. Còn không, chúng khác nhau. Vì vậy, chúng ta đã tạo phiên bản của Box a phụ thuộc vào phiên bản của a.

Trong công thức thứ hai, chúng ta xác định rằng nếu cả hai ô đều trống thì chúng bằng nhau.

Đối với mọi trường hợp khác, các hộp là khác nhau.

Điều này có ý nghĩa, nhưng có một sự giám sát LỚN từ phía chúng ta! Bạn có phát hiện ra nó không? Không sao đâu. Đó là mục đích của trình biên dịch ở đây! Nếu chúng ta chạy ô, chúng ta sẽ gặp lỗi trình biên dịch:

No instance for (Eq a) arising from a use of ‘==’

Ok, vì vậy trình biên dịch cho chúng ta biết rằng chúng ta đang áp dụng hàm == cho một Kiểu không có phiên bản Eq.

Chúng ta đang làm điều đó ở đâu?

In the expression: x == y
In an equation for ‘==’: Has x == Has y = x == y
In the instance declaration for ‘Eq (Box a)’

Trình biên dịch nói chính xác! Chúng ta đang sử dụng == giữa hai giá trị (x và y) của Kiểu a mà không đảm bảo rằng a chính Kiểu đó là một thực thể của Eq !

Vậy chúng ta nên làm gì? Chà, trình biên dịch cũng cho chúng ta biết cách khắc phục điều này:

Possible fix: add (Eq a) to the context of the instance declaration

Tương tự như với các hàm, chúng ta có thể thêm ràng buộc rằng kiểu a trong thực thể của Eq (Box a) cũng phải là một thực thể của Lớp Kiểu Eq. Như thế này:

instance (Eq a) => Eq (Box a) where
Has x == Has y = x == y
Empty == Empty = True
_ == _ = False

Bằng cách này, Kiểu Box a sẽ là một thực thể của Eq tất cả các Kiểu a cũng là một thực thể của Eq.

Aaaaa và chúng ta xong rồi! Chúng ta có thể sử dụng phiên bản mới này như thế này:

Has Cardano /= Has Ethereum -- True

Has Card == Empty -- False

Has Bitcoin /= Has Bitcoin -- False


data Choice = Yes | No -- We didn't create an Eq instance for Choice

Has Yes == Has No -- Angry compiler: There's no instance for (Eq Choice), you fool!

True

False

False

<interactive>:1:1: error:
• No instance for (Eq Choice) arising from a use of ‘==’
• In the expression: Has Yes == Has No
In an equation for ‘it’: it = Has Yes == Has No

Vì vậy, ngay cả khi bọc Kiểu này bên trong Kiểu khác, trình biên dịch vẫn sẽ bảo vệ chúng ta khỏi những sai lầm của con người.

Được rồi. Bây giờ chúng ta đã làm mọi thứ từng bước với Lớp Kiểu Eq, hãy làm lại mọi thứ, nhưng nhanh hơn và với một Lớp Kiểu mới không phải là một phần của Haskell tiêu chuẩn.

Lớp Kiểu WeAccept

Hãy tưởng tượng chúng ta đang viết một ứng dụng chấp nhận thanh toán cho một công ty và công ty này không chấp nhận tất cả các phương thức thanh toán, blockchain và quốc gia. Vì vậy, bạn phải tạo các hàm để kiểm tra xem:

-- Function to check if we accept that payment method
weAcceptPayment :: PaymentMethod -> Bool
weAcceptPayment p = case p of
Cash -> False
Card -> True
CC -> True

-- Function to check if we accept that blockchain
weAcceptBlockchain :: Blockchain -> Bool
weAcceptBlockchain b = case b of
Bitcoin -> True
Ethereum -> False
Cardano -> True

-- Country type
newtype Country = Country { countryName :: String }

-- Function to check if we accept that country
weAcceptCountry :: Country -> Bool
weAcceptCountry c = case countryName c of
"Mordor" -> False
_ -> True

Xem mã này, chúng ta nhận ra rằng hành vi kiểm tra xem công ty có chấp nhận điều gì đó hay không có thể được sử dụng trong nhiều khía cạnh khác. Như nhà cung cấp, công nghệ, v.v. Có rất nhiều thứ mà một công ty có thể quyết định chấp nhận hay không.

Để tránh có nhiều hàm khác nhau thực hiện giống nhau trên toàn bộ mã của bạn, chúng ta quyết định tạo một Lớp Kiểu đại diện cho hành vi này.

Và Lớp Kiểu đó trông như thế này:

-- Creating WeAccept type class
class WeAccept a where
weAccept :: a -> Bool

-- Checking kind of WeAccept

:k WeAccept

WeAccept :: * -> Constraint

Bây giờ chúng ta đã có Lớp Kiểu của mình, chúng ta có thể tạo các thực thể cho PaymentMethodBlockchainCountry và thậm chí Box như thế này:

instance WeAccept PaymentMethod where
weAccept x = case x of
Cash -> False
Card -> True
CC -> True

instance WeAccept Blockchain where
weAccept x = case x of
Bitcoin -> True
Ethereum -> False
Cardano -> True

instance WeAccept Country where
weAccept x = case countryName x of
"Mordor" -> False
_ -> True

instance (WeAccept a) => WeAccept (Box a) where
weAccept (Has x) = weAccept x
weAccept Empty = False

Và thử thực hiện! Điều này cho chúng ta khả năng áp dụng hàm weAccept Overloading cho ba Kiểu khác nhau:

weAccept Cardano
weAccept Cash
weAccept (Country "Mordor")
weAccept (Has Bitcoin)

True

False

False

True

Chúng ta cũng có thể tạo các hàm có thể được áp dụng cho tất cả các Kiểu là thực thể của WeAccept:

-- Creating fancyFunction
fancyFunction :: (WeAccept a) => a -> String
fancyFunction x =
if weAccept x
then "Do something fancy"
else "Don't do it!"

-- Using fancyFunction
fancyFunction Cardano
fancyFunction Card
fancyFunction (Country "Mordor")
fancyFunction (Has Bitcoin)

"Do something fancy"

"Do something fancy"

"Don't do it!"

"Do something fancy"

Một Lớp Kiểu khác dưới vành đai của chúng ta! Nó trở nên dễ dàng hơn từng phút!

Chúng ta sẽ làm thêm một ví dụ nữa trước khi tiếp tục sang phần tiếp theo. Cái này khó hơn một chút, nhưng nếu bạn hiểu nó, bạn sẽ có thể hiểu bất kỳ Lớp Kiểu nào! Cho dù nó phức tạp đến mức nào!

Lớp Kiểu Container

Đây là tình huống: Chúng ta đang làm việc trên một phần mềm hậu cần có hai Kiểu gói hàng khác nhau. Một chiếc hộp (Box) thông thường có thể chứa hoặc không chứa thứ gì đó, và một món quà (Present), có thể chứa hoặc không chứa thứ gì đó, nhưng nó luôn có bảng tên ghi món quà đó dành cho ai. Vì vậy, chúng ta có hai Kiểu sau:

    data Box a       = Empty          | Has a            deriving (Show)
data Present t a = EmptyPresent t | PresentFor t a deriving (Show)

:k Box
:k Present

Box :: * -> *

Present :: * -> * -> *

Bởi vì chúng ta đã quyết định rằng thẻ của Present (t) có thể là một số, tên hoặc bất kỳ thứ gì khác có thể xác định khách hàng, chúng ta cũng sẽ tham số hóa Kiểu của nó.

Bây giờ, một số phần của quy trình yêu cầu các hàm chung cho cả hai Kiểu. Chúng ta cần:

  • Một để kiểm tra xem Box hoặc Present có trống không.
  • Một để kiểm tra xem một giá trị cụ thể có được chứa bên trong Box hay không.
  • Và một để thay thế nội dung của hộp hoặc Present.

Thay vì tự viết các hàm và sau đó chuyển đổi chúng thành một Lớp Kiểu và các thực thể như chúng ta đã làm trong hai ví dụ trước, hãy chuyển thẳng sang Lớp Kiểu.

class Container c where
isEmpty :: -- ...
contains :: -- ...
replace :: -- ...

Lớp Kiểu (type class) sẽ được gọi là Container vì nó cung cấp các hành vi liên quan đến container. Biến Kiểu được gọi là c vì nó là một container.

Bây giờ, hãy viết ra các khai báo Kiểu. Chúng ta sẽ bắt đầu với hàm replace.

class Container c where
isEmpty :: -- ...
contains :: -- ...
replace :: c a -> b -> c b

replace có hai đầu vào:

  • Một container c có một số giá trị thuộc Kiểu—giả sử là a —bên trong.
  • Và một giá trị khác có thể cùng Kiểu hoặc khác Kiểu với giá trị bên trong container. Hãy gọi nó là b.

Hàm thay thế giá trị của Kiểu a bên trong container bằng giá trị của Kiểu b. Vì vậy, cuối cùng, chúng ta nhận được một giá trị thuộc Kiểu c b vì giá trị chứa trong đó hiện thuộc Kiểu b.

Bây giờ, hãy thực hiện hàm contains:

class Container c where
isEmpty :: -- ...
contains :: (Eq a) => c a -> a -> Bool
replace :: c a -> b -> c b

contains có hai đầu vào:

  • Một container c có một số giá trị của Kiểu a bên trong.
  • Và một giá trị khác sẽ được so sánh với giá trị bên trong container. Vì vậy, nó cần phải cùng Kiểu a và một ví dụ của Eq bởi vì chúng ta sẽ cần sử dụng == để kiểm tra xem nó có cùng giá trị hay không.

Hàm lấy giá trị, kiểm tra xem nó có giống với giá trị bên trong container hay không và trả về True nếu đúng và False nếu không. Vì vậy, chúng ta trả về một boolean.

Và cuối cùng, hãy thực hiện hàm isEmpty:

class Container c where
isEmpty :: c a -> Bool
contains :: (Eq a) => c a -> a -> Bool
replace :: c a -> b -> c b

isEmpty có một đầu vào:

  • Một container c có một số giá trị của Kiểu a bên trong.

Hàm lấy container và trả về True nếu nó chứa giá trị và False nếu không. Vì vậy, nó trả về một giá trị kiểu Bool.

Lớp Kiểu của chúng ta đã sẵn sàng để dùng!

Và bởi vì mỗi -> (mũi tên) phân tách một giá trị và tất cả các giá trị cần phải có một Kiểu cụ thể, chúng ta biết rằng cả hai a và đều b là Kiểu cụ thể. Bởi vì chúng đứng một mình giữa những mũi tên.

Sử dụng cùng một lý do, chúng ta biết điều đó c a và c b phải là các Kiểu cụ thể. Và bởi vì a và b là các kiểu cụ thể, điều này có nghĩa đó c là một hàm tạo kiểu lấy một kiểu cụ thể và trả về một kiểu cụ thể.

Chúng ta có thể thấy điều này nếu chúng ta kiểm tra Kiểu của Lớp Kiểu của chúng ta:

   :k Container

Container :: (* -> *) -> Constraint

Bây giờ chúng ta đã có Lớp Kiểu của mình, hãy tạo các thực thể cho kiểu Box:

    -- class Container c where
-- isEmpty :: c a -> Bool
-- contains :: (Eq a) => c a -> a -> Bool
-- replace :: c a -> b -> c b

instance Container Box where

isEmpty Empty = True
isEmpty _ = False

contains (Has x) y = x == y
contains Empty _ = False

replace _ x = Has x


:k Box
:k Container

Box :: * -> *

Container :: (* -> *) -> Constraint

Lưu ý rằng chúng ta tạo một phiên bản cho Box, không phải Box a. Đối với Lớp kiểu Eq, chúng ta đã áp dụng Box cho biến Kiểu a để có được Kiểu cụ thể Box a vì Lớp kiểu Eq cần một Kiểu cụ thể làm tham số. Nhưng Container lấy một hàm tạo thuộc Kiểu * -> *, cùng Kiểu với Box. Vì vậy, chúng ta phải vượt qua Box mà không áp dụng nó vào bất cứ điều gì.

Việc thực hiện thực tế của các hàm là khá đơn giản. Vì Box có hai hàm tạo nên chúng ta có hai công thức cho mỗi hàm.

Bây giờ hãy tạo thực thể cho Kiểu Present:

    -- class Container c where
-- isEmpty :: c a -> Bool
-- contains :: (Eq a) => c a -> a -> Bool
-- replace :: c a -> b -> c b


instance Container (Present t) where

isEmpty (EmptyPresent _) = True
isEmpty _ = False

contains (PresentFor _ x) y = x == y
contains (EmptyPresent _) _ = False

replace (PresentFor tag _) x = PresentFor tag x
replace (EmptyPresent tag) x = PresentFor tag x


:k Present
:k Container
:k Present String

Present :: * -> * -> *

Container :: (* -> *) -> Constraint

Present String :: * -> *

Bây giờ, thực thể dành cho hàm tạo kiểu Present t. Điều này là do Present bản thân nó có Kiểu * -> * -> *, nhưng vì Container có một hàm tạo Kiểu là Kiểu * -> *, nên chúng ta phải áp dụng Present cho một Kiểu—như Present String —để có được Kiểu chúng ta cần. Và bởi vì chúng ta muốn có thể sử dụng bất kỳ Kiểu nào làm thẻ, nên chúng ta sử dụng biến Kiểu t.

Vì vậy, phần này là quan trọng. Trong là thẻ tPresent t Và toàn bộ hàm Present t tạo kiểu là c. Chúng ta có thể coi hàm Present t tạo kiểu là c bởi vì nó là kiểu không bao giờ thay đổi. Chúng ta không thay đổi Kiểu thẻ trong bất kỳ hàm nào. Nhưng chúng ta sửa đổi Kiểu nội dung trong hàm replace. Khi chúng ta sử dụng replace, Kiểu nội dung có thể thay đổi từ a thành b, vì vậy chúng ta không thể coi chúng là Kiểu không đổi như t. Đó là lý do tại sao chúng là tham số cho c hàm tạo kiểu, vì vậy chúng ta có thể thay đổi kiểu trong hàm replace nếu cần.

Giống như trước đây, việc triển khai thực tế các hàm là đơn giản.

Và để lấy phần thưởng từ công việc của chúng ta, đây là một vài ví dụ sử dụng các hành vi Lớp Kiểu mới của chúng ta:

Has False `contains` False         -- True

isEmpty (Has 'a') -- False

PresentFor "Tommy" 5 `contains` 5 -- True

PresentFor "Tommy" 5 `replace` "Arduino" -- PresentFor "Tommy" "Arduino"


guessWhat'sInside :: (Container a, Eq b) => a b -> b -> String
guessWhat'sInside x y =
if x `contains` y
then "You're right!!"
else "WROOONG!"

guessWhat'sInside (PresentFor "Mary" "A Raspberry Pi!") "A Ponny!" -- **Mary's Dissapointment increasses**
guessWhat'sInside (Has 1) 15
Kết quả

True

False

True

PresentFor "Tommy" "Arduino"

"WROOONG!"

"WROOONG!"

Hiểu Lớp Kiểu này và các thực thể là phần khó nhất của bài học. Có thể mất một lúc để hiểu đầy đủ những gì chúng ta vừa thấy. Nhưng đừng lo lắng, nếu một cái gì đó không nhấp, nó sẽ làm với một số thực hành. Đó là lý do tại sao điều quan trọng là phải làm bài tập về nhà.

Bây giờ, hãy tìm hiểu về phân lớp. Sau tất cả những gì chúng ta đã trải qua, đây là một miếng bánh.

Khám phá Ord Lớp Kiểu (Subclassing)

Chúng ta chưa bao giờ nói về phân lớp (subclassing) trước đây, nhưng bạn đã biết nó hoạt động như thế nào.

Chúng ta hãy xem nó trong thực tế khi định nghĩa một thực thể cho Ord Lớp Kiểu.

Nếu chúng ta chạy lệnh info trên Ord Lớp Kiểu (:i Ord), chúng ta sẽ nhận được kết quả như sau:

type Ord :: * -> Constraint         -- Takes a concreate type
class Eq a => Ord a where -- That "Eq a =>" is new!! 🤔
compare :: a -> a -> Ordering
(<) :: a -> a -> Bool -- A bunch of functions
(<=) :: a -> a -> Bool
(>) :: a -> a -> Bool
(>=) :: a -> a -> Bool
max :: a -> a -> a
min :: a -> a -> a
{-# MINIMAL compare | (<=) #-} -- We can only implement "compare" or "<=".

Tất cả mọi thứ kiểm tra ra. Ngoại trừ điều đó Eq a =>. Chúng ta đã thấy điều này trong cả hàm và các trường hợp. Nhưng chưa bao giờ thấy trên định nghĩa Lớp Kiểu.

Điều này (Eq a =>) có nghĩa là những gì bạn tưởng tượng:

tip

Để biến một kiểu a thành thực thể của Ord, trước tiên chúng ta phải biến nó thành một thực thể của Eq ! Có nghĩa là đó Eq là điều kiện tiên quyết cho Ord

Nói cách khác, Eq là lớp cha của Ord hoặc Ord là lớp con của Eq.

Các siêu lớp cho phép suy ra các khai báo đơn giản hơn. Bằng cách nói rằng một Kiểu là một thực thể của Ord, chúng ta không chỉ biết rằng nó có các hành vi của Ord, mà còn cả các hành vi của Eq. Ngoài ra, điều này cho phép chúng ta sử dụng các hành vi của Lớp Kiểu Eq để xác định các thực thể của Ord Lớp Kiểu. Đó thực sự là một cái gì đó xảy ra trong trường hợp này. Lớp Ord kiểu sử dụng các hàm do Lớp Kiểu Eq cung cấp.

Chúng ta không thể nhìn thấy nó vì lệnh thông tin không hiển thị toàn bộ định nghĩa Lớp Kiểu. Tương tự như khi chúng ta chạy lệnh info cho Lớp Kiểu Eq, nó không hiển thị các định nghĩa đệ quy lẫn nhau của == và /= mà chúng ta vừa viết.

Tuy nhiên, mặc dù chúng ta không thể nhìn thấy chúng, nhưng chúng ta biết rằng có rất nhiều định nghĩa hàm được định nghĩa theo thuật ngữ của nhau. Đó là lý do tại sao chúng ta có thể triển khai toàn bộ thực thể bằng cách chỉ xác định compare hoặc <=.

Lệnh thông tin không hiển thị tất cả mã đó vì chúng ta, các nhà phát triển, không quan tâm đến nó. Chúng ta chỉ muốn biết:

  • Những hành vi nào đi kèm với Lớp Kiểu. Để xem nếu đó là những gì chúng ta cần.
  • Loại của Lớp Kiểu và các hành vi tối thiểu chúng ta cần thực hiện. Để chỉ thực hiện những điều đó.
  • Nếu nó phụ thuộc vào Lớp Kiểu khác. Để thực hiện cái đó trước cái này.
  • Và cuối cùng, những kiểu nào đã là một thực thể của Lớp Kiểu này. Để xem Kiểu nào đã có thể sử dụng những hành vi đó.

Và đó là những gì lệnh thông tin cho chúng ta thấy.

Vì vậy, để biến một kiểu thành thực thể của Ord, trước tiên, chúng ta phải biến nó thành một thực thể của Eq. May mắn thay, chúng ta đã tạo một vài phiên bản Eq trước đó, vì vậy chúng ta đã hoàn thành được nửa chặng đường nếu muốn tạo Ord phiên bản cho bất kỳ Kiểu nào trong số đó.

Ví dụ, nếu chúng ta muốn tạo một thực thể của Box a lớp Ord kiểu, chúng ta phải triển khai một trong các hàm cần thiết cho định nghĩa đầy đủ tối thiểu! Trong trường hợp này, chúng ta đã chọn hàm compare:

-- type Ord :: * -> Constraint  
-- class Eq a => Ord a where
-- compare :: a -> a -> Ordering

instance (Ord a) => Ord (Box a) where
Has x `compare` Has y = x `compare` y
Empty `compare` Has _ = LT
Has _ `compare` Empty = GT
Empty `compare` Empty = EQ


Has 9 >= Has 5
Empty `compare` Has 0
Empty < Empty

True

LT

False

Đây là những gì đang xảy ra ở đây:

  • Nếu cả hai hộp có một số giá trị bên trong, chúng ta sẽ so sánh các giá trị. Và bởi vì chúng ta đang áp dụng hàm compare cho x và y thuộc Kiểu a, nên chúng ta cần thêm ràng buộc rằng Kiểu a phải là một thực thể của Ord.
  • Nếu một trong các hộp có Empty và hộp kia không, thì bên trong hộp có gì không quan trọng. Nó sẽ luôn luôn lớn hơn một Empty. Bởi vì tôi nói thế.
  • Empty Tất nhiên, nếu cả hai đều bằng nhau.

Chúng ta tạo ra:

  • Loại Eq lớp với 3 trường hợp khác nhau.
  • Lớp WeAccept Kiểu với 4 trường hợp.
  • Sau đó, Container Lớp Kiểu với 3 trường hợp.
  • Và cuối cùng, chúng ta tạo một kiểu là một thực thể của Lớp Kiểu Ord.

Chúc mừng! 🎉 Bạn biết mọi thứ cần thiết để làm việc với các Lớp Kiểu!!

Trong phần cuối cùng của bài học này, chúng ta sẽ tìm hiểu cách thức và thời điểm tự động lấy các phiên bản. Tiết kiệm cho chúng ta một chút thời gian quý báu và giảm số lượng mã chúng ta phải duy trì.

Deriving

Các thực thể có nguồn gốc là một cách tự động để biến một kiểu thành một thực thể thành một Lớp Kiểu. Điều này là có thể bởi vì nhiều Lớp Kiểu phổ biến thường được thực hiện theo cùng một cách. Và một số người thông minh với bằng tiến sĩ đã tìm ra cách tạo mã này dựa trên định nghĩa của Kiểu.

Điều này được giới hạn ở EqOrdEnumShow và những thứ khác được xác định trong Prelude hoặc thư viện tiêu chuẩn—các thư viện đi kèm với Haskell và chúng ta sẽ khám phá trong các bài học sau. Bây giờ, hãy nghĩ rằng tất cả các Lớp Kiểu mà chúng ta đã sử dụng cho đến bây giờ và chúng ta không tự tạo ra chúng đều có thể được dẫn xuất.

Để sử dụng tính năng này, hãy thêm deriving từ khóa vào cuối khai báo kiểu của bạn với tên của tất cả các Lớp Kiểu mà bạn muốn dẫn xuất. Ví dụ: nếu chúng ta làm điều này:

data Choice = No | Idk | Yes deriving (Eq, Ord, Show, Bounded, Enum)

Yes /= No -- Are these values different? (Behavior from Eq)

Yes > No -- Is Yes bigger than No? (Behavior from Ord)

show Yes -- Transform Yes to String (Behavior from Show)

(minBound) :: Choice -- Smallest value of type Choice (Behavior from Bounded)

succ No -- Successor of No (Behavior from Enum)

True

True

"Yes"

No

Idk

Và thế là xong!! Loại của bạn Choice có các hành vi được cung cấp bởi tất cả các Lớp Kiểu đó.

Vì vậy, nếu chúng ta có thể làm điều đó ngay từ đầu, thì tại sao bạn lại quan tâm đến việc tạo ra các phiên bản thủ công?

Chà… Một lý do là không phải tất cả các Lớp Kiểu đều có thể được dẫn xuất. Và một điều nữa là việc bắt nguồn đôi khi có thể sai.

Deriving có thể đi sai

Mỗi Lớp Kiểu có bộ quy tắc riêng để dẫn xuất các thực thể. Ví dụ: khi lấy Ord Kiểu, các hàm tạo giá trị được xác định trước đó sẽ nhỏ hơn. Vì vậy, trong trường hợp này:

data PaymentMethod = Cash | Card | CC deriving (Eq, Ord)

Cash > Card
Card < CC
CC `compare` Cash

False

True

GT

Lưu ý: Cash nhỏ hơn Card, nhỏ hơn CC.

Và trong trường hợp này:

data Box a = Empty | Has a deriving (Eq, Ord)

Has 5 `compare` Has 6
Has "Hi" >= Has "Hello!"

LT

True

Nếu một hàm tạo giá trị có một tham số (Has a) và hai giá trị được tạo từ cùng một hàm tạo (Has 5 và Has 6), thì các tham số sẽ được so sánh (giống như chúng ta đã làm khi tự định nghĩa các thực thể).

Đó là những quy tắc mà trình biên dịch tuân theo để tự động tạo Ord thực thể cho kiểu của bạn. Các Lớp Kiểu khác có các quy tắc khác. Chúng ta sẽ không đi qua các quy tắc của từng Lớp Kiểu, nhưng tôi sẽ cung cấp một liên kết với một lời giải thích ngắn trong bài học tương tác. Trong trường hợp bạn muốn tìm hiểu thêm.

Bây giờ, giả sử chúng ta muốn sử dụng một Kiểu để quản lý độ dài cho phần mềm Kỹ thuật dân dụng.

Chúng ta làm việc với cả mét và kilômét, nhưng vì chúng ta không muốn vô tình trộn lẫn chúng và gặp lỗi nghiêm trọng tiềm ẩn, nên chúng ta xác định một Kiểu dữ liệu có hai hàm tạo. Một cho mét và một cho km. Cả hai đều chứa một giá trị của Kiểu Double. Chúng ta cũng sẽ lấy được Lớp kiểu Eq.

data Length = M Double | Km Double deriving (Eq)

Tuy nhiên, ngay khi bắt đầu sử dụng kiểu dữ liệu này, chúng ta phát hiện ra một vấn đề nhỏ. Chúng ta biết rằng 1000 mét bằng 1 km, nhưng khi chúng ta kiểm tra điều này trong mã của mình, chúng ta nhận thấy rằng không phải vậy!:

M 1000 == Km 1 -- False

False

Đó là bởi vì khi chúng ta dẫn xuất Eq, Haskell đã tạo mã này:

instance Eq Length where
(==) (M x) (M y) = x == y
(==) (Km x) (Km y) = x == y
(==) _ _ = False

Điều này rất hiệu quả nếu chúng ta so sánh mét với mét và kilômét với kilômét. Nhưng chúng ta đã triển khai sai để so sánh giữa các hàm tạo vì Haskell không biết rằng giá trị của các hàm tạo khác nhau có liên quan theo bất kỳ cách nào!! Haskell chỉ giả định rằng nếu các hàm tạo khác nhau, thì các giá trị cũng vậy!

Vì vậy, trong trường hợp này, chúng ta phải tự viết thực thể để tính đến mối quan hệ giữa các hàm tạo. Như thế này:

data Length = M Double | Km Double

instance Eq Length where
(==) (M x) (M y) = x == y
(==) (Km x) (Km y) = x == y
(==) (M x) (Km y) = x == 1000 * y
(==) (Km x) (M y) = x * 1000 == y


M 3000 == Km 3 -- True
Km 7 /= M 14 -- True

True

True

Đó là lý do tại sao nên ý thức về cách mỗi Lớp Kiểu được dẫn xuất. Để biết khi nào bạn có thể lấy chúng và khi nào bạn phải viết ví dụ bằng tay.

Và để kết thúc bài học, đây là một số mẹo để viết mã trong thế giới thực:

Mẹo viết mã trong thế giới thực

  • Tất cả mọi thứ tôi giải thích ở đây hôm nay áp dụng cho tất cả các Lớp Kiểu.
  • Chúng ta không định nghĩa các Lớp Kiểu thường xuyên. Thông thường, những thứ đi kèm với Haskell là tất cả những gì chúng ta cần.
  • Chúng ta triển khai các phiên bản khá nhiều. Và nó thường (nhưng không phải luôn luôn) là một ý tưởng hay để lấy chúng. Nếu bạn nghi ngờ, hãy thử tính toán tự động và kiểm tra các giả định của bạn. Bạn luôn có thể quay lại và xác định các phiên bản theo cách thủ công.
  • Bạn có thể xem qua định nghĩa Lớp Kiểu bằng cách sử dụng :i trên GHCi để xem các hành vi tối thiểu cần triển khai khi tạo phiên bản của mình. Thực hiện những điều đó, và bạn đã hoàn tất.

Nguồn bài viết tại đây


Picture