In gewissem Sinne repräsentieren Implizite den globalen Zustand. Sie sind jedoch nicht veränderlich, was das wahre Problem bei globalen Variablen ist - Sie sehen keine Leute, die sich über globale Konstanten beschweren, oder? Tatsächlich schreiben Codierungsstandards normalerweise vor, dass Sie alle Konstanten in Ihrem Code in Konstanten oder Aufzählungen umwandeln, die normalerweise global sind.
Beachten Sie auch, dass sich Implizite nicht in einem flachen Namespace befinden, was auch bei Globals ein häufiges Problem ist. Sie sind explizit an Typen und damit an die Pakethierarchie dieser Typen gebunden.
Nehmen Sie also Ihre Globals, machen Sie sie unveränderlich und initialisieren Sie sie auf der Deklarationsseite und setzen Sie sie in Namespaces. Sehen sie immer noch wie Globale aus? Sehen sie immer noch problematisch aus?
Aber lasst uns hier nicht aufhören. Implizite sind an Typen gebunden und genauso "global" wie Typen. Stört Sie die Tatsache, dass Typen global sind?
Es gibt viele Anwendungsfälle, aber wir können anhand ihrer Vorgeschichte einen kurzen Überblick geben. Ursprünglich hatte Scala keine Implikationen. Was Scala hatte, waren Ansichtstypen, eine Funktion, die viele andere Sprachen hatten. Wir können das heute noch sehen, wenn Sie so etwas schreiben T <% Ordered[T]
, was bedeutet, dass der Typ T
als Typ angesehen werden kann Ordered[T]
. Ansichtstypen sind eine Möglichkeit, automatische Umwandlungen für Typparameter (Generika) verfügbar zu machen.
Scala verallgemeinerte dieses Merkmal dann mit Impliziten. Automatische Umwandlungen sind nicht mehr vorhanden. Stattdessen haben Sie implizite Konvertierungen - dies sind nur Function1
Werte und können daher als Parameter übergeben werden. Von da an T <% Ordered[T]
bedeutete dies , dass ein Wert für eine implizite Konvertierung als Parameter übergeben wurde. Da die Umwandlung automatisch erfolgt, muss der Aufrufer der Funktion den Parameter nicht explizit übergeben. Daher wurden diese Parameter zu impliziten Parametern .
Beachten Sie, dass es zwei Konzepte gibt - implizite Konvertierungen und implizite Parameter -, die sehr nahe beieinander liegen, sich jedoch nicht vollständig überschneiden.
Auf jeden Fall wurden Ansichtstypen zu syntaktischem Zucker für implizite Konvertierungen, die implizit übergeben wurden. Sie würden folgendermaßen umgeschrieben:
def max[T <% Ordered[T]](a: T, b: T): T = if (a < b) b else a
def max[T](a: T, b: T)(implicit $ev1: Function1[T, Ordered[T]]): T = if ($ev1(a) < b) b else a
Die impliziten Parameter sind lediglich eine Verallgemeinerung dieses Musters, die es ermöglicht, jede Art von impliziten Parametern anstelle von nur zu übergeben Function1
. Die tatsächliche Verwendung für sie folgte dann, und syntaktischer Zucker für diese Verwendungen kam letztere.
Eine davon sind Kontextgrenzen , die zum Implementieren des Typklassenmusters verwendet werden (Muster, da es sich nicht um eine integrierte Funktion handelt, sondern nur um die Verwendung der Sprache, die ähnliche Funktionen wie die Typklasse von Haskell bietet). Eine Kontextbindung wird verwendet, um einen Adapter bereitzustellen, der Funktionen implementiert, die einer Klasse inhärent sind, aber nicht von dieser deklariert werden. Es bietet die Vorteile von Vererbung und Schnittstellen ohne deren Nachteile. Zum Beispiel:
def max[T](a: T, b: T)(implicit $ev1: Ordering[T]): T = if ($ev1.lt(a, b)) b else a
def max[T: Ordering](a: T, b: T): T = if (implicitly[Ordering[T]].lt(a, b)) b else a
Sie haben das wahrscheinlich schon verwendet - es gibt einen häufigen Anwendungsfall, den die Leute normalerweise nicht bemerken. Es ist das:
new Array[Int](size)
Dabei wird ein an Klassenmanifeste gebundener Kontext verwendet, um eine solche Array-Initialisierung zu ermöglichen. Wir können das an diesem Beispiel sehen:
def f[T](size: Int) = new Array[T](size)
Sie können es so schreiben:
def f[T: ClassManifest](size: Int) = new Array[T](size)
In der Standardbibliothek werden am häufigsten folgende Kontextgrenzen verwendet:
Manifest
ClassManifest
Ordering
Numeric
CanBuildFrom
Die letzteren drei sind meist mit einer Sammlung, mit Methoden wie max
, sum
und map
. Eine Bibliothek, die Kontextgrenzen in großem Umfang nutzt, ist Scalaz.
Eine andere übliche Verwendung besteht darin, die Kesselplatte bei Vorgängen zu verringern, die einen gemeinsamen Parameter haben müssen. Zum Beispiel Transaktionen:
def withTransaction(f: Transaction => Unit) = {
val txn = new Transaction
try { f(txn); txn.commit() }
catch { case ex => txn.rollback(); throw ex }
}
withTransaction { txn =>
op1(data)(txn)
op2(data)(txn)
op3(data)(txn)
}
Was dann so vereinfacht wird:
withTransaction { implicit txn =>
op1(data)
op2(data)
op3(data)
}
Dieses Muster wird mit dem Transaktionsspeicher verwendet, und ich denke (bin mir aber nicht sicher), dass die Scala-E / A-Bibliothek es auch verwendet.
Die dritte häufig verwendete Verwendung besteht darin, Beweise für die übergebenen Typen zu erstellen, die es ermöglichen, zur Kompilierungszeit Dinge zu erkennen, die andernfalls zu Laufzeitausnahmen führen würden. Siehe diese Definition beispielsweise unter Option
:
def flatten[B](implicit ev: A <:< Option[B]): Option[B]
Das macht es möglich:
scala> Option(Option(2)).flatten
res0: Option[Int] = Some(2)
scala> Option(2).flatten
<console>:8: error: Cannot prove that Int <:< Option[B].
Option(2).flatten
^
Eine Bibliothek, die diese Funktion in großem Umfang nutzt, ist Shapeless.
Ich denke nicht, dass das Beispiel der Akka-Bibliothek in eine dieser vier Kategorien passt, aber das ist der springende Punkt bei allgemeinen Funktionen: Die Leute können es auf alle möglichen Arten verwenden, anstatt auf die vom Sprachdesigner vorgeschriebenen Arten.
Wenn Sie gerne verschrieben werden (wie zum Beispiel Python), dann ist Scala einfach nichts für Sie.