scala - Jeder vs Unterstrich in Generika

76

Was ist der Unterschied zwischen den folgenden generischen Definitionen in Scala:

class Foo[T <: List[_]]

und

class Bar[T <: List[Any]]

Mein Bauch sagt mir, dass sie ungefähr gleich sind, aber dass letzteres expliziter ist. Ich finde Fälle, in denen das erstere kompiliert, das letztere jedoch nicht, aber den genauen Unterschied nicht erkennen kann.

Vielen Dank!

Bearbeiten:

Kann ich noch einen in die Mischung werfen?

class Baz[T <: List[_ <: Any]]
Sean Connolly
quelle
7
Wenn Sie einen Typ so einschränken, dass er <: Anyniemals etwas ändert. Jeder Typ in Scala ist <: Any.
Randall Schulz

Antworten:

101

OK, ich dachte mir, ich sollte meine Meinung dazu haben, anstatt nur Kommentare zu posten. Entschuldigung, dies wird lange dauern, wenn Sie die TL; DR bis zum Ende überspringen möchten.

Wie Randall Schulz sagte, ist hier _eine Abkürzung für einen existenziellen Typ. Nämlich,

class Foo[T <: List[_]]

ist eine Abkürzung für

class Foo[T <: List[Z] forSome { type Z }]

Beachten Sie, dass im Gegensatz zu Randall Shulz 'Antwort (vollständige Offenlegung: Ich habe es auch in einer früheren Version dieses Beitrags falsch verstanden, danke an Jesper Nordenberg für den Hinweis) dies nicht dasselbe ist wie:

class Foo[T <: List[Z]] forSome { type Z }

noch ist es das gleiche wie:

class Foo[T <: List[Z forSome { type Z }]]

Achtung, es ist leicht, etwas falsch zu machen (wie mein früherer Fehler zeigt): Der Autor des Artikels, auf den Randall Shulz 'Antwort verweist, hat es selbst falsch verstanden (siehe Kommentare) und später behoben. Mein Hauptproblem bei diesem Artikel ist, dass in dem gezeigten Beispiel die Verwendung von Existentials uns vor einem Tippproblem bewahren soll, dies jedoch nicht. Überprüfen Sie den Code und versuchen Sie, compileAndRun(helloWorldVM("Test"))oder zu kompilieren compileAndRun(intVM(42)). Ja, kompiliert nicht. Einfach compileAndRungenerisch machenA würde den Code Kompilierung machen, und es wäre viel einfacher sein. Kurz gesagt, das ist wahrscheinlich nicht der beste Artikel, um etwas über Existentiale und deren Zweck zu erfahren (der Autor selbst bestätigt in einem Kommentar, dass der Artikel "aufgeräumt werden muss").

Daher würde ich eher empfehlen, diesen Artikel zu lesen: http://www.artima.com/scalazine/articles/scalas_type_system.html , insbesondere die Abschnitte "Existenzielle Typen" und "Varianz in Java und Scala".

Der wichtige Punkt, den Sie aus diesem Artikel erhalten sollten, ist, dass Existenziale nützlich sind (abgesehen davon, dass sie mit generischen Java-Klassen umgehen können), wenn es um nicht kovariante Typen geht. Hier ist ein Beispiel.

case class Greets[T]( private val name: T ) {
  def hello() { println("Hello " + name) }
  def getName: T = name
}

Diese Klasse ist generisch (beachten Sie auch, dass sie unveränderlich ist), aber wir können sehen, dass helloder Typparameter (im Gegensatz zu getName) wirklich nicht verwendet wird. Wenn ich also eine Instanz von bekomme, Greetssollte ich sie immer aufrufen können, wie auch immer Tist. Wenn ich eine Methode definieren möchte, die eine GreetsInstanz nimmt und nur ihre helloMethode aufruft , könnte ich Folgendes versuchen:

def sayHi1( g: Greets[T] ) { g.hello() } // Does not compile

Sicher genug, dies kompiliert nicht, wie Thier aus dem Nichts kommt.

OK, dann machen wir die Methode generisch:

def sayHi2[T]( g: Greets[T] ) { g.hello() }
sayHi2( Greets("John"))
sayHi2( Greets('Jack))

Großartig, das funktioniert. Wir könnten hier auch Existentials verwenden:

def sayHi3( g: Greets[_] ) { g.hello() }
sayHi3( Greets("John"))
sayHi3( Greets('Jack))

Funktioniert auch. Alles in allem gibt es hier also keinen wirklichen Vorteil, wenn ein existenzieller (wie in sayHi3) gegenüber einem Typparameter (wie in sayHi2) verwendet wird.

Dies ändert Greetssich jedoch, wenn es selbst als Typparameter für eine andere generische Klasse angezeigt wird. Angenommen, wir möchten mehrere Instanzen von Greets(mit unterschiedlichen T) in einer Liste speichern . Lass es uns versuchen:

val greets1: Greets[String] = Greets("John")
val greets2: Greets[Symbol] = Greets('Jack)
val greetsList1: List[Greets[Any]] = List( greets1, greets2 ) // Does not compile

Die letzte Zeile wird nicht kompiliert, da sie Greetsunveränderlich ist, sodass a Greets[String]und Greets[Symbol]nicht als Greets[Any]obwohl behandelt werden kann Stringund Symbolbeide erweitert werden Any.

OK, versuchen wir es mit einem Existenziellen unter Verwendung der Kurzschreibweise _:

val greetsList2: List[Greets[_]] = List( greets1, greets2 ) // Compiles fine, yeah

Dies lässt sich gut kompilieren, und Sie können wie erwartet Folgendes tun:

greetsSet foreach (_.hello)

Denken Sie jetzt daran, dass der Grund, warum wir überhaupt ein Problem mit der Typprüfung hatten, darin bestand, dass Greetses unveränderlich ist. Wenn es in eine kovariante Klasse ( class Greets[+T]) umgewandelt worden wäre, hätte alles sofort geklappt und wir hätten niemals Existenziale benötigt.


Zusammenfassend lässt sich sagen, dass Existentials nützlich sind, um mit generischen invarianten Klassen umzugehen. Wenn die generische Klasse jedoch nicht als Typparameter für eine andere generische Klasse angezeigt werden muss, benötigen Sie wahrscheinlich keine Existentials und fügen einfach einen Typparameter hinzu zu Ihrer Methode wird funktionieren

Kommen Sie nun (endlich, ich weiß!) Auf Ihre spezifische Frage zurück

class Foo[T <: List[_]]

Weil Listes kovariant ist, ist dies in jeder Hinsicht dasselbe wie nur zu sagen:

class Foo[T <: List[Any]]

In diesem Fall ist die Verwendung einer der beiden Notationen also nur eine Frage des Stils.

Wenn Sie jedoch ersetzen Listmit Set, Dinge zu ändern:

class Foo[T <: Set[_]]

Setist invariant und somit befinden wir uns in der gleichen Situation wie bei der GreetsKlasse aus meinem Beispiel. Somit unterscheidet sich das Obige wirklich sehr von

class Foo[T <: Set[Any]]
Régis Jean-Gilles
quelle
1
Nein, class Foo[T <: List[_]]ist eine Abkürzung für class Foo[T <: List[Z] forSome { type Z }]. Der gleiche Weg List[Greets[_]]ist eine Abkürzung für List[Greets[Z] forSome { type Z }](und nicht List[Greets[Z]] forSome { type Z }).
Jesper Nordenberg
Doh, dumm mich! Danke, ich habe das behoben. Komischerweise war mein erster Versuch , den Artikel von David R MacIver ( drmaciver.com/2008/03/existential-types-in-scala ) zu lesen , der genau über die existenzielle Abkürzung spricht und vor ihrem unintuitiven Desugaring warnt. Die Sache ist, er scheint es selbst falsch zu haben. Tatsächlich hat sich die Art und Weise, wie das Desugaring durchgeführt wird, kurz nach seinem Artikel geändert (in Scala 2.7.1 siehe Änderungsprotokoll unter scala-lang.org/node/43#2.8.0 ). Ich denke, diese Änderung trägt zur Verwirrung bei.
Régis Jean-Gilles
Nun, es ist einfach, die Bedeutung der existenziellen Typsyntax zu verwechseln. Zumindest das derzeitige Desugaring ist meiner Meinung nach das logischste.
Jesper Nordenberg
Wo geht der letzte ]rein class Foo[T <: List[Z forSome { type Z }]?
Joel
Ja, ]am Ende fehlte etwas . Das hätte sein sollen class Foo[T <: List[Z forSome { type Z }]]. Jetzt behoben, danke.
Régis Jean-Gilles
7

Ersteres ist eine Abkürzung für einen existenziellen Typ, wenn der Code nicht wissen muss, um welchen Typ es sich handelt, oder ihn einschränken muss:

class Foo[T <: List[Z forSome { type Z }]]

Dieses Formular besagt, dass der Elementtyp von Listeher unbekannt ist class Fooals Ihr zweites Formular, das speziell angibt, dass der Elementtyp des ListElements lautet Any.

Lesen Sie diesen kurzen erklärenden Blog-Artikel über existentielle Typen in Scala ( BEARBEITEN : Dieser Link ist jetzt tot, ein Schnappschuss ist auf archive.org verfügbar. )

Randall Schulz
quelle
3
Während Sie erklären, was ein Existential ist, denke ich, dass die Frage nicht ist, was Existenzial im Allgemeinen ist, aber gibt es einen tatsächlich beobachtbaren Unterschied zwischen T[_](was ist ein besonderer Fall von existenziellem Gebrauch) und T[Any]?
Régis Jean-Gilles
Natürlich gibt es. Der Blog, auf den ich mich bezog, hat eine schöne Illustration davon.
Randall Schulz
3
Ich möchte in Ihrer Antwort lieber erwähnen, wie wichtig Kovarianz für den Unterschied zwischen T[_]und ist T[Any], da dies der Kern der Frage ist, warum überhaupt T[_] über verwendet wird T[Any]. Darüber hinaus erwähnte Sean Connolly in seiner Frage ausdrücklich List[_]. Angesichts der Tatsache, dass dies Listtatsächlich kovariant ist, kann man sich fragen, ob es in diesem Fall wirklich einen Unterschied zwischen List[_]und gibt List[Any].
Régis Jean-Gilles
2
Wenn T kovariant ist und sein Typparameter Anyals Obergrenze gilt, scheint es keinen Unterschied zwischen T[_]und zu geben T[Any]. Wenn Sie jedoch haben T[+A <: B]die Compiler seltsam genug nicht ableiten , T[_ <: B]aus T[_].
Jesper Nordenberg
@Jesper Nordenberg: Tatsächlich hat diese Schlussfolgerung in Scala vor 2.8 funktioniert, sie wurde in Scala 2.8 "gebrochen". Tatsächlich war es absichtlich so, weil dies anscheinend schlecht mit dem Rest des Typsystems zusammenspielte und inkonsistent war (ich kann mich jedoch nicht an die Details erinnern).
Régis Jean-Gilles