Conversione implicita vs. classe di tipo


93

In Scala, possiamo utilizzare almeno due metodi per aggiornare i tipi esistenti o nuovi. Supponiamo di voler esprimere che qualcosa può essere quantificato usando un Int. Possiamo definire il seguente tratto.

Conversione implicita

trait Quantifiable{ def quantify: Int }

E poi possiamo usare conversioni implicite per quantificare ad esempio stringhe ed elenchi.

implicit def string2quant(s: String) = new Quantifiable{ 
  def quantify = s.size 
}
implicit def list2quantifiable[A](l: List[A]) = new Quantifiable{ 
  val quantify = l.size 
}

Dopo averli importati, possiamo chiamare il metodo quantifysu stringhe ed elenchi. Si noti che l'elenco quantificabile memorizza la sua lunghezza, quindi evita il costoso attraversamento dell'elenco nelle chiamate successive a quantify.

Tipo di classi

Un'alternativa è definire un "testimone" Quantified[A]che dichiari che un certo tipo Apuò essere quantificato.

trait Quantified[A] { def quantify(a: A): Int }

Forniamo quindi istanze di questo tipo di classe per Stringe Listda qualche parte.

implicit val stringQuantifiable = new Quantified[String] {
  def quantify(s: String) = s.size 
}

E se poi scriviamo un metodo che deve quantificare i suoi argomenti, scriviamo:

def sumQuantities[A](as: List[A])(implicit ev: Quantified[A]) = 
  as.map(ev.quantify).sum

O usando la sintassi del contesto:

def sumQuantities[A: Quantified](as: List[A]) = 
  as.map(implicitly[Quantified[A]].quantify).sum

Ma quando utilizzare quale metodo?

Ora arriva la domanda. Come posso decidere tra questi due concetti?

Quello che ho notato finora.

classi di tipo

  • le classi di tipo consentono la bella sintassi legata al contesto
  • con le classi di tipo non creo un nuovo oggetto wrapper ad ogni utilizzo
  • la sintassi legata al contesto non funziona più se la classe di tipo ha più parametri di tipo; immagina di voler quantificare le cose non solo con numeri interi ma con valori di qualche tipo generale T. Vorrei creare una classe di tipoQuantified[A,T]

conversione implicita

  • poiché creo un nuovo oggetto, posso memorizzare nella cache i valori o calcolare una rappresentazione migliore; ma dovrei evitarlo, poiché potrebbe accadere più volte e una conversione esplicita verrebbe probabilmente invocata solo una volta?

Cosa mi aspetto da una risposta

Presenta uno (o più) casi d'uso in cui la differenza tra i due concetti è importante e spiega perché preferirei uno rispetto all'altro. Anche spiegare l'essenza dei due concetti e la loro relazione reciproca sarebbe bello, anche senza esempio.


C'è un po 'di confusione nei punti della classe di tipo in cui si parla di "limite di vista", sebbene le classi di tipo utilizzino limiti di contesto.
Daniel C. Sobral

1
+1 domanda eccellente; Sono molto interessato a una risposta esauriente a questo.
Dan Burton

@Daniel Grazie. Sbaglio sempre quelli.
ziggystar

2
Si sbaglia in un unico luogo: nel secondo esempio conversione implicita si memorizza il sizedi una lista in un valore e dire che evita l'attraversamento costoso della lista per le chiamate successive per quantificare, ma di ogni vostra chiamata al quantifyl' list2quantifiableviene attivato tutto da capo reinstanziando così Quantifiablela quantifyproprietà e ricalcolando la proprietà. Quello che sto dicendo è che in realtà non c'è modo di memorizzare nella cache i risultati con conversioni implicite.
Nikita Volkov

@NikitaVolkov La tua osservazione è giusta. E lo affronto nella mia domanda nel penultimo paragrafo. La memorizzazione nella cache funziona, quando l'oggetto convertito viene utilizzato più a lungo dopo una chiamata al metodo di conversione (e forse viene trasmesso nella sua forma convertita). Mentre le classi di tipo verrebbero probabilmente incatenate lungo l'oggetto non convertito quando si andava più in profondità.
ziggystar

Risposte:


42

Anche se non voglio duplicare il mio materiale da Scala In Depth , penso che valga la pena notare che le classi / i tratti del tipo sono infinitamente più flessibili.

def foo[T: TypeClass](t: T) = ...

ha la capacità di cercare nel suo ambiente locale una classe di tipo predefinito. Tuttavia, posso sovrascrivere il comportamento predefinito in qualsiasi momento in due modi:

  1. Creazione / importazione di un'istanza di classe di tipo implicito in Scope per cortocircuitare la ricerca implicita
  2. Passaggio diretto di una classe di tipo

Ecco un esempio:

def myMethod(): Unit = {
   // overrides default implicit for Int
   implicit object MyIntFoo extends Foo[Int] { ... }
   foo(5)
   foo(6) // These all use my overridden type class
   foo(7)(new Foo[Int] { ... }) // This one needs a different configuration
}

Ciò rende le classi di tipo infinitamente più flessibili. Un'altra cosa è che le classi / i tratti di tipo supportano meglio la ricerca implicita .

Nel tuo primo esempio, se usi una vista implicita, il compilatore eseguirà una ricerca implicita per:

Function1[Int, ?]

Che esaminerà l' Function1oggetto associato di e l' Intoggetto associato.

Si noti che non siQuantifiable trova da nessuna parte nella ricerca implicita. Ciò significa che devi posizionare la vista implicita in un oggetto pacchetto o importarla nell'ambito. È più faticoso ricordare cosa sta succedendo.

D'altra parte, una classe di tipo è esplicita . Vedi cosa sta cercando nella firma del metodo. Hai anche una ricerca implicita di

Quantifiable[Int]

che cercherà Quantifiablenell'oggetto companion di e Int nell'oggetto companion di. Significa che puoi fornire valori predefiniti e nuovi tipi (come una MyStringclasse) possono fornire un valore predefinito nel loro oggetto associato e verrà cercato implicitamente.

In generale, utilizzo le classi di tipo. Sono infinitamente più flessibili per l'esempio iniziale. L'unico posto in cui utilizzo le conversioni implicite è quando utilizzo un livello API tra un wrapper Scala e una libreria Java, e anche questo può essere "pericoloso" se non stai attento.


20

Un criterio che può entrare in gioco è come vuoi che la nuova funzionalità "si senta"; usando le conversioni implicite, puoi far sembrare che sia solo un altro metodo:

"my string".newFeature

... mentre usi le classi di tipo sembrerà sempre che tu stia chiamando una funzione esterna:

newFeature("my string")

Una cosa che puoi ottenere con le classi di tipo e non con le conversioni implicite è l'aggiunta di proprietà a un tipo , piuttosto che a un'istanza di un tipo. È quindi possibile accedere a queste proprietà anche quando non si dispone di un'istanza del tipo disponibile. Un esempio canonico potrebbe essere:

trait Default[T] { def value : T }

implicit object DefaultInt extends Default[Int] {
  def value = 42
}

implicit def listsHaveDefault[T : Default] = new Default[List[T]] {
  def value = implicitly[Default[T]].value :: Nil
}

def default[T : Default] = implicitly[Default[T]].value

scala> default[List[List[Int]]]
resN: List[List[Int]] = List(List(42))

Questo esempio mostra anche come i concetti siano strettamente correlati: le classi di tipo non sarebbero altrettanto utili se non ci fosse un meccanismo per produrre infinitamente molte delle loro istanze; senza il implicitmetodo (non una conversione, è vero), potrei avere solo un numero limitato di tipi che hanno la Defaultproprietà.


@Phillippe - Mi interessa molto la tecnica che hai scritto ... ma sembra non funzionare su Scala 2.11.6. Ho pubblicato una domanda chiedendo un aggiornamento sulla tua risposta. grazie in anticipo se puoi aiutare: Vedi: stackoverflow.com/questions/31910923/…
Chris Bedford

@ChrisBedford ho aggiunto la definizione di defaultper futuri lettori.
Philippe

13

Puoi pensare alla differenza tra le due tecniche per analogia con l'applicazione della funzione, solo con un wrapper denominato. Per esempio:

trait Foo1[A] { def foo(a: A): Int }  // analogous to A => Int
trait Foo0    { def foo: Int }        // analogous to Int

Un'istanza della prima incapsula una funzione di tipo A => Int, mentre un'istanza della seconda è già stata applicata a un file A. Potresti continuare lo schema ...

trait Foo2[A, B] { def foo(a: A, b: B): Int } // sort of like A => B => Int

quindi potresti pensare a una Foo1[B]specie di applicazione parziale di Foo2[A, B]a qualche Aistanza. Un ottimo esempio di questo è stato scritto da Miles Sabin come "Dipendenze Funzionali in Scala" .

Quindi in realtà il mio punto è che, in linea di principio:

  • "sfruttare" una classe (attraverso la conversione implicita) è il caso di "ordine zero" ...
  • dichiarare un typeclass è il caso del "primo ordine" ...
  • classi di tipi multiparametrici con fundeps (o qualcosa come fundeps) è il caso generale.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.