Restricción typeclass en typeclass genérico

Sep 06 2020

Durante la última semana, he estado trabajando en un rasgo de matriz indexada y tipificada para Scala. Me gustaría proporcionar el rasgo como una clase de tipos y permitir que el usuario de la biblioteca lo implemente como quiera. Aquí hay un ejemplo, usando una lista de listas para implementar la clase de tipo de matriz 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)

Todo esto parece estar bien. Donde tengo dificultades es que me gustaría restringir los tipos de índice a una lista de tipos aprobados (que el usuario puede cambiar). Dado que el programa no es el propietario original de todos los tipos aprobados, estaba pensando en tener una clase de tipo para representar estos tipos aprobados y que los tipos aprobados la implementen:

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] {}

luego cambie la definición de la clase de tipo para imponer una restricción Idx0y Idx1tenga que implementar la IsValidIndexclase de tipo (y aquí es donde las cosas comienzan a no 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
  }

Esto no se compilará porque requiere que un rasgo tenga un parámetro implícito para la clase de tipo, que no se les permite tener: ( Restricción de parámetros de tipo en clases de casos y rasgos ).

Esto me deja con dos posibles soluciones, pero ambas se sienten un poco subóptimas:

  1. En su lugar, implemente la clase de tipo IsA2dArray original como una clase abstracta, lo que me permite usar la Idx0: IsValidIndexsintaxis directamente arriba (sugerida amablemente en el enlace anterior). Este fue mi pensamiento original, pero a) es menos fácil de usar, ya que requiere que el usuario envuelva cualquier tipo que esté usando en otra clase que luego extiende esta clase abstracta. Mientras que con una clase de tipos, la nueva funcionalidad se puede atornillar directamente, yb) esto rápidamente se volvió bastante complicado y difícil de escribir: encontré esta publicación de blog (https://tpolecat.github.io/2015/04/29/f-bounds.html) relevantes para los problemas, y se sintió que tomar la ruta de la clase de tipos sería más fácil a largo plazo.
  2. La restricción que Idx0 Idx0y Idx1debe implementar IsValidIndexse puede colocar en la definición implícita para implementar la clase de tipos: implicit def arr2dIsA2dArray[T, Idx0: IsValidIndex, Idx1: IsValidIndex] = ... Pero esto está en manos del usuario en lugar de en las manos del escritor de la biblioteca, y no hay garantía de que lo hagan cumplir.

Si alguien pudiera sugerir una solución alternativa para cuadrar este círculo o un cambio general de enfoque que logre el mismo objetivo, estaría muy agradecido. Entiendo que Scala 3 permite que los rasgos tengan parámetros implícitos y, por lo tanto, me permitiría usar la Idx0: IsValidIndexrestricción directamente en la lista de parámetros genéricos de la clase de tipo, lo cual sería genial. Pero cambiar a 3 solo por eso se siente como un martillo bastante grande para romper una nuez relativamente pequeña.

Respuestas

2 DmytroMitin Sep 07 2020 at 08:29

Supongo que la solucion es

  1. En su lugar, implemente la IsA2dArrayclase de tipo original como una clase abstracta, lo que me permite usar la Idx0: IsValidIndexsintaxis directamente arriba (sugerida amablemente en el enlace anterior).

Este fue mi pensamiento original, pero a) es menos fácil de usar, ya que requiere que el usuario envuelva cualquier tipo que esté usando en otra clase que luego extiende esta clase abstracta.

No, la clase abstracta no se extenderá * , seguirá siendo una clase de tipo, solo una clase de tipo de clase abstracta y no una clase de tipo de rasgo.

¿Puedo asumir que el rasgo y la clase abstracta son intercambiables al definir clases de tipos?

Principalmente.

¿Cuál es la ventaja de usar clases abstractas en lugar de rasgos?

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

A menos que tenga jerarquía de clases de tipos (como Functor, Applicative, Monad... en los gatos). La clase de rasgo o abstracta (una clase de tipo) no puede extender varias clases abstractas (clases de tipo) mientras que puede extender varios rasgos (clases de tipo). Pero de todos modos la herencia de clases de tipos es complicada

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


* Bueno, cuando escribimos implicit def arr2dIsA2dArray[T, Idx0, Idx1] = new IsA2dArray[Arr2d[T, Idx0, Idx1], T, Idx0, Idx1] {...técnicamente se está extendiendo, IsA2dArraypero es similar por IsA2dArrayser una clase de rasgo y abstracta.