5 bước tạo Lớp Loại đầu tiên của bạn trong Scala

Trong bài đăng trên blog này, bạn sẽ tìm hiểu về cách triển khai lớp loại đầu tiên của mình, đây là tính năng ngôn ngữ cơ bản trong biểu tượng của ngôn ngữ lập trình chức năng - Haskell.

Ảnh của Stanley Dai trên Bapt

Type Class là một mẫu có nguồn gốc từ Haskell và đó là cách tiêu chuẩn để thực hiện đa hình. Loại đa hình này được gọi là đa hình ad-hoc. Tên của nó xuất phát từ thực tế là trái với đa hình gõ phụ nổi tiếng, chúng ta có thể mở rộng một số chức năng của thư viện ngay cả khi không có quyền truy cập vào mã nguồn của thư viện và lớp mà chúng ta muốn mở rộng.

Trong bài viết này, bạn sẽ thấy rằng việc sử dụng các lớp loại có thể thuận tiện như sử dụng đa hình OOP thông thường. Nội dung bên dưới sẽ dẫn bạn qua tất cả các giai đoạn triển khai mẫu Loại lớp để giúp bạn hiểu rõ hơn về nội bộ của các thư viện lập trình chức năng.

Tạo lớp loại đầu tiên của bạn

Về mặt kỹ thuật Class Class chỉ là một đặc điểm được tham số hóa với số phương thức trừu tượng có thể được thực hiện trong các lớp mở rộng đặc điểm đó. Cho đến nay mọi thứ trông thực sự giống như trong mô hình gõ phụ nổi tiếng.
Sự khác biệt duy nhất là với sự trợ giúp của việc gõ phụ, chúng ta cần thực hiện hợp đồng trong các lớp là một phần của mô hình miền, trong lớp Loại thực hiện tính trạng được đặt trong lớp hoàn toàn khác nhau được liên kết với lớp miền miền cứng theo tham số loại.

Như một ví dụ trong bài viết này, tôi sẽ sử dụng Lớp Loại Eq từ thư viện Mèo.

đặc điểm phương trình [A] {
  def areEquals (a: A, b: A): Boolean
}

Loại lớp Eq [A] là một hợp đồng có khả năng kiểm tra xem hai đối tượng loại A có bằng nhau hay không dựa trên một số tiêu chí được thực hiện trong phương thức areEquals.

Tạo cá thể của Lớp Loại của chúng tôi đơn giản như lớp khởi tạo mở rộng đặc điểm được đề cập chỉ với một điểm khác biệt là thể hiện của lớp loại của chúng tôi sẽ có thể truy cập được dưới dạng đối tượng ẩn.

def moduloEq (ước số: Int): Eq [Int] = new Eq [Int] {
 ghi đè def areEquals (a: Int, b: Int) = a% divisor == b% ước số
}
ẩn val modulo5Eq: Eq [Int] = moduloEq (5)

Đoạn mã trên có thể được nén một chút theo mẫu sau.

def moduloEq: Eq [Int] = (a: Int, b: Int) => a% 5 == b% 5

Nhưng chờ đã, làm thế nào bạn có thể gán hàm (Int, Int) => Boolean để tham chiếu với loại Eq [Int]?! Điều này có thể thực hiện được nhờ tính năng Java 8 được gọi là loại giao diện Phương thức trừu tượng đơn. Chúng ta có thể làm một điều như vậy khi chúng ta chỉ có một phương pháp trừu tượng trong đặc điểm của chúng ta.

Loại độ phân giải

Trong đoạn này, tôi sẽ chỉ cho bạn cách sử dụng các thể hiện của lớp loại và cách liên kết một cách kỳ diệu với loại lớp Eq [A] với đối tượng tương ứng của loại A khi cần thiết.

Ở đây, chúng tôi đã triển khai chức năng so sánh hai giá trị Int bằng cách kiểm tra xem giá trị phân chia modulo của chúng có bằng nhau không. Với tất cả công việc đã hoàn thành, chúng tôi có thể sử dụng Lớp Loại của mình để thực hiện một số logic nghiệp vụ, ví dụ: chúng tôi muốn ghép hai giá trị bằng nhau.

def cặpEquals [A] (a: A, b: A) (ẩn eq: Eq [A]): ​​Tùy chọn [(A, A)] = {
 if (eq.areEquals (a, b)) Một số ((a, b)) khác Không
}

Chúng tôi tích hợp cặp hàm tham số tham số để làm việc với bất kỳ loại nào cung cấp thể hiện của lớp Eq [A] có sẵn trong phạm vi ẩn của nó.

Khi trình biên dịch thắng won tìm thấy bất kỳ trường hợp nào khớp với khai báo ở trên, nó sẽ kết thúc với cảnh báo lỗi biên dịch về việc thiếu thể hiện đúng trong phạm vi ẩn được cung cấp.
  1. Trình biên dịch sẽ suy ra loại tham số được cung cấp bằng cách áp dụng các đối số cho hàm của chúng ta và gán nó cho bí danh A.
  2. Đối số trước eq: Eq [A] với từ khóa ẩn sẽ kích hoạt đề xuất để tìm kiếm đối tượng của loại Eq [A] trong phạm vi ẩn.

Nhờ có các tham số và các tham số đã gõ, trình biên dịch có thể liên kết cùng lớp với thể hiện của lớp loại tương ứng.

Tất cả các phiên bản và chức năng đều được xác định, hãy để kiểm tra nếu mã của chúng tôi cho kết quả hợp lệ

cặpEquals (2,7)
res0: Tùy chọn [(Int, Int)] = Một số ((2,7))
cặpEquals (2,3)
res0: Tùy chọn [(Int, Int)] = Không có

Như bạn thấy, chúng tôi đã nhận được kết quả mong đợi vì vậy lớp loại của chúng tôi đang hoạt động tốt. Nhưng cái này có vẻ hơi lộn xộn, với số lượng nồi hơi vừa phải. Nhờ vào phép thuật của cú pháp Scala, chúng ta có thể biến mất rất nhiều bản tóm tắt.

Giới hạn bối cảnh

Điều đầu tiên tôi muốn cải thiện trong mã của chúng tôi là loại bỏ danh sách đối số thứ hai (với từ khóa ẩn). Chúng ta không truyền trực tiếp cái đó khi gọi hàm, vì vậy hãy ẩn ngầm một lần nữa. Trong Scala, các đối số ngầm với các tham số kiểu có thể được thay thế bằng cấu trúc ngôn ngữ có tên là Bối cảnh.

Context Bound là khai báo trong danh sách tham số kiểu mà cú pháp A: Eq nói rằng mọi loại được sử dụng làm đối số của hàm cặpEquals phải có giá trị ngầm định của loại Eq [A] trong phạm vi ẩn.

def cặpEquals [A: Eq] (a: A, b: A): Tùy chọn [(A, A)] = {
 if (ngầm [Eq [A]]. areEquals (a, b)) Một số ((a, b)) khác Không
}

Như bạn đã nhận thấy, chúng tôi đã kết thúc mà không có tham chiếu nào chỉ ra giá trị ngầm định. Để khắc phục vấn đề này, chúng tôi đang sử dụng hàm ngầm định [F [_]] để tìm giá trị ngầm định bằng cách chỉ định loại mà chúng tôi đề cập đến.

Đây là những gì ngôn ngữ Scala cung cấp cho chúng tôi để làm cho tất cả ngắn gọn hơn. Mặc dù vậy, nó vẫn không đủ tốt cho tôi. Bối cảnh là một đường cú pháp thực sự mát mẻ, nhưng điều này dường như làm ô nhiễm mã của chúng tôi. Tôi sẽ thực hiện một mẹo hay để khắc phục vấn đề này và giảm mức độ chi tiết thực hiện của chúng tôi.

Những gì chúng ta có thể làm là cung cấp hàm áp dụng được tham số hóa trong đối tượng đồng hành của lớp loại của chúng ta.

đối tượng {
 def áp dụng [A] (ẩn eq: Eq [A]): ​​Eq [A] = eq
}

Điều thực sự đơn giản này cho phép chúng ta thoát khỏi ngầm và kéo cá thể của chúng ta khỏi tình trạng lấp lửng để được sử dụng trong logic miền mà không cần nồi hơi.

def cặpEquals [A: Eq] (a: A, b: A): Tùy chọn [(A, A)] = {
 if (Eq [A] .areEquals (a, b)) Một số ((a, b)) khác Không
}

Chuyển đổi ngầm định - aka. Mô đun cú pháp

Điều tiếp theo tôi muốn có trên bàn làm việc của mình là Eq [A] .areEquals (a, b). Cú pháp này trông rất dài dòng vì chúng ta đề cập rõ ràng đến thể hiện của lớp nên được ẩn, phải không? Điều thứ hai là ở đây, thể hiện lớp loại của chúng ta hoạt động như Service (theo nghĩa DDD) thay vì phần mở rộng lớp A thực. May mắn thay, người ta cũng có thể sửa lỗi với sự trợ giúp của việc sử dụng từ khóa ẩn khác.

Những gì chúng ta sẽ làm ở đây là cung cấp mô-đun được gọi là cú pháp hoặc (ops như trong một số thư viện FP) bằng cách sử dụng các chuyển đổi ngầm cho phép chúng ta mở rộng API của một số lớp mà không sửa đổi mã nguồn của nó.

lớp ẩn EqSyntax [A: Eq] (a: A) {
 def === (b: A): Boolean = Eq [A] .areEquals (a, b)
}

Mã này báo cho trình biên dịch chuyển đổi lớp A có thể hiện của lớp Eq [A] thành lớp EqSyntax có một hàm ===. Tất cả những điều này tạo ấn tượng rằng chúng tôi đã thêm chức năng === vào lớp A mà không cần sửa đổi mã nguồn.

Chúng tôi không chỉ ẩn tham chiếu cá thể lớp loại mà còn cung cấp nhiều cú pháp lớp khác, điều này tạo ấn tượng về phương thức === được thực hiện trong lớp A ngay cả khi chúng tôi không biết gì về lớp này. Hai con chim bị giết bằng một hòn đá.

Bây giờ chúng tôi được phép áp dụng phương thức === để gõ A bất cứ khi nào chúng tôi có lớp EqSyntax trong phạm vi. Bây giờ việc triển khai cặpEquals của chúng tôi sẽ thay đổi một chút và sẽ như sau.

def cặpEquals [A: Eq] (a: A, b: A): Tùy chọn [(A, A)] = {
 if (a === b) Một số ((a, b)) khác Không
}

Như tôi đã hứa, chúng tôi đã kết thúc việc triển khai trong đó sự khác biệt duy nhất có thể nhìn thấy so với triển khai OOP là chú thích Bối cảnh sau tham số loại A. Tất cả các khía cạnh kỹ thuật của lớp loại được tách ra khỏi logic miền của chúng tôi. Điều đó có nghĩa là bạn có thể đạt được nhiều thứ hay ho hơn (mà tôi sẽ đề cập trong bài viết riêng những gì sẽ sớm được công bố) mà không làm tổn thương mã của bạn.

Phạm vi ngầm

Như bạn thấy các lớp loại trong Scala phụ thuộc hoàn toàn vào việc sử dụng tính năng ẩn, do đó, điều cần thiết là phải hiểu cách làm việc với phạm vi ẩn.

Phạm vi ngầm định là một phạm vi trong đó trình biên dịch sẽ tìm kiếm các trường hợp ẩn. Có nhiều sự lựa chọn vì vậy cần phải xác định một thứ tự mà các trường hợp được tìm kiếm. Thứ tự như sau:

1. Trường hợp địa phương và kế thừa
2. Phiên bản nhập khẩu
3. Các định nghĩa từ đối tượng đồng hành của lớp loại hoặc các tham số

Điều này rất quan trọng bởi vì khi trình biên dịch tìm thấy một số trường hợp hoặc không phải là phiên bản nào cả, nó sẽ phát sinh lỗi. Đối với tôi cách thuận tiện nhất để nhận các thể hiện của các lớp loại là đặt chúng trong đối tượng đồng hành của chính loại lớp. Nhờ đó, chúng tôi không cần phải bận tâm đến việc nhập hoặc triển khai các trường hợp tại chỗ cho phép chúng tôi quên đi các vấn đề về vị trí. Tất cả mọi thứ được cung cấp một cách kỳ diệu bởi trình biên dịch.

Vì vậy, hãy thảo luận về điểm 3 bằng cách sử dụng ví dụ về chức năng nổi tiếng từ thư viện tiêu chuẩn Scala, đã sắp xếp chức năng nào dựa trên các bộ so sánh được cung cấp ngầm.

đã sắp xếp [B>: A] (ord ẩn: math.Ordering [B]): List [A]

Loại thể hiện lớp sẽ được tìm kiếm trong:
 * Đặt hàng đối tượng đồng hành
 * Liệt kê đối tượng đồng hành
 * Đối tượng đồng hành B (cũng có thể là Đối tượng đồng hành vì tồn tại định nghĩa giới hạn dưới)

Simulacrum

Tất cả những điều đó giúp ích rất nhiều khi sử dụng mẫu lớp nhưng đây là công việc lặp lại phải được thực hiện trong mọi dự án. Những manh mối này là một dấu hiệu rõ ràng cho thấy quá trình có thể được trích xuất vào thư viện. Có một thư viện dựa trên macro tuyệt vời được gọi là Simulacrum, xử lý tất cả những thứ cần thiết để tạo mô-đun cú pháp (được gọi là ops trong Simulacrum), v.v.

Thay đổi duy nhất chúng tôi nên giới thiệu là chú thích @typeclass là dấu hiệu cho các macro để mở rộng mô-đun cú pháp của chúng tôi.

nhập simulacrum._
@typeclass tính trạng Eq [A] {
 @op (Hồi === Lần) def areEquals (a: A, b: A): Boolean
}

Các phần khác trong quá trình thực hiện của chúng tôi don đòi hỏi bất kỳ thay đổi. Đó là tất cả. Bây giờ bạn đã biết cách tự triển khai mẫu lớp trong Scala và tôi hy vọng bạn có được nhận thức về cách các thư viện hoạt động như Simulacrum.

Cảm ơn bạn đã đọc, tôi sẽ thực sự đánh giá cao bất kỳ phản hồi nào từ bạn và tôi sẽ mong gặp bạn trong tương lai với một bài báo được xuất bản khác.