Bisher sehen implizite Parameter in Scala für mich nicht gut aus - es ist zu nahe an globalen Variablen, aber da Scala wie eine ziemlich strenge Sprache erscheint, bezweifle ich meiner Meinung nach :-).
Frage: Können Sie ein reales (oder nahes) gutes Beispiel zeigen, wenn implizite Parameter wirklich funktionieren? IOW: etwas Ernsthafteres als das showPrompt
, das ein solches Sprachdesign rechtfertigen würde.
Oder im Gegenteil - könnten Sie ein zuverlässiges Sprachdesign zeigen (kann imaginär sein), das implizit nicht notwendig macht? Ich denke, dass selbst kein Mechanismus besser ist als implizite, weil der Code klarer ist und es keine Vermutungen gibt.
Bitte beachten Sie, ich frage nach Parametern, nicht nach impliziten Funktionen (Konvertierungen)!
Aktualisierung
Globale Variablen
Vielen Dank für alle tollen Antworten. Vielleicht kläre ich meinen Einwand "globale Variablen". Betrachten Sie eine solche Funktion:
max(x : Int,y : Int) : Int
du nennst es
max(5,6);
Sie könnten es (!) so machen:
max(x:5,y:6);
aber in meinen Augen implicits
funktioniert das so:
x = 5;
y = 6;
max()
es unterscheidet sich nicht sehr von einem solchen Konstrukt (PHP-ähnlich)
max() : Int
{
global x : Int;
global y : Int;
...
}
Dereks Antwort
Dies ist ein großartiges Beispiel. Wenn Sie sich jedoch vorstellen können, dass das Senden von Nachrichten, die nicht verwendet werden, flexibel ist, veröffentlichen implicit
Sie bitte ein Gegenbeispiel. Ich bin sehr gespannt auf Reinheit im Sprachdesign ;-).
quelle
Antworten:
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 TypT
als Typ angesehen werden kannOrdered[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 anT <% 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 // latter followed by the syntactic sugar 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) // won't compile!
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 // Provides reflection on a type ClassManifest // Provides reflection on a type after erasure Ordering // Total ordering of elements Numeric // Basic arithmetic of elements CanBuildFrom // Collection creation
Die letzteren drei sind meist mit einer Sammlung, mit Methoden wie
max
,sum
undmap
. 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 // compiles res0: Option[Int] = Some(2) scala> Option(2).flatten // does not compile! <console>:8: error: Cannot prove that Int <:< Option[B]. Option(2).flatten // does not compile! ^
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.
quelle
Sicher. Akka hat ein großartiges Beispiel dafür in Bezug auf seine Schauspieler. Wenn Sie sich in der
receive
Methode eines Schauspielers befinden , möchten Sie möglicherweise eine Nachricht an einen anderen Schauspieler senden. Wenn Sie dies tun, bündelt Akka (standardmäßig) den aktuellen Akteur wie folgtsender
aus der Nachricht:trait ScalaActorRef { this: ActorRef => ... def !(message: Any)(implicit sender: ActorRef = null): Unit ... }
Das
sender
ist implizit. Im Schauspieler gibt es eine Definition, die wie folgt aussieht:trait Actor { ... implicit val self = context.self ... }
Dies schafft den impliziten Wert im Rahmen Ihres eigenen Codes und ermöglicht es Ihnen, einfache Dinge wie diese zu tun:
someOtherActor ! SomeMessage
Jetzt können Sie dies auch tun, wenn Sie möchten:
someOtherActor.!(SomeMessage)(self)
oder
someOtherActor.!(SomeMessage)(null)
oder
someOtherActor.!(SomeMessage)(anotherActorAltogether)
Aber normalerweise nicht. Sie behalten nur die natürliche Verwendung bei, die durch die implizite Wertedefinition im Actor-Merkmal ermöglicht wird. Es gibt ungefähr eine Million andere Beispiele. Die Sammlungsklassen sind riesig. Wenn Sie in einer nicht trivialen Scala-Bibliothek herumwandern, finden Sie eine Lastwagenladung.
quelle
Traversable.max
Typklassen und dergleichen.implicits
, dass Sie es getan haben , ich nicht - Ihr Beispiel ist großartig als Rätsel (und ich bin dafür dankbar), wie Sie das Problem aus einer anderen Perspektive angehen können. Das ist mein POV, also ersparen Sie mir bitte die Ratschläge "Lernen Sie die Sprache, in der Sie programmieren" (wahr, aber nicht sehr höflich - Bevormundung ist keine Diskussion willkommen).Ein Beispiel wären die Vergleichsoperationen an
Traversable[A]
. ZBmax
odersort
:def max[B >: A](implicit cmp: Ordering[B]) : A
Diese können nur dann sinnvoll definiert werden , wenn eine Operation
<
aufA
. Ohne Implikationen müssten wir alsoOrdering[B]
jedes Mal den Kontext angeben, wenn wir diese Funktion verwenden möchten. (Oder geben Sie die statische Typprüfung im Inneren aufmax
und riskieren Sie einen Laufzeitfehler.)Wenn jedoch eine implizite Vergleich Typklasse ist in Umfang, zB einige
Ordering[Int]
, können wir es einfach sofort verwenden oder einfach die Vergleichsmethode ändern , indem Sie einen anderen Wert für den impliziten Parameter liefert.Natürlich können Implizite abgeschattet werden, und daher kann es Situationen geben, in denen das tatsächliche Implizit, das in den Geltungsbereich fällt, nicht klar genug ist. Für einfache Anwendungen von
max
odersort
es könnte in der Tat ausreichend sein , eine feste Ordnung zu haben ,trait
aufInt
und einige Syntax verwenden , um zu überprüfen , ob dieses Merkmal zur Verfügung. Dies würde jedoch bedeuten, dass es keine zusätzlichen Merkmale geben könnte und jeder Code die ursprünglich definierten Merkmale verwenden müsste.Ergänzung:
Antwort auf den globalen Variablenvergleich .
Ich denke, Sie haben Recht, dass in einem Code wie abgeschnitten
implicit val num = 2 implicit val item = "Orange" def shopping(implicit num: Int, item: String) = { "I’m buying "+num+" "+item+(if(num==1) "." else "s.") } scala> shopping res: java.lang.String = I’m buying 2 Oranges.
es kann nach faulen und bösen globalen Variablen riechen. Der entscheidende Punkt ist jedoch, dass es möglicherweise nur eine implizite Variable pro Typ im Bereich gibt. Ihr Beispiel mit zwei
Int
s wird nicht funktionieren.Dies bedeutet auch, dass implizite Variablen praktisch nur verwendet werden, wenn es für einen Typ eine nicht unbedingt eindeutige, aber eindeutige Primärinstanz gibt. Die
self
Referenz eines Schauspielers ist ein gutes Beispiel für so etwas. Das Typklassenbeispiel ist ein weiteres Beispiel. Es kann Dutzende algebraischer Vergleiche für jeden Typ geben, aber es gibt einen besonderen. (Auf einer anderen Ebene kann die tatsächliche Zeilennummer im Code selbst auch eine gute implizite Variable darstellen, solange sie einen sehr unterschiedlichen Typ verwendet.)Normalerweise verwenden Sie
implicit
s nicht für alltägliche Typen. Und bei speziellen Typen (wieOrdering[Int]
) besteht kein allzu großes Risiko, sie zu beschatten.quelle
Int
bei Bedarf kein anderes Merkmal hinzufügen könnte, z. B. oder einen anderen vordefinierten Typ. (Ein oft zitiertes Beispiel ist das einer Halbgruppe, die möglicherweise weder auf Int noch auf String ein Originalmerkmal ist - und es wäre auch nicht möglich, dieses Merkmal in fester Form hinzuzufügen.) Das Problem ist: Es gibt keinen Weg einen Typ über alle möglichen Merkmale zu verallgemeinern. Dies ist immer Code (Typanmerkungen), der ad-hoc angegeben werden muss, sonst verlieren Sie die Typensicherheit. Implizite Variablen reduzieren hierfür lediglich den Boilerplate-Code.Int's
Sammlung vonInt's
, wie Liste oder Array gegeben. Wenn Sie davon ausgehen, dass die Elemente vergleichbar sind und wieimplicit
oben beschrieben schreiben, können Sie auch die Reihenfolge oben in der Klasse definieren (wie in C ++). In C ++ wird der Namespace hier nicht mit beliebigen Namen wie "cmp" verschmutzt, da Sie den Wert übergeben.Ordering[Int]
ist eine Typklasseninstanz , keine Typklasse .Nach meiner Erfahrung gibt es kein wirklich gutes Beispiel für die Verwendung von impliziten Parametern oder impliziten Konvertierungen.
Der kleine Vorteil der Verwendung von Implikits (ohne dass ein Parameter oder ein Typ explizit geschrieben werden muss) ist im Vergleich zu den von ihnen verursachten Problemen überflüssig.
Ich bin seit 15 Jahren Entwickler und arbeite seit 1,5 Jahren mit Scala.
Ich habe oft Fehler gesehen, die vom Entwickler verursacht wurden, ohne zu wissen, dass Implizite verwendet werden und dass eine bestimmte Funktion tatsächlich einen anderen Typ zurückgibt als den angegebenen. Aufgrund impliziter Konvertierung.
Ich habe auch Aussagen gehört, die besagen, dass Sie Implikits nicht verwenden sollten, wenn Sie sie nicht mögen. Dies ist in der realen Welt nicht praktikabel, da häufig externe Bibliotheken verwendet werden und viele von ihnen implizite Elemente verwenden. Daher verwendet Ihr Code implizite Elemente, und Sie sind sich dessen möglicherweise nicht bewusst. Sie können einen Code schreiben, der entweder Folgendes enthält:
import org.some.common.library.{TypeA, TypeB}
oder:
import org.some.common.library._
Beide Codes werden kompiliert und ausgeführt. Sie führen jedoch nicht immer zu denselben Ergebnissen, da der Import der zweiten Version eine Konvertierung impliziert, die dazu führt, dass sich der Code anders verhält.
Der dadurch verursachte 'Fehler' kann sehr lange nach dem Schreiben des Codes auftreten, falls einige Werte, die von dieser Konvertierung betroffen sind, ursprünglich nicht verwendet wurden.
Sobald Sie auf den Fehler stoßen, ist es keine leichte Aufgabe, die Ursache zu finden. Sie müssen einige gründliche Untersuchungen durchführen.
Obwohl Sie sich wie ein Experte für Scala fühlen, nachdem Sie den Fehler gefunden und durch Ändern einer Importanweisung behoben haben, haben Sie tatsächlich viel wertvolle Zeit verschwendet.
Weitere Gründe, warum ich generell gegen Implikits bin, sind:
Es gibt keine Option zum Kompilieren von Scala ohne Implikationen (falls vorhanden, bitte korrigieren Sie mich), und wenn es eine Option gäbe, hätte keine der gängigen Community-Scala-Bibliotheken kompiliert.
Aus all den oben genannten Gründen denke ich, dass Implizite eine der schlimmsten Praktiken sind, die die Scala-Sprache anwendet.
Scala hat viele großartige Funktionen und viele nicht so großartige.
Bei der Auswahl einer Sprache für ein neues Projekt sind Implikationen einer der Gründe gegen Scala, nicht dafür. Meiner Meinung nach.
quelle
Eine andere gute allgemeine Verwendung impliziter Parameter besteht darin, den Rückgabetyp einer Methode vom Typ einiger an sie übergebener Parameter abhängig zu machen. Ein gutes Beispiel, das von Jens erwähnt wird, ist das Sammlungsframework und Methoden wie
map
, deren vollständige Signatur normalerweise lautet:def map[B, That](f: (A) ⇒ B)(implicit bf: CanBuildFrom[GenSeq[A], B, That]): That
Beachten Sie, dass der Rückgabetyp
That
durch die beste Anpassung bestimmt wirdCanBuildFrom
, die der Compiler finden kann.Ein weiteres Beispiel hierfür finden Sie in dieser Antwort . Dort wird der Rückgabetyp der Methode
Arithmetic.apply
anhand eines bestimmten impliziten Parametertyps (BiConverter
) bestimmt.quelle
List
Gegenleistung und müssen diese dann erneut durchlaufen, um eine zu erhaltenSet
. Durch die Verwendung der impliziten 'Annotation' kann diemap
Methode vermeiden, die falsche Sammlung zu initialisieren und zu füllen.Es ist einfach, denken Sie daran:
z.B
def myFunction(): Int = { implicit val y: Int = 33 implicit val z: Double = 3.3 functionWithImplicit("foo") // calls functionWithImplicit("foo")(y, z) } def functionWithImplicit(foo: String)(implicit x: Int, d: Double) = // blar blar
quelle
Implizite Parameter werden in der Erfassungs-API häufig verwendet. Viele Funktionen erhalten ein implizites CanBuildFrom, das sicherstellt, dass Sie die beste Implementierung der Ergebnissammlung erhalten.
Ohne Implikationen würden Sie entweder die ganze Zeit so etwas passieren, was die normale Nutzung umständlich machen würde. Oder verwenden Sie weniger spezialisierte Sammlungen, was ärgerlich wäre, da Sie dadurch an Leistung / Leistung verlieren würden.
quelle
Ich kommentiere diesen Beitrag etwas spät, aber ich habe in letzter Zeit angefangen, Scala zu lernen. Daniel und andere haben nette Hintergrundinformationen über implizite Schlüsselwörter gegeben. Ich würde mir aus praktischer Sicht zwei Cent für implizite Variablen zur Verfügung stellen.
Scala eignet sich am besten zum Schreiben von Apache Spark-Codes. In Spark haben wir einen Spark-Kontext und höchstwahrscheinlich die Konfigurationsklasse, die möglicherweise die Konfigurationsschlüssel / -werte aus einer Konfigurationsdatei abruft.
Nun, wenn ich eine abstrakte Klasse habe und ein Objekt der Konfiguration deklariere und den Kontext wie folgt funke: -
abstract class myImplicitClass { implicit val config = new myConfigClass() val conf = new SparkConf().setMaster().setAppName() implicit val sc = new SparkContext(conf) def overrideThisMethod(implicit sc: SparkContext, config: Config) : Unit } class MyClass extends myImplicitClass { override def overrideThisMethod(implicit sc: SparkContext, config: Config){ /*I can provide here n number of methods where I can pass the sc and config objects, what are implicit*/ def firstFn(firstParam: Int) (implicit sc: SparkContext, config: Config){ /*I can use "sc" and "config" as I wish: making rdd or getting data from cassandra, for e.g.*/ val myRdd = sc.parallelize(List("abc","123")) } def secondFn(firstParam: Int) (implicit sc: SparkContext, config: Config){ /*following are the ways we can use "sc" and "config" */ val keyspace = config.getString("keyspace") val tableName = config.getString("table") val hostName = config.getString("host") val userName = config.getString("username") val pswd = config.getString("password") implicit val cassandraConnectorObj = CassandraConnector(....) val cassandraRdd = sc.cassandraTable(keyspace, tableName) } } }
Wie wir den obigen Code sehen können, habe ich zwei implizite Objekte in meiner abstrakten Klasse und ich habe diese beiden impliziten Variablen als implizite Parameter für Funktion / Methode / Definition übergeben. Ich denke, dies ist der beste Anwendungsfall, den wir in Bezug auf die Verwendung impliziter Variablen darstellen können.
quelle