Wo sucht Scala nach Implikaten?

398

Eine implizite Frage an Scala-Neulinge scheint zu sein: Wo sucht der Compiler nach Impliziten? Ich meine implizit, weil die Frage nie vollständig formuliert zu sein scheint, als gäbe es keine Worte dafür. :-) Woher kommen zum Beispiel die Werte für integralunten?

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

Eine andere Frage, die sich an diejenigen richtet, die sich entschließen, die Antwort auf die erste Frage zu lernen, ist, wie der Compiler in bestimmten Situationen offensichtlicher Mehrdeutigkeit das zu verwendende Implizit auswählt (das aber trotzdem kompiliert).

Definiert beispielsweise scala.Predefzwei Konvertierungen von String: eine nach WrappedStringund eine andere nach StringOps. Beide Klassen teilen jedoch viele Methoden. Warum beschwert sich Scala nicht über Mehrdeutigkeiten, wenn sie beispielsweise anruft map?

Hinweis: Diese Frage wurde von dieser anderen Frage inspiriert , in der Hoffnung, das Problem allgemeiner zu formulieren. Das Beispiel wurde von dort kopiert, da in der Antwort darauf verwiesen wird.

Daniel C. Sobral
quelle

Antworten:

554

Arten von Implikationen

Implizite in Scala beziehen sich entweder auf einen Wert, der sozusagen "automatisch" übergeben werden kann, oder auf eine Konvertierung von einem Typ in einen anderen, die automatisch vorgenommen wird.

Implizite Konvertierung

Wenn man kurz über den letzteren Typ spricht und eine Methode mfür ein Objekt oeiner Klasse aufruft Cund diese Klasse keine Methode unterstützt m, sucht Scala nach einer impliziten Konvertierung von Czu etwas, das unterstützt m. Ein einfaches Beispiel wäre die Methode mapauf String:

"abc".map(_.toInt)

Stringunterstützt die Methode nicht map, StringOpstut es aber und es gibt eine implizite Konvertierung von Stringzu StringOpsverfügbar (siehe implicit def augmentStringweiter Predef).

Implizite Parameter

Die andere Art von implizit ist der implizite Parameter . Diese werden wie alle anderen Parameter an Methodenaufrufe übergeben, aber der Compiler versucht, sie automatisch auszufüllen. Wenn es nicht kann, wird es sich beschweren. Man kann diese Parameter explizit übergeben, wie man sie breakOutbeispielsweise verwendet (siehe Frage zu breakOut, an einem Tag, an dem Sie sich einer Herausforderung stellen).

In diesem Fall muss die Notwendigkeit eines Impliziten wie der fooMethodendeklaration deklariert werden :

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

Grenzen anzeigen

Es gibt eine Situation, in der ein Impliziter sowohl eine implizite Konvertierung als auch ein impliziter Parameter ist. Zum Beispiel:

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

getIndex("abc", 'a')

Die Methode getIndexkann jedes Objekt empfangen, solange eine implizite Konvertierung von ihrer Klasse in verfügbar ist Seq[T]. Aus diesem Grund kann ich ein Stringan übergeben getIndex, und es wird funktionieren.

Hinter den Kulissen wechselt der Compiler seq.IndexOf(value)zu conv(seq).indexOf(value).

Dies ist so nützlich, dass es syntaktischen Zucker gibt, um sie zu schreiben. Mit diesem syntaktischen Zucker getIndexkann wie folgt definiert werden:

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

Dieser syntaktische Zucker wird als Ansichtsgrenze beschrieben , ähnlich einer Obergrenze ( CC <: Seq[Int]) oder einer Untergrenze ( T >: Null).

Kontextgrenzen

Ein weiteres häufiges Muster in impliziten Parametern ist das Typklassenmuster . Dieses Muster ermöglicht die Bereitstellung gemeinsamer Schnittstellen für Klassen, die diese nicht deklariert haben. Es kann sowohl als Brückenmuster - zur Trennung von Bedenken - als auch als Adaptermuster dienen.

Die von IntegralIhnen erwähnte Klasse ist ein klassisches Beispiel für ein Muster einer Typklasse. Ein weiteres Beispiel für die Standardbibliothek von Scala ist Ordering. Es gibt eine Bibliothek namens Scalaz, die dieses Muster stark nutzt.

Dies ist ein Beispiel für seine Verwendung:

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

Es gibt auch syntaktischen Zucker dafür, der als kontextgebunden bezeichnet wird , was durch die Notwendigkeit, auf das Implizite zu verweisen, weniger nützlich wird. Eine direkte Konvertierung dieser Methode sieht folgendermaßen aus:

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)(_ + _)
}

Context Grenzen sind nützlich , wenn Sie nur brauchen , um passieren sie zu anderen Methoden , die sie verwenden. Zum Beispiel benötigt die Methode sortedon Seqein implizites Ordering. Um eine Methode zu erstellen reverseSort, könnte man schreiben:

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

Da Ordering[T]implizit an übergeben wurde reverseSort, kann es dann implizit an übergeben werden sorted.

Woher kommen Implizite?

Wenn der Compiler die Notwendigkeit eines Implizits erkennt, entweder weil Sie eine Methode aufrufen, die in der Klasse des Objekts nicht vorhanden ist, oder weil Sie eine Methode aufrufen, für die ein impliziter Parameter erforderlich ist, sucht er nach einem Implizit, das dem Bedarf entspricht .

Diese Suche folgt bestimmten Regeln, die definieren, welche Implikate sichtbar sind und welche nicht. Die folgende Tabelle, die zeigt, wo der Compiler nach Implikits suchen wird, stammt aus einer hervorragenden Präsentation über Implikits von Josh Suereth, die ich jedem wärmstens empfehlen kann, der sein Scala-Wissen verbessern möchte. Es wurde seitdem durch Feedback und Updates ergänzt.

Die unter Nummer 1 unten verfügbaren Implizite haben Vorrang vor denen unter Nummer 2. Abgesehen davon, wenn mehrere geeignete Argumente vorhanden sind, die dem Typ des impliziten Parameters entsprechen, wird nach den Regeln der statischen Überladungsauflösung ein spezifischstes ausgewählt (siehe Scala) Spezifikation §6.26.3). Nähere Informationen finden Sie in einer Frage, auf die ich am Ende dieser Antwort verweise.

  1. Schauen Sie sich zuerst den aktuellen Umfang an
    • Im aktuellen Geltungsbereich definierte Implikationen
    • Explizite Importe
    • Platzhalterimporte
    • Gleicher Bereich in anderen Dateien
  2. Schauen Sie sich nun die zugehörigen Typen in an
    • Begleitobjekte eines Typs
    • Impliziter Umfang des Typs eines Arguments (2.9.1)
    • Impliziter Umfang der Typargumente (2.8.0)
    • Äußere Objekte für verschachtelte Typen
    • Andere Abmessungen

Geben wir ihnen einige Beispiele:

Im aktuellen Geltungsbereich definierte Implikationen

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

Explizite Importe

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

Platzhalterimporte

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)(_ + _)
}

Gleicher Bereich in anderen Dateien

Bearbeiten : Es scheint, dass dies keine andere Priorität hat. Wenn Sie ein Beispiel haben, das eine Prioritätsunterscheidung zeigt, geben Sie bitte einen Kommentar ab. Andernfalls verlassen Sie sich nicht auf diesen.

Dies ist wie im ersten Beispiel, jedoch unter der Annahme, dass sich die implizite Definition in einer anderen Datei befindet als ihre Verwendung. Siehe auch, wie Paketobjekte verwendet werden können, um Implizite einzufügen.

Begleitobjekte eines Typs

Hier gibt es zwei bemerkenswerte Objektbegleiter. Zunächst wird der Objektbegleiter vom Typ "Quelle" untersucht. Zum Beispiel gibt es innerhalb des Objekts Optioneine implizite Konvertierung in Iterable, sodass man IterableMethoden aufrufen Optionoder Optionan etwas übergeben kann, das eine erwartet Iterable. Zum Beispiel:

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

Dieser Ausdruck wird vom Compiler in übersetzt

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

Allerdings List.flatMaperwartet ein TraversableOnce, was Optionnicht ist. Der Compiler schaut dann in Optionden Objektbegleiter und findet die Konvertierung in Iterablea TraversableOnce, wodurch dieser Ausdruck korrekt wird.

Zweitens das Begleitobjekt des erwarteten Typs:

List(1, 2, 3).sorted

Die Methode ist sortedimplizit Ordering. In diesem Fall schaut es in das Objekt Ordering, den Begleiter der Klasse Ordering, und findet dort ein implizites Objekt Ordering[Int].

Beachten Sie, dass auch Begleitobjekte von Superklassen untersucht werden. Zum Beispiel:

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"

So fand Scala das Implizite Numeric[Int]und Numeric[Long]in Ihrer Frage übrigens, wie sie sich darin befinden Numeric, nicht Integral.

Impliziter Umfang des Typs eines Arguments

Wenn Sie eine Methode mit einem Argumenttyp haben A, Awird auch der implizite Umfang des Typs berücksichtigt. Mit "impliziter Bereich" meine ich, dass alle diese Regeln rekursiv angewendet werden - zum Beispiel wird das Begleitobjekt von Agemäß der obigen Regel nach impliziten durchsucht.

Beachten Sie, dass dies nicht bedeutet, dass der implizite Bereich von Anach Konvertierungen dieses Parameters durchsucht wird, sondern des gesamten Ausdrucks. Zum Beispiel:

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)

Dies ist seit Scala 2.9.1 verfügbar.

Impliziter Umfang der Typargumente

Dies ist erforderlich, damit das Typklassenmuster wirklich funktioniert. Bedenken OrderingSie zum Beispiel: Das Begleitobjekt enthält einige Implikationen, aber Sie können nichts hinzufügen. Wie können Sie also eine Orderingfür Ihre eigene Klasse erstellen, die automatisch gefunden wird?

Beginnen wir mit der Implementierung:

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)
    }
}

Überlegen Sie also, was passiert, wenn Sie anrufen

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

Wie wir gesehen haben, sortederwartet die Methode ein Ordering[A](tatsächlich erwartet sie ein Ordering[B], wo B >: A). Es gibt so etwas nicht im Inneren Orderingund es gibt keinen "Quellentyp", auf den man schauen könnte. Offensichtlich findet es es im Inneren A, was ein typisches Argument von ist Ordering.

Auf diese Weise funktionieren auch verschiedene Erfassungsmethoden, die CanBuildFromArbeit erwarten : Die Implikationen werden in Begleitobjekten zu den Typparametern von gefunden CanBuildFrom.

Hinweis : Orderingist definiert als trait Ordering[T], wo Tein Typparameter ist. Zuvor habe ich gesagt, dass Scala sich mit Typparametern befasst, was wenig Sinn macht. Das oben gesuchte implizite ist Ordering[A], wo Aein tatsächlicher Typ ist, kein Typparameter: Es ist ein Typargument für Ordering. Siehe Abschnitt 7.2 der Scala-Spezifikation.

Dies ist seit Scala 2.8.0 verfügbar.

Äußere Objekte für verschachtelte Typen

Ich habe keine Beispiele dafür gesehen. Ich wäre dankbar, wenn jemand einen teilen könnte. Das Prinzip ist einfach:

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"

Andere Abmessungen

Ich bin mir ziemlich sicher, dass dies ein Witz war, aber diese Antwort ist möglicherweise nicht aktuell. Nehmen Sie diese Frage also nicht als endgültigen Schiedsrichter für das Geschehen. Wenn Sie bemerkt haben, dass sie veraltet ist, informieren Sie mich bitte, damit ich sie beheben kann.

BEARBEITEN

Verwandte Fragen von Interesse:

Daniel C. Sobral
quelle
60
Es ist Zeit für Sie, Ihre Antworten in einem Buch zu verwenden. Jetzt geht es nur noch darum, alles zusammenzufügen.
Pedrofurla
3
@pedrofurla Ich habe überlegt, ein Buch auf Portugiesisch zu schreiben. Wenn mir jemand einen Kontakt mit einem technischen Verlag finden kann ...
Daniel C. Sobral
2
Die Paketobjekte der Gefährten der Teile des Typs werden ebenfalls durchsucht. lampsvn.epfl.ch/trac/scala/ticket/4427
retronym
1
In diesem Fall ist dies Teil des impliziten Bereichs. Die Anrufstelle muss sich nicht in diesem Paket befinden. Das hat mich überrascht.
Retronym
2
Ja, also behandelt stackoverflow.com/questions/8623055 dies speziell, aber ich habe festgestellt, dass Sie geschrieben haben: "Die folgende Liste soll in der Rangfolge angezeigt werden ... bitte melden." Grundsätzlich sollten die inneren Listen ungeordnet sein, da sie alle das gleiche Gewicht haben (zumindest in 2.10).
Eugene Yokota
23

Ich wollte den Vorrang der impliziten Parameterauflösung herausfinden, nicht nur, wo sie gesucht wird, und schrieb deshalb einen Blog-Beitrag , in dem implizite Vorgaben ohne Importsteuer (und nach einigen Rückmeldungen erneut implizite Vorrang vor Parametern) erneut aufgegriffen wurden.

Hier ist die Liste:

  • 1) Implikationen, die für den aktuellen Aufrufbereich über lokale Deklaration sichtbar sind, Importe, äußerer Bereich, Vererbung, Paketobjekt, auf die ohne Präfix zugegriffen werden kann.
  • 2) impliziter Bereich , der alle Arten von Begleitobjekten und Paketobjekten enthält, die in irgendeiner Beziehung zum impliziten Typ stehen, nach dem wir suchen (dh Paketobjekt vom Typ, Begleitobjekt vom Typ selbst, von seinem Typkonstruktor, falls vorhanden, von gegebenenfalls seine Parameter sowie seinen Supertyp und seine Supertraits).

Wenn wir in beiden Phasen mehr als eine implizite statische Überladungsregel finden, wird diese behoben.

Eugene Yokota
quelle
3
Dies könnte verbessert werden, wenn Sie Code geschrieben haben, der nur Pakete, Objekte, Merkmale und Klassen definiert und deren Buchstaben verwendet, wenn Sie sich auf den Bereich beziehen. Es ist überhaupt nicht erforderlich, eine Methodendeklaration abzugeben - nur Namen und wer wen erweitert und in welchem ​​Umfang.
Daniel C. Sobral