Ràng buộc typeclass trong typeclass chung

Sep 06 2020

Trong khoảng hơn một tuần qua, tôi đã làm việc trên một đặc điểm mảng được đánh chỉ mục, được đánh chỉ mục cho Scala. Tôi muốn cung cấp đặc điểm dưới dạng typeclass và cho phép người dùng thư viện triển khai nó theo cách họ muốn. Đây là một ví dụ, sử dụng danh sách các danh sách để triển khai kiểu chữ mảng 2d:

// crate a 2d Array typeclass, with additional parameters
trait IsA2dArray[A, T, Idx0, Idx1] {
  def get(arr: A, x: Int, y: Int): T // get a single element of the array; its type will be T
}
// give this typeclass method syntax
implicit class IsA2dArrayOps[A, T, Idx0, Idx1](value: A) {
  def get(x: Int, y: Int)(implicit isA2dArrayInstance: IsA2dArray[A, T, Idx0, Idx1]): T = 
    isA2dArrayInstance.get(value, x, y)
}

// The user then creates a simple case class that can act as a 2d array
case class Arr2d[T, Idx0, Idx1] (
  values: List[List[T]],
  idx0: List[Idx0],
  idx1: List[Idx1],
)
// A couple of dummy index element types:
case class Date(i: Int) // an example index element
case class Item(a: Char) // also an example
// The user implements the IsA2dArray typeclass 
implicit def arr2dIsA2dArray[T, Idx0, Idx1] = new IsA2dArray[Arr2d[T, Idx0, Idx1], T, Idx0, Idx1] {
  def get(arr: Arr2d[T, Idx0, Idx1], x: Int, y: Int): T = arr.values(x)(y)
}
// create an example instance of the type
val arr2d = Arr2d[Double, Date, Item] (
  List(List(1.0, 2.0), List(3.0, 4.0)),
  List(Date(0), Date(1)),
  List(Item('a'), Item('b')),
)
// check that it works
arr2d.get(0, 1)

Tất cả điều này có vẻ ổn. Nơi tôi đang gặp khó khăn là tôi muốn giới hạn các loại chỉ mục trong danh sách các loại được chấp thuận (người dùng có thể thay đổi). Vì chương trình không phải là chủ sở hữu ban đầu của tất cả các loại được phê duyệt, tôi đã nghĩ đến việc có một typeclass để đại diện cho các loại đã được phê duyệt này và để các loại đã được phê duyệt triển khai nó:

trait IsValidIndex[A] // a typeclass, indicating whether this is a valid index type
implicit val dateIsValidIndex: IsValidIndex[Date] = new IsValidIndex[Date] {} 
implicit val itemIsValidIndex: IsValidIndex[Item] = new IsValidIndex[Item] {}

sau đó thay đổi định nghĩa typeclass để áp đặt một ràng buộc Idx0và Idx1phải triển khai IsValidIndextypeclass (và đây là nơi mọi thứ bắt đầu không hoạt động):

  trait IsA2dArray[A, T, Idx0: IsValidIndex, Idx1: IsValidIndex] {
    def get(arr: A, x: Int, y: Int): T // get a single element of the array; its type will be T
  }

Điều này sẽ không biên dịch vì nó yêu cầu một đặc điểm phải có một tham số ngầm định cho typeclass, mà chúng không được phép có: ( Ràng buộc các tham số kiểu trên các lớp và đặc điểm trường hợp ).

Điều này để lại cho tôi hai giải pháp tiềm năng, nhưng cả hai đều cảm thấy hơi tối ưu:

  1. Thực hiện typeclass IsA2dArray ban đầu như một lớp trừu tượng để thay thế, sau đó cho phép tôi sử dụng Idx0: IsValidIndexcú pháp trực tiếp ở trên (vui lòng đề xuất trong liên kết ở trên). Đây là suy nghĩ ban đầu của tôi, nhưng a) nó ít thân thiện với người dùng hơn, vì nó yêu cầu người dùng bọc bất kỳ loại nào họ đang sử dụng trong một lớp khác, sau đó mở rộng lớp trừu tượng này. Trong khi với typeclass, chức năng mới có thể được bắt vít trực tiếp và b) điều này nhanh chóng trở nên khá rắc rối và khó gõ - Tôi tìm thấy bài đăng trên blog này (https://tpolecat.github.io/2015/04/29/f-bounds.html) liên quan đến các vấn đề - và có cảm giác như việc đi theo con đường typeclass sẽ dễ dàng hơn trong thời gian dài hơn.
  2. Đối chiếu mà Idx0 Idx0và Idx1phải triển khai IsValidIndexcó thể được đặt trong định nghĩa ngầm định để triển khai typeclass: implicit def arr2dIsA2dArray[T, Idx0: IsValidIndex, Idx1: IsValidIndex] = ... Nhưng điều này sau đó nằm trong tay của người dùng chứ không phải của người viết thư viện và không có gì đảm bảo rằng họ sẽ thực thi nó.

Nếu ai đó có thể đề xuất một cách làm việc để giải quyết vòng tròn này, hoặc một sự thay đổi tổng thể về cách tiếp cận để đạt được cùng một mục tiêu, tôi rất biết ơn. Tôi hiểu rằng Scala 3 cho phép các đặc điểm có các tham số ngầm định và do đó sẽ cho phép tôi sử dụng Idx0: IsValidIndexràng buộc trực tiếp trong danh sách tham số chung của typeclass, điều này sẽ rất tuyệt. Nhưng chuyển sang 3 chỉ vì điều đó cảm thấy giống như một chiếc búa khá lớn để bẻ một hạt tương đối nhỏ.

Trả lời

2 DmytroMitin Sep 07 2020 at 08:29

Tôi đoán giải pháp là

  1. IsA2dArrayThay vào đó, hãy triển khai typeclass ban đầu dưới dạng lớp trừu tượng, sau đó cho phép tôi sử dụng Idx0: IsValidIndexcú pháp trực tiếp ở trên (vui lòng đề xuất trong liên kết ở trên).

Đây là suy nghĩ ban đầu của tôi, nhưng a) nó ít thân thiện với người dùng hơn, vì nó yêu cầu người dùng bọc bất kỳ loại nào họ đang sử dụng trong một lớp khác, sau đó mở rộng lớp trừu tượng này.

Không, lớp trừu tượng sẽ không được mở rộng * , nó sẽ vẫn là một lớp kiểu, chỉ là lớp kiểu trừu tượng-lớp chứ không phải lớp kiểu đặc điểm.

Tôi có thể chỉ giả sử rằng lớp đặc điểm và lớp trừu tượng có thể hoán đổi cho nhau khi định nghĩa kính chữ không?

Hầu hết.

Lợi thế của việc sử dụng các lớp trừu tượng thay vì các đặc điểm là gì?

https://www.geeksforgeeks.org/difference-between-traits-and-abstract-classes-in-scala/

Trừ khi bạn có hệ thống các lớp học kiểu (như Functor, Applicative, Monad... trong Cats). Lớp đặc điểm hoặc lớp trừu tượng (lớp kiểu) không thể mở rộng một số lớp trừu tượng (lớp kiểu) trong khi nó có thể mở rộng một số đặc điểm (lớp kiểu). Nhưng dù sao thì việc thừa kế các lớp kiểu cũng khó

https://typelevel.org/blog/2016/09/30/subtype-typeclasses.html


* Chà, khi chúng ta viết implicit def arr2dIsA2dArray[T, Idx0, Idx1] = new IsA2dArray[Arr2d[T, Idx0, Idx1], T, Idx0, Idx1] {...về mặt kỹ thuật thì nó mở rộng IsA2dArraynhưng điều này cũng tương tự vì IsA2dArraynó là một lớp đặc điểm và trừu tượng.