Typeclass vincolo in typeclass generic

Sep 06 2020

Nell'ultima settimana o giù di lì ho lavorato su un tratto di array indicizzato e tipizzato per Scala. Vorrei fornire il tratto come classe di caratteri e consentire all'utente della libreria di implementarlo come preferisce. Ecco un esempio, utilizzando un elenco di elenchi per implementare la classe di tipi di array 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)

Sembra tutto a posto. Il punto in cui ho difficoltà è che vorrei limitare i tipi di indice a un elenco di tipi approvati (che l'utente può modificare). Poiché il programma non è il proprietario originale di tutti i tipi approvati, stavo pensando di avere una classe di tipi per rappresentare questi tipi approvati e di farla implementare dai tipi approvati:

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

quindi modificare la definizione della classe di tipo per imporre un vincolo Idx0e Idx1implementare la IsValidIndexclasse di tipo (ed è qui che le cose iniziano a non funzionare):

  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
  }

Questo non verrà compilato perché richiede che un tratto abbia un parametro implicito per la classe di tipo, che non è consentito avere: ( Vincolare i parametri di tipo su classi e tratti ).

Questo mi lascia con due potenziali soluzioni, ma entrambe sembrano un po 'non ottimali:

  1. Implementa invece la classe di tipi IsA2dArray originale come una classe astratta, che quindi mi consente di utilizzare la Idx0: IsValidIndexsintassi direttamente sopra (gentilmente suggerita nel link sopra). Questo era il mio pensiero originale, ma a) è meno facile da usare, poiché richiede all'utente di racchiudere qualunque tipo stia usando in un'altra classe che poi estende questa classe astratta. Considerando che con una classe di caratteri, la nuova funzionalità può essere fissata direttamente, eb) questo è diventato rapidamente piuttosto complicato e difficile da digitare - ho trovato questo post sul blog (https://tpolecat.github.io/2015/04/29/f-bounds.html) rilevanti per i problemi - e sembrava che prendere il percorso della classe di tipo sarebbe stato più facile a lungo termine.
  2. Il vincolo che Idx0 Idx0e Idx1deve implementare IsValidIndexpuò essere posto nel def implicito per implementare la typeclass: implicit def arr2dIsA2dArray[T, Idx0: IsValidIndex, Idx1: IsValidIndex] = ... Ma questo è quindi nelle mani dell'utente piuttosto che dell'autore della libreria, e non c'è alcuna garanzia che lo applicheranno.

Se qualcuno potesse suggerire una soluzione per quadrare questo cerchio o un cambiamento generale di approccio che raggiunga lo stesso obiettivo, ne sarei molto grato. Capisco che Scala 3 consente ai tratti di avere parametri impliciti e quindi mi consentirebbe di utilizzare il Idx0: IsValidIndexvincolo direttamente nell'elenco dei parametri generici della classe di tipo, il che sarebbe fantastico. Ma passare a 3 solo per questo sembra un grosso martello per rompere un dado relativamente piccolo.

Risposte

2 DmytroMitin Sep 07 2020 at 08:29

Immagino che la soluzione sia

  1. Implementa invece la IsA2dArraytypeclass originale come una classe astratta, che poi mi permette di usare la Idx0: IsValidIndexsintassi direttamente sopra (gentilmente suggerita nel link sopra).

Questo era il mio pensiero originale, ma a) è meno facile da usare, poiché richiede all'utente di racchiudere qualunque tipo stia usando in un'altra classe che poi estende questa classe astratta.

No, la classe astratta non verrà estesa * , sarà ancora una classe di tipo, solo una classe di tipo astratto e non una classe di tipo di tratto.

Posso semplicemente presumere che il tratto e la classe astratta siano intercambiabili quando si definiscono le classi di tipi?

Soprattutto.

Qual è il vantaggio di usare classi astratte invece dei tratti?

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

A meno che non si dispone di gerarchia di classi di tipo (come Functor, Applicative, Monad... in Gatti). La classe di tratti o astratta (una classe di tipo) non può estendere più classi astratte (classi di tipo) mentre può estendere diversi tratti (classi di tipo). Ma comunque l'ereditarietà delle classi di tipo è complicata

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


* Beh, quando scriviamo implicit def arr2dIsA2dArray[T, Idx0, Idx1] = new IsA2dArray[Arr2d[T, Idx0, Idx1], T, Idx0, Idx1] {...tecnicamente si sta estendendo IsA2dArrayma questo è simile per IsA2dArrayessere un tratto e una classe astratta.