Nhập các lớp trong Scala3: Hướng dẫn cho người mới bắt đầu | Sổ cái

Nhập các lớp trong Scala3: Hướng dẫn cho người mới bắt đầu | Sổ cái

Nhập các lớp trong Scala3: Hướng dẫn cho người mới bắt đầu | Sổ cái PlatoThông tin dữ liệu Blockchain. Tìm kiếm dọc. Ái.

Tài liệu này dành cho nhà phát triển Scala3 mới bắt đầu, người đã thành thạo văn xuôi Scala, nhưng đang bối rối về tất cả `implicits` và các đặc điểm được tham số hóa trong mã.

Tài liệu này giải thích lý do tại sao, như thế nào, ở đâu và khi nào Loại lớp (TC).

Sau khi đọc tài liệu này, nhà phát triển Scala3 mới bắt đầu sẽ có được kiến ​​thức vững chắc để sử dụng và đi sâu vào mã nguồn của rất nhiều của các thư viện Scala và bắt đầu viết mã Scala thành ngữ.

Hãy bắt đầu với lý do tại sao…

Vấn đề biểu hiện

Trong 1998, Philip Wadler đã nêu rằng “bài toán biểu thức là một tên mới cho một bài toán cũ”. Đó là vấn đề về khả năng mở rộng phần mềm. Theo ông Wadler viết, lời giải bài toán biểu thức phải tuân theo các quy tắc sau:

  • Quy tắc 1: Cho phép thực hiện hành vi hiện có (nghĩ về đặc điểm Scala) được áp dụng cho đại diện mới (nghĩ về một trường hợp lớp)
  • Quy tắc 2:  Cho phép triển khai hành vi mới được áp dụng cho đại diện hiện có
  • Quy tắc 3: Nó không được gây nguy hiểm cho loại an toàn
  • Quy tắc 4: Không cần phải biên dịch lại mã hiện có

Giải quyết vấn đề này sẽ là chủ đề bạc của bài viết này.

Quy tắc 1: thực hiện hành vi hiện có trên biểu diễn mới

Bất kỳ ngôn ngữ hướng đối tượng nào cũng có giải pháp tích hợp cho quy tắc 1 với đa hình kiểu phụ. Bạn có thể thực hiện một cách an toàn bất kỳ `trait` được xác định trong phần phụ thuộc vào `class` bằng mã của riêng bạn mà không cần biên dịch lại phần phụ thuộc. Hãy xem điều đó trong thực tế:

Scala

def todo = 42
type Height = Int
type Block = Int

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo


object Lib2:
 import Lib1.*

 case class Polkadot() extends Blockchain:
 override def getBlock(height: Height): Block = todo

val eth = Lib1.Ethereum()
val btc = Lib1.Bitcoin()
val dot = Lib2.Polkadot()

Trong ví dụ hư cấu này, thư viện `Lib1` (dòng 5) xác định một đặc điểm `Blockchain` (dòng 6) với 2 cách triển khai của nó (dòng 9 & 12). `Lib1` sẽ giữ nguyên trong TẤT CẢ tài liệu này (thực thi quy tắc 4).

`Lib2`(dòng 15) thực hiện hành vi hiện có`Blockchain`trên một lớp mới`Polkadot` (quy tắc 1) theo kiểu an toàn (quy tắc 3), không cần biên dịch lại `Lib1`(quy tắc 4). 

Quy tắc 2: thực hiện các hành vi mới sẽ được áp dụng cho các biểu diễn hiện có

Hãy tưởng tượng trong `Lib2`chúng tôi muốn có một hành vi mới`lastBlock` được triển khai cụ thể cho từng `Blockchain`.

Điều đầu tiên bạn nghĩ đến là tạo một công tắc lớn dựa trên loại tham số.

Scala

def todo = 42
type Height = Int
type Block = Int

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo

object Lib2:
 import Lib1.*

 case class Polkadot() extends Blockchain:
 override def getBlock(height: Height): Block = todo

 def lastBlock(blockchain: Blockchain): Block = blockchain match
 case _:Ethereum => todo
 case _:Bitcoin => todo
 case _:Polkadot => todo
 

object Lib3:
 import Lib1.*

 case class Polygon() extends Blockchain:
 override def getBlock(height: Height): Block = todo

import Lib1.*, Lib2.*, Lib3.*
println(lastBlock(Bitcoin()))
println(lastBlock(Ethereum()))
println(lastBlock(Polkadot()))
println(lastBlock(Polygon()))

Giải pháp này là sự triển khai lại yếu kém của tính đa hình dựa trên kiểu đã có sẵn trong ngôn ngữ!

`Lib1` không bị ảnh hưởng (hãy nhớ, quy tắc 4 được thực thi trong toàn bộ tài liệu này). 

Giải pháp được triển khai trong `Lib2` là ổn cho đến khi một blockchain khác được giới thiệu trong `Lib3`. Nó vi phạm quy tắc an toàn loại (quy tắc 3) vì mã này bị lỗi khi chạy trên dòng 37. Và sửa đổi `Lib2` sẽ vi phạm quy tắc 4.

Một giải pháp khác là sử dụng `extension`.

Scala

def todo = 42
type Height = Int
type Block = Int

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo

object Lib2:
 import Lib1.*

 case class Polkadot() extends Blockchain:
 override def getBlock(height: Height): Block = todo

 def lastBlock(): Block = todo

 extension (eth: Ethereum) def lastBlock(): Block = todo

 extension (btc: Bitcoin) def lastBlock(): Block = todo

import Lib1.*, Lib2.*
println(Bitcoin().lastBlock())
println(Ethereum().lastBlock())
println(Polkadot().lastBlock())

def polymorphic(blockchain: Blockchain) =
 // blockchain.lastBlock()
 ???

`Lib1` không bị ảnh hưởng (thực thi quy tắc 4 trong toàn bộ tài liệu). 

`Lib2` xác định hành vi cho loại của nó (dòng 21) và `phần mở rộng` cho loại hiện có (dòng 23 & 25).

Dòng 28-30, hành vi mới có thể được sử dụng trong mỗi lớp. 

Nhưng không có cách nào gọi hành vi mới này là đa hình (dòng 32). Bất kỳ nỗ lực nào để làm như vậy đều dẫn đến lỗi biên dịch (dòng 33) hoặc chuyển đổi dựa trên kiểu gõ. 

Quy tắc số 2 này rất phức tạp. Chúng tôi đã cố gắng triển khai nó bằng định nghĩa riêng của chúng tôi về tính đa hình và thủ thuật `mở rộng`. Và điều đó thật kỳ lạ.

Có một phần còn thiếu tên là đa hình đặc biệt: khả năng gửi một cách an toàn việc triển khai hành vi theo một loại, bất cứ nơi nào hành vi và loại được xác định. Nhập Loại lớp mô hình.

Mẫu lớp loại

Công thức mẫu Loại Loại (viết tắt là TC) có 3 bước. 

  1. Xác định hành vi mới
  2. Thực hiện hành vi
  3. Sử dụng hành vi

Trong phần sau, tôi triển khai mẫu TC theo cách đơn giản nhất. Nó dài dòng, vụng về và không thực tế. Nhưng chờ đã, những lưu ý đó sẽ được khắc phục từng bước một trong tài liệu.

1. Xác định hành vi mới
Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

`Lib1`, một lần nữa, vẫn được giữ nguyên.

Hành vi mới is TC được cụ thể hóa bởi đặc điểm. Các chức năng được xác định trong đặc điểm là một cách để áp dụng một số khía cạnh của hành vi đó.

Tham số `A` đại diện cho kiểu mà chúng ta muốn áp dụng hành vi, là kiểu con của `Blockchain` trong trường hợp của chúng tôi.

Một số nhận xét:

  • Nếu cần, loại tham số `A` có thể bị hạn chế hơn nữa bởi hệ thống kiểu Scala. Chẳng hạn, chúng ta có thể thực thi `A` trở thành một `Blockchain`. 
  • Ngoài ra, TC có thể có nhiều chức năng hơn được khai báo trong đó.
  • Cuối cùng, mỗi hàm có thể có nhiều tham số tùy ý hơn.

Nhưng hãy giữ mọi thứ đơn giản để dễ đọc.

2. Thực hiện hành vi
Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 val ethereumLastBlock = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 val bitcoinLastBlock = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

Đối với mỗi loại, ` mớiLastBlock` hành vi được mong đợi, có một trường hợp cụ thể về hành vi đó. 

Các `Ethereum`dòng thực hiện 22 được tính từ `eth` dụ được truyền dưới dạng tham số. 

Việc thực hiện `LastBlock` cho `Bitcoin`dòng 25 được triển khai với IO không được quản lý và không sử dụng tham số của nó.

Vì vậy, `Lib2` thực hiện hành vi mới `LastBlock` cho `Lib1` các lớp học.

3. Sử dụng hành vi
Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 val ethereumLastBlock = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 val bitcoinLastBlock = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

import Lib1.*, Lib2.*

def useLastBlock[A](instance: A, behavior: LastBlock[A]) =
 behavior.lastBlock(instance)

println(useLastBlock(Ethereum(lastBlock = 2), ethereumLastBlock))
println(useLastBlock(Bitcoin(), bitcoinLastBlock))

Dòng 30`useLastBlock` sử dụng một thể hiện của `A` và `LastBlock` hành vi được xác định cho trường hợp đó.

Dòng 33`useLastBlock` được gọi với một thể hiện của `Ethereum` và việc triển khai `LastBlock` được xác định trong `Lib2`. Lưu ý rằng có thể chuyển bất kỳ triển khai thay thế nào của `LastBlock[A]`(nghĩ tới tiêm phụ thuộc).

`useLastBlock` là chất kết dính giữa biểu diễn (A thực tế) và hành vi của nó. Dữ liệu và hành vi được tách biệt, đó là điều mà lập trình chức năng ủng hộ.

Thảo luận

Hãy tóm tắt lại các quy tắc của bài toán biểu thức:

  • Quy tắc 1: Cho phép thực hiện hành vi hiện có  được áp dụng cho lớp học mới
  • Quy tắc 2:  Cho phép triển khai hành vi mới được áp dụng cho các lớp hiện có
  • Quy tắc 3: Nó không được gây nguy hiểm cho loại an toàn
  • Quy tắc 4: Không cần phải biên dịch lại mã hiện có

Quy tắc 1 có thể được giải quyết ngay lập tức bằng tính đa hình kiểu con.

Mẫu TC vừa trình bày (xem ảnh chụp màn hình trước đó) giải quyết quy tắc 2. Đó là loại an toàn (quy tắc 3) và chúng tôi chưa bao giờ chạm vào `Lib1`(quy tắc 4). 

Tuy nhiên, nó không thực tế để sử dụng vì một số lý do:

  • Dòng 33-34 chúng ta phải chuyển hành vi một cách rõ ràng dọc theo thể hiện của nó. Đây là một chi phí bổ sung. Chúng ta chỉ nên viết `useLastBlock(Bitcoin())`.
  • Cú pháp dòng 31 không phổ biến. Chúng tôi muốn viết ngắn gọn và hướng đối tượng hơn  `instance.lastBlock()`tuyên bố.

Hãy nêu bật một số tính năng của Scala để sử dụng TC thực tế. 

Trải nghiệm của nhà phát triển nâng cao

Scala có một bộ tính năng và cú pháp độc đáo giúp TC trở thành một trải nghiệm thực sự thú vị cho các nhà phát triển.

Hệ lụy

Phạm vi tiềm ẩn là một phạm vi đặc biệt được giải quyết tại thời điểm biên dịch trong đó chỉ có thể tồn tại một phiên bản của một loại nhất định. 

Một chương trình đặt một thể hiện trong phạm vi tiềm ẩn với `given` từ khóa. Ngoài ra, một chương trình có thể truy xuất một thể hiện từ phạm vi tiềm ẩn bằng từ khóa `using`.

Phạm vi tiềm ẩn được giải quyết tại thời điểm biên dịch, có cách để thay đổi nó một cách linh hoạt khi chạy. Nếu chương trình biên dịch, phạm vi tiềm ẩn sẽ được giải quyết. Trong thời gian chạy, không thể thiếu các phiên bản tiềm ẩn nơi chúng được sử dụng. Sự nhầm lẫn duy nhất có thể xảy ra có thể đến từ việc sử dụng sai phiên bản tiềm ẩn, nhưng vấn đề này được để lại cho sinh vật nằm giữa ghế và bàn phím.

Nó khác với phạm vi toàn cầu vì: 

  1. Nó được giải quyết theo ngữ cảnh. Hai vị trí của một chương trình có thể sử dụng một thể hiện của cùng một loại đã cho trong phạm vi ngầm định, nhưng hai thể hiện đó có thể khác nhau.
  2. Phía sau, mã đang chuyển hàm đối số ngầm sang hàm cho đến khi đạt được mức sử dụng ngầm. Nó không sử dụng không gian bộ nhớ chung.

Quay trở lại lớp loại! Hãy lấy ví dụ tương tự.

Scala

def todo = 42
type Height = Int
type Block = Int
def http(uri: String): Block = todo

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo

`Lib1` là mã chưa sửa đổi mà chúng tôi đã xác định trước đó. 

Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 given ethereumLastBlock:LastBlock[Ethereum] = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given bitcoinLastBlock:LastBlock[Bitcoin] = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

import Lib1.*, Lib2.*

def useLastBlock[A](instance: A)(using behavior: LastBlock[A]) =
 behavior.lastBlock(instance)

println(useLastBlock(Ethereum(lastBlock = 2)))
println(useLastBlock(Bitcoin()))

Dòng 19 một hành vi mới `LastBlock` được xác định, chính xác như chúng ta đã làm trước đây.

Dòng 22 và dòng 25, `val` được thay thế bằng `given`. Cả hai cách triển khai `LastBlock` được đặt trong phạm vi ngầm định.

Dòng 31`useLastBlock` khai báo hành vi `LastBlock` như một tham số ngầm định. Trình biên dịch giải quyết phiên bản thích hợp của `LastBlock` từ phạm vi tiềm ẩn được ngữ cảnh hóa từ vị trí người gọi (dòng 33 và 34). Dòng 28 nhập mọi thứ từ `Lib2`, bao gồm cả phạm vi ngầm định. Vì vậy, trình biên dịch chuyển các thể hiện được xác định dòng 22 và 25 làm tham số cuối cùng của `useLastBlock`. 

Là người dùng thư viện, việc sử dụng lớp loại dễ dàng hơn trước. Dòng 34 và 35, nhà phát triển chỉ phải đảm bảo rằng một phiên bản của hành vi được đưa vào phạm vi ngầm định (và đây có thể chỉ là `import`). Nếu một ẩn ý không phải là `given`mã ở đâu`using` nó, trình biên dịch nói với anh ta.

Khả năng tiềm ẩn của Scala giúp dễ dàng thực hiện nhiệm vụ chuyển các thể hiện của lớp cùng với các thể hiện hành vi của chúng.

Đường tiềm ẩn

Dòng 22 và 25 của mã trước đó có thể được cải thiện hơn nữa! Hãy lặp lại việc triển khai TC.

Scala

given LastBlock[Ethereum] = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

Dòng 22 và 25, nếu tên của instance không được sử dụng thì có thể bỏ qua.

Scala


 given LastBlock[Ethereum] with
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] with
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

Dòng 22 và 25, sự lặp lại của loại có thể được thay thế bằng `with` từ khóa.

Scala

given LastBlock[Ethereum] = _.lastBlock

 given LastBlock[Bitcoin] = _ => http("http://bitcoin/last")

Vì chúng tôi sử dụng một đặc điểm thoái hóa với một hàm duy nhất trong đó nên IDE có thể đề xuất đơn giản hóa mã bằng biểu thức SAM. Mặc dù đúng nhưng tôi không nghĩ đó là cách sử dụng SAM hợp lý, trừ khi bạn tình cờ chơi gôn.

Scala cung cấp cú pháp để hợp lý hóa cú pháp, loại bỏ việc đặt tên, khai báo và loại dư thừa không cần thiết.

Extension

Được sử dụng một cách khôn ngoan, `extension` cơ chế có thể đơn giản hóa cú pháp sử dụng một lớp loại.

Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 given LastBlock[Ethereum] with
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] with
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

 extension[A](instance: A)
 def lastBlock(using tc: LastBlock[A]) = tc.lastBlock(instance)

import Lib1.*, Lib2.*

println(Ethereum(lastBlock = 2).lastBlock)
println(Bitcoin().lastBlock)

Dòng 28-29 một phương thức mở rộng chung `lastBlock` được xác định cho bất kỳ `A` với một `LastBlock` Tham số TC trong phạm vi ngầm định.

Các dòng 33-34 phần mở rộng tận dụng cú pháp hướng đối tượng để sử dụng TC.

Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 given LastBlock[Ethereum] with
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] with
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

 extension[A](instance: A)(using tc: LastBlock[A])
 def lastBlock = tc.lastBlock(instance)
 def penultimateBlock = tc.lastBlock(instance) - 1

import Lib1.*, Lib2.*

val eth = Ethereum(lastBlock = 2)
println(eth.lastBlock)
println(eth.penultimateBlock)

val btc = Bitcoin()
println(btc.lastBlock)
println(btc.penultimateBlock)

Dòng 28, tham số TC cũng có thể được xác định cho toàn bộ phần mở rộng để tránh lặp lại. Dòng 30 chúng tôi sử dụng lại TC trong phần mở rộng để xác định `penultimateBlock` (mặc dù nó có thể được triển khai trên `LastBlock` đặc điểm trực tiếp)

Điều kỳ diệu sẽ xảy ra khi TC được sử dụng. Biểu cảm có cảm giác tự nhiên hơn rất nhiều, tạo ảo giác rằng hành vi `lastBlock` được kết hợp với thể hiện.

Loại chung có TC
Scala

import Lib1.*, Lib2.*

def useLastBlock1[A](instance: A)(using LastBlock[A]) = instance.lastBlock

def useLastBlock2[A: LastBlock](instance: A) = instance.lastBlock

val eth = Ethereum(lastBlock = 2)
assert(useLastBlock1(eth) == useLastBlock2(eth))

Dòng 34 hàm sử dụng TC ẩn. Lưu ý rằng TC không cần đặt tên nếu tên đó không cần thiết.

Mẫu TC được sử dụng rộng rãi đến mức có một cú pháp kiểu chung để thể hiện “một kiểu có hành vi tiềm ẩn”. Cú pháp dòng 36 là một thay thế ngắn gọn hơn cho cú pháp trước đó (dòng 34). Nó tránh khai báo cụ thể tham số TC ẩn không được đặt tên.

Điều này kết thúc phần trải nghiệm của nhà phát triển. Chúng ta đã thấy các phần mở rộng, hàm ý và một số đường cú pháp có thể cung cấp cú pháp ít lộn xộn hơn khi TC được sử dụng và xác định.

Dẫn xuất tự động

Rất nhiều thư viện Scala sử dụng TC, để lập trình viên triển khai chúng trong cơ sở mã của họ.

Ví dụ: Circe (thư viện khử tuần tự hóa json) sử dụng TC `Encoder[T]`và 'Decoder[T]` để các lập trình viên triển khai trong cơ sở mã của họ. Sau khi triển khai, toàn bộ phạm vi của thư viện có thể được sử dụng. 

Việc triển khai TC đó thường xuyên hơn người lập bản đồ hướng dữ liệu. Chúng không cần bất kỳ logic nghiệp vụ nào, viết rất nhàm chán và là gánh nặng để duy trì sự đồng bộ với các lớp trường hợp.

Trong tình huống như vậy, những thư viện đó cung cấp cái được gọi là tự động đạo hàm hoặc bán tự động nguồn gốc. Xem ví dụ Circe tự động bán tự động nguồn gốc. Với dẫn xuất bán tự động, lập trình viên có thể khai báo một thể hiện của một lớp loại bằng một số cú pháp nhỏ, trong khi dẫn xuất tự động không yêu cầu bất kỳ sửa đổi mã nào ngoại trừ việc nhập.

Dưới mui xe, tại thời điểm biên dịch, các macro chung sẽ xem xét nội tâm loại dưới dạng cấu trúc dữ liệu thuần túy và tạo TC[T] cho người dùng thư viện. 

Nói chung, việc tạo ra một TC là rất phổ biến, vì vậy Scala đã giới thiệu một hộp công cụ hoàn chỉnh cho mục đích đó. Phương pháp này không phải lúc nào cũng được quảng cáo bởi các tài liệu thư viện mặc dù đó là cách sử dụng đạo hàm Scala 3.

Scala

object GenericLib:

 trait Named[A]:
 def blockchainName(instance: A): String

 object Named:
 import scala.deriving.*

 inline final def derived[A](using inline m: Mirror.Of[A]): Named[A] =
 val nameOfType: String = inline m match
 case p: Mirror.ProductOf[A] => compiletime.constValue[p.MirroredLabel]
 case _ => compiletime.error("Not a product")
 new Named[A]:
 override def blockchainName(instance: A):String = nameOfType.toLowerCase

 extension[A] (instance: A)(using tc: Named[A])
 def blockchainName = tc.blockchainName(instance)

import Lib1.*, GenericLib.*

case class Polkadot() derives Named
given Named[Bitcoin] = Named.derived
given Named[Ethereum] = Named.derived

println(Ethereum(lastBlock = 2).blockchainName)
println(Bitcoin().blockchainName)
println(Polkadot().blockchainName)

Dòng 18 một TC mới `Named` được giới thiệu. Nói đúng ra, TC này không liên quan đến hoạt động kinh doanh blockchain. Mục đích của nó là đặt tên cho blockchain dựa trên tên của lớp trường hợp.

Đầu tiên tập trung vào các dòng định nghĩa 36-38. Có 2 cú pháp để lấy TC:

  1. Dòng 36, phiên bản TC có thể được định nghĩa trực tiếp trên lớp trường hợp với `derives` từ khóa. Dưới mui xe, trình biên dịch tạo ra một ` nhất địnhNamed` ví dụ trong `Polkadot` đối tượng đồng hành.
  2. Dòng 37 và 38, các thể hiện của lớp loại được cung cấp trên các lớp có sẵn với `TC.derived

Dòng 31 một phần mở rộng chung được xác định (xem các phần trước) và `blockchainName` được sử dụng một cách tự nhiên.  

Các `derives` từ khóa mong đợi một phương thức có dạng `inline def derived[T](using Mirror.Of[T]): TC[T] = ???` được xác định ở dòng 24. Tôi sẽ không giải thích sâu về chức năng của mã. Trong các phác thảo rộng rãi:

  • `inline def` định nghĩa một macro
  • `Mirror` là một phần của hộp công cụ để xem xét các kiểu nội tâm. Có nhiều loại gương khác nhau và dòng 26 mã tập trung vào `Product` gương (một lớp trường hợp là một sản phẩm). Dòng 27, nếu người lập trình cố gắng rút ra thứ gì đó không phải là `Product`, mã sẽ không được biên dịch.
  • cái 'Mirror` chứa các loại khác. Một trong số họ, `MirrorLabel`, là một chuỗi chứa tên loại. Giá trị này được sử dụng trong quá trình triển khai, dòng 29, của `Named`TC.

Tác giả TC có thể sử dụng lập trình meta để cung cấp các hàm tạo ra các phiên bản TC cho một loại một cách tổng quát. Lập trình viên có thể sử dụng API thư viện chuyên dụng hoặc các công cụ phái sinh Scala để tạo phiên bản cho mã của họ.

Cho dù bạn cần mã chung hay mã cụ thể để triển khai TC thì luôn có giải pháp cho từng tình huống. 

Tóm tắt tất cả các lợi ích

  • Nó giải quyết vấn đề biểu hiện
    • Các kiểu mới có thể thực hiện hành vi hiện có thông qua kế thừa đặc điểm truyền thống
    • Hành vi mới có thể được thực hiện trên các loại hiện có
  • Tách mối quan tâm
    • Mã không bị sai và dễ dàng xóa. TC tách biệt dữ liệu và hành vi, đây là phương châm lập trình chức năng.
  • Nó an toàn
    • Nó an toàn vì nó không dựa vào sự xem xét nội tâm. Nó tránh việc khớp mẫu lớn liên quan đến các loại. nếu bạn gặp phải việc viết mã như vậy, bạn có thể phát hiện trường hợp mẫu TC sẽ phù hợp hoàn hảo.
    • Cơ chế ngầm được biên dịch an toàn! Nếu thiếu một phiên bản tại thời điểm biên dịch thì mã sẽ không được biên dịch. Không có gì ngạc nhiên khi chạy.
  • Nó mang lại tính đa hình đặc biệt
    • Tính đa hình đặc biệt thường bị thiếu trong lập trình hướng đối tượng truyền thống.
    • Với tính đa hình đặc biệt, nhà phát triển có thể triển khai hành vi tương tự cho nhiều loại không liên quan khác nhau mà không cần sử dụng kiểu gõ phụ truyền thống (kết hợp mã)
  • Việc tiêm phụ thuộc được thực hiện dễ dàng
    • Một phiên bản TC có thể được thay đổi theo nguyên tắc thay thế Liskov. 
    • Khi một thành phần phụ thuộc vào TC, một TC giả có thể dễ dàng được đưa vào cho mục đích thử nghiệm. 

chỉ dẫn truy cập

Mỗi chiếc búa đều được thiết kế để giải quyết nhiều vấn đề khác nhau.

Loại Lớp dành cho các vấn đề về hành vi và không được sử dụng để kế thừa dữ liệu. Sử dụng thành phần cho mục đích đó.

Việc phân nhóm thông thường đơn giản hơn. Nếu bạn sở hữu cơ sở mã và không hướng tới khả năng mở rộng, các lớp loại có thể là quá mức cần thiết.

Chẳng hạn, trong lõi Scala, có một `Numeric`loại lớp:

Scala

trait Numeric[T] extends Ordering[T] {
 def plus(x: T, y: T): T
 def minus(x: T, y: T): T
 def times(x: T, y: T): T

Việc sử dụng một lớp kiểu như vậy thực sự có ý nghĩa vì nó không chỉ cho phép sử dụng lại các thuật toán đại số trên các kiểu được nhúng trong Scala (Int, BigInt, …), mà còn trên các kiểu do người dùng xác định (a `ComplexNumber` chẳng hạn).

Mặt khác, việc triển khai các bộ sưu tập Scala chủ yếu sử dụng kiểu con thay vì kiểu lớp. Thiết kế này có ý nghĩa vì nhiều lý do:

  • API bộ sưu tập được cho là hoàn chỉnh và ổn định. Nó bộc lộ hành vi chung thông qua các đặc điểm được kế thừa từ quá trình triển khai. Khả năng mở rộng cao không phải là một mục tiêu cụ thể ở đây.
  • Nó phải đơn giản để sử dụng. TC bổ sung thêm chi phí tinh thần cho lập trình viên người dùng cuối.
  • TC cũng có thể phải chịu chi phí nhỏ về hiệu suất. Điều này có thể rất quan trọng đối với API bộ sưu tập.
  • Tuy nhiên, API bộ sưu tập vẫn có thể mở rộng thông qua TC mới do thư viện bên thứ ba xác định.

Kết luận

Chúng ta đã thấy rằng TC là một mẫu đơn giản có thể giải quyết được một vấn đề lớn. Nhờ cú pháp phong phú Scala, mẫu TC có thể được triển khai và sử dụng theo nhiều cách. Mẫu TC phù hợp với mô hình lập trình hàm và là một công cụ tuyệt vời cho một kiến ​​trúc gọn gàng. Không có viên đạn bạc và mẫu TC phải được áp dụng khi phù hợp.

Hy vọng bạn đã có được kiến ​​thức khi đọc tài liệu này. 

Mã có sẵn tại https://github.com/jprudent/type-class-article. Hãy liên hệ với tôi nếu bạn có bất kỳ câu hỏi hoặc nhận xét nào. Bạn có thể sử dụng các vấn đề hoặc nhận xét mã trong kho lưu trữ nếu muốn.


Jerome THẬN TRỌNG

Kỹ sư phần mềm

Dấu thời gian:

Thêm từ Ledger