Restrição de typeclass em typeclass genérica
Ao longo da última semana ou assim, estive trabalhando em um traço de matriz digitada e indexada para Scala. Eu gostaria de fornecer o traço como uma typeclass e permitir que o usuário da biblioteca implemente como quiser. Aqui está um exemplo, usando uma lista de listas para implementar a typeclass 2d array:
// 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)
Isso tudo parece bem. Estou tendo dificuldades é que gostaria de restringir os tipos de índice a uma lista de tipos aprovados (que o usuário pode alterar). Como o programa não é o proprietário original de todos os tipos aprovados, eu estava pensando em ter uma typeclass para representar esses tipos aprovados e fazer com que os tipos aprovados a implementassem:
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] {}
em seguida, altere a definição de typeclass para impor uma restrição que Idx0e Idx1tem que implementar a IsValidIndextypeclass (e é aqui que as coisas começam a não funcionar):
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
}
Isso não será compilado porque requer que um trait tenha um parâmetro implícito para a typeclass, o que eles não podem ter: ( Restringindo parâmetros de tipo em classes de caso e traits ).
Isso me deixa com duas soluções potenciais, mas ambas parecem um pouco abaixo do ideal:
- Implemente a typeclass IsA2dArray original como uma classe abstrata, o que me permite usar a
Idx0: IsValidIndexsintaxe diretamente acima (gentilmente sugerida no link acima). Esse era meu pensamento original, mas a) é menos amigável, pois requer que o usuário envolva qualquer tipo que esteja usando em outra classe que então estende essa classe abstrata. Considerando que, com uma typeclass, a nova funcionalidade pode ser diretamente anexada eb) isso rapidamente se tornou bastante complicado e difícil de digitar - eu encontrei esta postagem do blog (https://tpolecat.github.io/2015/04/29/f-bounds.html) relevantes para os problemas - e parecia que seguir o caminho da typeclass seria mais fácil a longo prazo. - A restrição que Idx0
Idx0eIdx1deve implementarIsValidIndexpode ser colocada na definição implícita para implementar a typeclass:implicit def arr2dIsA2dArray[T, Idx0: IsValidIndex, Idx1: IsValidIndex] =... Mas isso está nas mãos do usuário, e não nas mãos do escritor da biblioteca, e não há garantia de que eles irão aplicá-lo.
Se alguém pudesse sugerir uma solução alternativa para resolver esse círculo ou uma mudança geral de abordagem que atinja o mesmo objetivo, eu ficaria muito grato. Eu entendo que Scala 3 permite que traits tenham parâmetros implícitos e, portanto, me permitiria usar a Idx0: IsValidIndexrestrição diretamente na lista de parâmetros genéricos de typeclass, o que seria ótimo. Mas mudar para 3 apenas por causa disso parece um grande martelo para quebrar uma noz relativamente pequena.
Respostas
Eu acho que a solução é
- Implemente a
IsA2dArraytypeclass original como uma classe abstrata, o que me permite usar aIdx0: IsValidIndexsintaxe diretamente acima (gentilmente sugerida no link acima).
Esse era meu pensamento original, mas a) é menos amigável, pois requer que o usuário envolva qualquer tipo que esteja usando em outra classe que então estende essa classe abstrata.
Não, a classe abstrata não será estendida * , ela ainda será uma classe de tipo, apenas uma classe de tipo de classe abstrata e não uma classe de tipo de característica.
Posso apenas assumir que o trait e a classe abstrata são intercambiáveis ao definir typeclasses?
Na maioria das vezes.
Qual é a vantagem de usar classes abstratas em vez de características?
https://www.geeksforgeeks.org/difference-between-traits-and-abstract-classes-in-scala/
A menos que você tem hierarquia de classes tipo (como Functor, Applicative, Monad... em gatos). Traço ou classe abstrata (uma classe de tipo) não pode estender várias classes abstratas (classes de tipo), embora possa estender vários traços (classes de tipo). Mas de qualquer maneira, a herança de classes de tipo é complicada
https://typelevel.org/blog/2016/09/30/subtype-typeclasses.html
* Bem, quando escrevemos implicit def arr2dIsA2dArray[T, Idx0, Idx1] = new IsA2dArray[Arr2d[T, Idx0, Idx1], T, Idx0, Idx1] {...tecnicamente é uma extensão, IsA2dArraymas isso é semelhante por IsA2dArrayser um traço e uma classe abstrata.