A cosa servono le classi di tipi in Scala?

Come capisco da questo post del blog “classi di caratteri” in Scala è solo un “pattern” implementato con tratti e adattatori impliciti.

Come dice il blog se ho tratto A e un adattatore B -> A allora posso invocare una funzione, che richiede l’argomento di tipo A , con un argomento di tipo B senza richiamare esplicitamente questo adattatore.

L’ho trovato carino ma non particolarmente utile. Potresti fornire un caso d’uso / esempio, che mostra a cosa è utile questa funzione?

Un caso d’uso, come richiesto …

Immagina di avere una lista di cose, potrebbero essere numeri interi, numeri in virgola mobile, matrici, stringhe, forms d’onda, ecc. Dato questo elenco, vuoi aggiungere il contenuto.

Un modo per farlo sarebbe avere un tratto Addable che deve essere ereditato da ogni singolo tipo che può essere aggiunto insieme, o una conversione implicita a un Addable se si tratta di oggetti da una libreria di terze parti che non è ansible aggiornare le interfacce a .

Questo approccio diventa rapidamente travolgente quando si desidera iniziare ad aggiungere altre operazioni simili che possono essere fatte a un elenco di oggetti. Inoltre, non funziona bene se hai bisogno di alternative (ad esempio: l’aggiunta di due forms d’onda le concatena o la sovrappone?) La soluzione è il polimorfismo ad-hoc, in cui puoi scegliere e scegliere il comportamento da riadattare ai tipi esistenti.

Quindi, per il problema originale, è ansible implementare una class di tipi Addable :

 trait Addable[T] { def zero: T def append(a: T, b: T): T } //yup, it's our friend the monoid, with a different name! 

È quindi ansible creare istanze sottoclass implicite di questo, corrispondente a ciascun tipo che si desidera rendere addable:

 implicit object IntIsAddable extends Addable[Int] { def zero = 0 def append(a: Int, b: Int) = a + b } implicit object StringIsAddable extends Addable[String] { def zero = "" def append(a: String, b: String) = a + b } //etc... 

Il metodo per sumre una lista diventa quindi banale da scrivere …

 def sum[T](xs: List[T])(implicit addable: Addable[T]) = xs.FoldLeft(addable.zero)(addable.append) //or the same thing, using context bounds: def sum[T : Addable](xs: List[T]) = { val addable = implicitly[Addable[T]] xs.FoldLeft(addable.zero)(addable.append) } 

La bellezza di questo approccio è che è ansible fornire una definizione alternativa di alcuni typeclass, controllando l’implicito desiderato nell’ambito tramite importazioni o fornendo esplicitamente l’argomento altrimenti implicito. Diventa quindi ansible fornire diversi modi di aggiungere forms d’onda o specificare l’aritmetica modulo per l’aggiunta di interi. È anche abbastanza indolore aggiungere un tipo da una libreria di terze parti al tuo typeclass.

Per inciso, questo è esattamente l’approccio adottato dall’API delle raccolte 2.8. Sebbene il metodo sum sia definito su TraversableLike anziché su List , e la class type è Numeric (contiene anche qualche operazione in più rispetto a zero e append )

Rileggi il primo commento qui:

Una distinzione cruciale tra le classi di tipi e le interfacce è che per la class A essere un “membro” di un’interfaccia deve dichiararlo nel sito della propria definizione. Al contrario, qualsiasi tipo può essere aggiunto a una class di caratteri in qualsiasi momento, purché sia ​​ansible fornire le definizioni richieste, e quindi i membri di una class di caratteri in un dato momento dipendono dall’ambito corrente. Quindi non ci interessa se il creatore di A ha anticipato la class del tipo a cui vogliamo appartenere; se no, possiamo semplicemente creare la nostra definizione che mostra che effettivamente appartiene e quindi usarla di conseguenza. Quindi questo non solo fornisce una soluzione migliore degli adattatori, ma in un certo senso ovvia a tutto ciò che gli adattatori del problema intendevano affrontare.

Penso che questo sia il vantaggio più importante delle classi di tipi.

Inoltre, gestiscono correttamente i casi in cui le operazioni non hanno l’argomento del tipo che stiamo inviando o ne hanno più di uno. Ad esempio, considera questa class di tipo:

 case class Default[T](val default: T) object Default { implicit def IntDefault: Default[Int] = Default(0) implicit def OptionDefault[T]: Default[Option[T]] = Default(None) ... } 

Penso alle classi di tipi come alla possibilità di aggiungere metadati sicuri di tipo a una class.

Quindi, prima definisci una class per modellare il dominio del problema e poi pensa ai metadati da aggiungere ad essa. Cose come Equals, Hashable, Viewable, ecc. Questo crea una separazione tra il dominio del problema e la meccanica per usare la class e apre la sottoclass perché la class è più snella.

Tranne ciò, è ansible aggiungere classi di tipi in qualsiasi punto dell’ambito, non solo dove è definita la class e è ansible modificare le implementazioni. Ad esempio, se calcolo un codice hash per una class Point utilizzando Point # hashCode, allora sono limitato a quella specifica implementazione che potrebbe non creare una buona distribuzione di valori per il set specifico di punti che ho. Ma se utilizzo il parametro [Punto], potrei fornire la mia implementazione.

[Aggiornato con l’esempio] Ad esempio, ecco un caso d’uso che ho avuto la scorsa settimana. Nel nostro prodotto ci sono diversi casi di mappe contenenti container come valori. Es. Map[Int, List[String]] o Map[String, Set[Int]] . L’aggiunta a queste raccolte può essere dettagliata:

 map += key -> (value :: map.getOrElse(key, List())) 

Quindi volevo avere una funzione che avvolgesse questo così potessi scrivere

 map +++= key -> value 

Il problema principale è che le collezioni non hanno tutti gli stessi metodi per aggiungere elementi. Alcuni hanno ‘+’ mentre altri ‘: +’. Volevo anche mantenere l’efficienza di aggiungere elementi a un elenco, quindi non volevo usare fold / map per creare nuove raccolte.

La soluzione è usare le classi di tipi:

  trait Addable[C, CC] { def add(c: C, cc: CC) : CC def empty: CC } object Addable { implicit def listAddable[A] = new Addable[A, List[A]] { def empty = Nil def add(c: A, cc: List[A]) = c :: cc } implicit def addableAddable[A, Add](implicit cbf: CanBuildFrom[Add, A, Add]) = new Addable[A, Add] { def empty = cbf().result def add(c: A, cc: Add) = (cbf(cc) += c).result } } 

Qui ho definito una class di tipo Addable che può aggiungere un elemento C ad una collezione CC. Dispongo di 2 implementazioni predefinite: Per elenchi che utilizzano :: e per altre raccolte, utilizzando il framework builder.

Quindi, utilizzando questa class di tipo è:

 class RichCollectionMap[A, C, B[_], M[X, Y] <: collection.Map[X, Y]](map: M[A, B[C]])(implicit adder: Addable[C, B[C]]) { def updateSeq[That](a: A, c: C)(implicit cbf: CanBuildFrom[M[A, B[C]], (A, B[C]), That]): That = { val pair = (a -> adder.add(c, map.getOrElse(a, adder.empty) )) (map + pair).asInstanceOf[That] } def +++[That](t: (A, C))(implicit cbf: CanBuildFrom[M[A, B[C]], (A, B[C]), That]): That = updateSeq(t._1, t._2)(cbf) } implicit def toRichCollectionMap[A, C, B[_], M[X, Y] <: col 

Il bit speciale utilizza adder.add per aggiungere gli elementi e adder.empty per creare nuove raccolte per nuove chiavi.

Per confrontare, senza classi di tipi avrei avuto 3 opzioni: 1. scrivere un metodo per tipo di raccolta. Ad esempio, addElementToSubList e addElementToSet ecc. Questo crea un grande numero di combinazioni nell'implementazione e inquina lo spazio dei nomi 2. per utilizzare la riflessione per determinare se la sottocategoria è un elenco / set. Ciò è complicato in quanto la mappa è vuota per cominciare (ovviamente scala aiuta anche qui con Manifests) 3. per avere class di tipo di povero uomo richiedendo all'utente di fornire il sumtore. Quindi qualcosa come addToMap(map, key, value, adder) , che è semplicemente brutto

Un altro modo in cui trovo utile questo post del blog è dove descrive i typeclasss: Monads Are Not metaphors

Cerca l’articolo per typeclass. Dovrebbe essere la prima partita. In questo articolo, l’autore fornisce un esempio di una class di caratteri Monad.

Un modo per esaminare le classi di tipi è che consentono l’estensione retrotriggers o il polimorfismo retroattivo . Ci sono un paio di fantastici messaggi di Casual Miracles e Daniel Westheide che mostrano esempi di utilizzo di Classi di tipi in Scala per ottenere ciò.

Ecco un post sul mio blog che esplora vari metodi in scala del supertyping retroattivo , una sorta di estensione retrotriggers, incluso un esempio di typeclass.

Il thread del forum ” Cosa rende le classi di tipo migliori dei tratti? ” Rende alcuni punti interessanti:

  • I typeclasss possono rappresentare molto facilmente nozioni che sono piuttosto difficili da rappresentare in presenza di sottotipi, come l’ uguaglianza e l’ ordinamento .
    Esercizio: creare una gerarchia di classi / trait di piccole dimensioni e provare a implementare .equals su ogni class / tratto in modo tale che l’operazione su istanze arbitrarie dalla gerarchia sia correttamente riflessiva, simmetrica e transitiva.
  • I Typeclasss ti consentono di dimostrare che un tipo al di fuori del tuo “controllo” è conforms ad alcuni comportamenti.
    Il tipo di qualcun altro può essere un membro del tuo typeclass.
  • Non è ansible esprimere “questo metodo prende / restituisce un valore dello stesso tipo del ricevitore del metodo” in termini di sottotipizzazione, ma questo vincolo (molto utile) è semplice usando i caratteri tipografici. Questo è il problema dei tipi con f (dove un tipo con limiti F è parametrizzato sui suoi sottotipi).
  • Tutte le operazioni definite su un tratto richiedono un’istanza ; c’è sempre un argomento. Quindi non è ansible definire ad esempio un fromString(s:String): Foo sul trait Foo in modo tale da poterlo chiamare senza un’istanza di Foo .
    In Scala ciò si manifesta quando le persone cercano disperatamente di astrarre gli oggetti complementari.
    Ma è semplice con un typeclass, come illustrato dall’elemento zero in questo esempio monoidale .
  • Le classi di tipa possono essere definite in modo induttivo ; per esempio, se hai un JsonCodec[Woozle] puoi ottenere un JsonCodec[List[Woozle]] gratuitamente.
    L’esempio sopra illustra questo per “cose ​​che puoi aggiungere insieme”.

Non conosco nessun altro caso d’uso del polimorfismo ad-hoc che viene qui illustrato nel modo migliore ansible.

Sia gli impliciti che i typeclass sono usati per la conversione del tipo . Il principale caso d’uso per entrambi è quello di fornire un polimorfismo ad-hoc (cioè) su classi che non è ansible modificare, ma si aspettano un tipo di polimorfismo di tipo ereditario. In caso di impliciti puoi usare sia una class implicita o una class implicita (che è la tua class wrapper ma nascosta dal client). I typeclasss sono più potenti in quanto possono aggiungere funzionalità a una catena di ereditarietà già esistente (ad esempio: ordinare [T] nella funzione di ordinamento di scala). Per maggiori dettagli puoi vedere https://lakshmirajagopalan.github.io/diving-into-scala-typeclasss/

Nelle classi di tipo scala

  • Abilita il polimorfismo ad-hoc
  • Tipizzato staticamente (es. Sicuro per tipo)
  • Preso in prestito da Haskell
  • Risolve il problema dell’espressione

Il comportamento può essere esteso – in fase di compilazione – dopo il fatto – senza modificare / ricompilare il codice esistente

Impliciti di Scala

L’ultimo elenco di parametri di un metodo può essere contrassegnato come implicito

  • I parametri impliciti sono compilati dal compilatore

  • In effetti, è necessaria la prova del compilatore

  • … come l’esistenza di una class di caratteri in ambito

  • È anche ansible specificare i parametri esplicitamente, se necessario

Sotto l’estensione di esempio sulla class String con l’implementazione della class di tipo si estende la class con nuovi metodi anche se la stringa è finale 🙂

 /** * Created by nihat.hosgur on 2/19/17. */ case class PrintTwiceString(val original: String) { def printTwice = original + original } object TypeClassString extends App { implicit def stringToString(s: String) = PrintTwiceString(s) val name: String = "Nihat" name.printTwice } 

Questa è una differenza importante (necessaria per la programmazione funzionale):

inserisci la descrizione dell'immagine qui

considerare inc:Num a=> a -> a :

a ricevuto è lo stesso che viene restituito, questo non può essere fatto con sottotipizzazione

Mi piace usare le classi di tipi come una forma leggera idiomatica di Dependency Injection che funziona ancora con le dipendenze circolari ma non aggiunge molta complessità al codice. Recentemente ho riscritto un progetto Scala dall’uso di Cake Pattern per digitare classi per DI e ottenuto una riduzione del 59% delle dimensioni del codice.