Dove cerca impliciti Scala?


398

Una domanda implicita per i nuovi arrivati ​​in Scala sembra essere: dove il compilatore cerca impliciti? Intendo implicito perché la domanda non sembra mai essere pienamente formata, come se non ci fossero parole per questo. :-) Ad esempio, da dove vengono i valori per integralsotto?

scala> import scala.math._
import scala.math._

scala> def foo[T](t: T)(implicit integral: Integral[T]) {println(integral)}
foo: [T](t: T)(implicit integral: scala.math.Integral[T])Unit

scala> foo(0)
scala.math.Numeric$IntIsIntegral$@3dbea611

scala> foo(0L)
scala.math.Numeric$LongIsIntegral$@48c610af

Un'altra domanda che fa seguito a coloro che decidono di apprendere la risposta alla prima domanda è come fa il compilatore a scegliere quale implicito usare, in certe situazioni di apparente ambiguità (ma che comunque compila)?

Ad esempio, scala.Predefdefinisce due conversioni da String: una a WrappedStringe un'altra a StringOps. Entrambe le classi, tuttavia, condividono molti metodi, quindi perché Scala non si lamenta dell'ambiguità quando, per esempio, chiama map?

Nota: questa domanda è stata ispirata da questa altra domanda , nella speranza di affermare il problema in modo più generale. L'esempio è stato copiato da lì, perché è indicato nella risposta.

Risposte:


554

Tipi di impliciti

Gli impliciti in Scala si riferiscono sia a un valore che può essere passato "automaticamente", per così dire, sia a una conversione da un tipo a un altro effettuata automaticamente.

Conversione implicita

Parlando molto brevemente del secondo tipo, se si chiama un metodo msu un oggetto odi una classe C, e che la classe non lo fa metodo di supporto m, quindi Scala cercherà una conversione implicita da Ca qualcosa che fa il supporto m. Un semplice esempio potrebbe essere il metodo mapsu String:

"abc".map(_.toInt)

Stringnon supporta il metodo map, ma StringOpslo fa, e c'è una conversione implicita da Stringa StringOpsdisposizione (vedi implicit def augmentStringsu Predef).

Parametri impliciti

L'altro tipo di implicito è il parametro implicito . Questi vengono passati alle chiamate di metodo come qualsiasi altro parametro, ma il compilatore tenta di riempirli automaticamente. In caso contrario, si lamenterà. Si possono passare esplicitamente questi parametri, ed è così che si usa breakOut, per esempio (vedi domanda su breakOut, in un giorno in cui ti senti in difficoltà).

In questo caso, si deve dichiarare la necessità di un implicito, come la foodichiarazione del metodo:

def foo[T](t: T)(implicit integral: Integral[T]) {println(integral)}

Visualizza i limiti

C'è una situazione in cui un implicito è sia una conversione implicita che un parametro implicito. Per esempio:

def getIndex[T, CC](seq: CC, value: T)(implicit conv: CC => Seq[T]) = seq.indexOf(value)

getIndex("abc", 'a')

Il metodo getIndexpuò ricevere qualsiasi oggetto, purché sia ​​disponibile una conversione implicita dalla sua classe a Seq[T]. Per questo motivo, posso passare un Stringa getIndex, e funzionerà.

Dietro le quinte, il compilatore cambia seq.IndexOf(value)a conv(seq).indexOf(value).

Questo è così utile che c'è dello zucchero sintattico per scriverli. Usando questo zucchero sintattico, si getIndexpuò definire così:

def getIndex[T, CC <% Seq[T]](seq: CC, value: T) = seq.indexOf(value)

Questo zucchero sintattico è descritto come un limite di vista , simile a un limite superiore ( CC <: Seq[Int]) o un limite inferiore ( T >: Null).

Vincoli di contesto

Un altro modello comune nei parametri impliciti è il modello di classe di tipo . Questo modello consente di fornire interfacce comuni alle classi che non le hanno dichiarate. Può fungere sia da modello a ponte - ottenendo la separazione delle preoccupazioni - sia da modello adattatore.

La Integralclasse che hai citato è un classico esempio di modello di classe di tipo. Un altro esempio sulla libreria standard di Scala è Ordering. C'è una biblioteca che fa ampio uso di questo modello, chiamato Scalaz.

Questo è un esempio del suo utilizzo:

def sum[T](list: List[T])(implicit integral: Integral[T]): T = {
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

C'è anche uno zucchero sintattico per esso, chiamato limite di contesto , che è reso meno utile dalla necessità di fare riferimento all'implicito. Una conversione diretta di quel metodo è simile alla seguente:

def sum[T : Integral](list: List[T]): T = {
    val integral = implicitly[Integral[T]]
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

I limiti di contesto sono più utili quando è sufficiente passarli ad altri metodi che li utilizzano. Ad esempio, il metodo sortedin Seqbisogno di un implicito Ordering. Per creare un metodo reverseSort, si potrebbe scrivere:

def reverseSort[T : Ordering](seq: Seq[T]) = seq.sorted.reverse

Poiché è Ordering[T]stato implicitamente passato a reverseSort, può quindi passarlo implicitamente a sorted.

Da dove vengono gli impliciti?

Quando il compilatore vede la necessità di un implicito, sia perché stai chiamando un metodo che non esiste sulla classe dell'oggetto, sia perché stai chiamando un metodo che richiede un parametro implicito, cercherà un implicito adatto alla necessità .

Questa ricerca obbedisce a determinate regole che definiscono quali impliciti sono visibili e quali no. La seguente tabella che mostra dove il compilatore cercherà gli impliciti è stata presa da un'eccellente presentazione sugli impliciti di Josh Suereth, che consiglio vivamente a chiunque voglia migliorare la propria conoscenza della Scala. Da allora è stato integrato con feedback e aggiornamenti.

Gli impliciti disponibili sotto il numero 1 di seguito hanno la precedenza su quelli sotto il numero 2. Oltre a ciò, se ci sono diversi argomenti ammissibili che corrispondono al tipo di parametro implicito, verrà scelto uno più specifico usando le regole della risoluzione di sovraccarico statico (vedere Scala Specifica §6.26.3). Informazioni più dettagliate sono disponibili in una domanda a cui mi collego alla fine di questa risposta.

  1. Primo sguardo nel campo di applicazione attuale
    • Impliciti definiti nell'ambito attuale
    • Importazioni esplicite
    • importazioni di caratteri jolly
    • Stesso ambito in altri file
  2. Ora guarda i tipi associati in
    • Oggetti compagni di un tipo
    • Ambito implicito del tipo di argomento (2.9.1)
    • Ambito implicito degli argomenti di tipo (2.8.0)
    • Oggetti esterni per tipi nidificati
    • Altre dimensioni

Diamo alcuni esempi per loro:

Impliciti definiti nell'ambito corrente

implicit val n: Int = 5
def add(x: Int)(implicit y: Int) = x + y
add(5) // takes n from the current scope

Importazioni esplicite

import scala.collection.JavaConversions.mapAsScalaMap
def env = System.getenv() // Java map
val term = env("TERM")    // implicit conversion from Java Map to Scala Map

Importazioni con caratteri jolly

def sum[T : Integral](list: List[T]): T = {
    val integral = implicitly[Integral[T]]
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

Stesso ambito in altri file

Modifica : sembra che questo non abbia una precedenza diversa. Se hai qualche esempio che dimostra una distinzione di precedenza, fai un commento. Altrimenti, non fare affidamento su questo.

Questo è come il primo esempio, ma supponendo che la definizione implicita sia in un file diverso rispetto al suo utilizzo. Vedi anche come gli oggetti pacchetto potrebbero essere utilizzati per portare impliciti.

Oggetti compagni di un tipo

Ci sono due compagni oggetto di nota qui. Innanzitutto, viene esaminato l'oggetto compagno del tipo "sorgente". Ad esempio, all'interno dell'oggetto Optionc'è una conversione implicita in Iterable, quindi si può chiamare Iterablemetodi Optiono passare Optiona qualcosa che si aspetta un Iterable. Per esempio:

for {
    x <- List(1, 2, 3)
    y <- Some('x')
} yield (x, y)

Tale espressione viene tradotta dal compilatore in

List(1, 2, 3).flatMap(x => Some('x').map(y => (x, y)))

Tuttavia, si List.flatMapaspetta un TraversableOnce, che Optionnon lo è. Il compilatore quindi guarda all'interno Optiondel compagno oggetto e trova la conversione in Iterable, che è a TraversableOnce, rendendo questa espressione corretta.

In secondo luogo, l'oggetto associato del tipo previsto:

List(1, 2, 3).sorted

Il metodo sortedprende un implicito Ordering. In questo caso, guarda all'interno dell'oggetto Ordering, compagno della classe Orderinge trova un implicito Ordering[Int]lì.

Si noti che vengono esaminati anche gli oggetti companion di superclasse. Per esempio:

class A(val n: Int)
object A { 
    implicit def str(a: A) = "A: %d" format a.n
}
class B(val x: Int, y: Int) extends A(y)
val b = new B(5, 2)
val s: String = b  // s == "A: 2"

È così che Scala ha trovato l'implicito Numeric[Int]e Numeric[Long]nella tua domanda, a proposito, come si trovano all'interno Numeric, no Integral.

Ambito implicito del tipo di argomento

Se si dispone di un metodo con un tipo di argomento A, Averrà considerato anche l'ambito implicito del tipo . Per "ambito implicito" intendo che tutte queste regole saranno applicate in modo ricorsivo - per esempio, l'oggetto associato Asarà cercato in modo implicito, come da regola sopra.

Si noti che ciò non significa che l'ambito implicito di Averrà cercato per le conversioni di quel parametro, ma dell'intera espressione. Per esempio:

class A(val n: Int) {
  def +(other: A) = new A(n + other.n)
}
object A {
  implicit def fromInt(n: Int) = new A(n)
}

// This becomes possible:
1 + new A(1)
// because it is converted into this:
A.fromInt(1) + new A(1)

Questo è disponibile da Scala 2.9.1.

Ambito implicito di argomenti di tipo

Ciò è necessario per far sì che il modello di classe di tipo funzioni davvero. Considera Ordering, ad esempio: viene fornito con alcuni impliciti nel suo oggetto associato, ma non puoi aggiungere elementi ad esso. Quindi, come puoi crearne uno Orderingper la tua classe che si trova automaticamente?

Cominciamo con l'implementazione:

class A(val n: Int)
object A {
    implicit val ord = new Ordering[A] {
        def compare(x: A, y: A) = implicitly[Ordering[Int]].compare(x.n, y.n)
    }
}

Quindi, considera cosa succede quando chiami

List(new A(5), new A(2)).sorted

Come abbiamo visto, il metodo sortedprevede un Ordering[A](in realtà, prevede un Ordering[B], dove B >: A). Non c'è nulla di simile all'interno Orderinge non esiste un tipo "sorgente" su cui guardare. Ovviamente, lo sta trovando all'interno A, che è un argomento tipo di Ordering.

Questo è anche il modo in cui funzionano diversi metodi di raccolta in attesa CanBuildFrom: gli impliciti si trovano all'interno degli oggetti associati ai parametri di tipo di CanBuildFrom.

Nota : Orderingè definito come trait Ordering[T], dove Tè un parametro di tipo. In precedenza, avevo detto che Scala guardava all'interno dei parametri di tipo, il che non ha molto senso. L'implicito cercato sopra è Ordering[A], dove Aè un tipo reale, non un parametro di tipo: è un argomento di tipo a Ordering. Vedere la sezione 7.2 delle specifiche Scala.

Questo è disponibile da Scala 2.8.0.

Oggetti esterni per tipi nidificati

In realtà non ho visto esempi di questo. Le sarei grato se qualcuno potesse condividerne uno. Il principio è semplice:

class A(val n: Int) {
  class B(val m: Int) { require(m < n) }
}
object A {
  implicit def bToString(b: A#B) = "B: %d" format b.m
}
val a = new A(5)
val b = new a.B(3)
val s: String = b  // s == "B: 3"

Altre dimensioni

Sono abbastanza sicuro che fosse uno scherzo, ma questa risposta potrebbe non essere aggiornata. Quindi non prendere questa domanda come l'arbitro finale di ciò che sta accadendo, e se noti che è diventato obsoleto, ti prego di informarmi in modo che io possa risolverlo.

MODIFICARE

Domande correlate di interesse:


60
È tempo che inizi a usare le tue risposte in un libro, ormai è solo questione di mettere tutto insieme.
pedrofurla,

3
@pedrofurla Sono stato considerato scrivere un libro in portoghese. Se qualcuno può trovarmi un contatto con un editore tecnico ...
Daniel C. Sobral,

2
Vengono anche cercati gli oggetti pacchetto dei compagni delle parti del tipo. lampsvn.epfl.ch/trac/scala/ticket/4427
retronym

1
In questo caso, fa parte dell'ambito implicito. Il sito di chiamata non deve necessariamente trovarsi all'interno di quel pacchetto. È stato sorprendente per me.
retronym,

2
Sì, quindi stackoverflow.com/questions/8623055 tratta questo in modo specifico, ma ho notato che hai scritto "Il seguente elenco è destinato a essere presentato in ordine di precedenza ... per favore, segnala." Fondamentalmente, gli elenchi interni dovrebbero essere non ordinati poiché hanno tutti lo stesso peso (almeno in 2.10).
Eugene Yokota,

23

Volevo scoprire la precedenza della risoluzione implicita dei parametri, non solo dove cercava, quindi ho scritto un post sul blog rivisitando impliciti senza tasse di importazione (e di nuovo la precedenza implicita dei parametri dopo un feedback).

Ecco la lista:

  • 1) implicito visibile all'attuale ambito di invocazione tramite dichiarazione locale, importazioni, ambito esterno, eredità, oggetto pacchetto accessibile senza prefisso.
  • 2) ambito implicito , che contiene ogni tipo di oggetto associato e oggetto pacchetto che presentano una relazione con il tipo di oggetto implicito che cerchiamo (ad esempio oggetto pacchetto del tipo, oggetto compagno del tipo stesso, se del caso tipo di costruttore, di i suoi parametri, se presenti, e anche del suo supertipo e supertraits).

Se in entrambe le fasi troviamo più di una regola di sovraccarico statica implicita per risolverla.


3
Questo potrebbe essere migliorato se scrivessi del codice semplicemente definendo pacchetti, oggetti, tratti e classi e usando le loro lettere quando ti riferisci all'ambito. Non è necessario inserire alcuna dichiarazione di metodo: solo nomi e chi estende chi e in quale ambito.
Daniel C. Sobral,
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.