Warum Scala den unteren Typ ableiten, wenn der Typparameter nicht angegeben ist?
Ich frage mich, ob jemand die Inferenzregel in diesem speziellen Fall unten erklären könnte, und vor allem ist es rational / implizit?
case class E[A, B](a: A) // class E
E(2) // E[Int,Nothing] = E(2)
Beachten Sie, dass ich hätte schreiben können E[Int](2)
. Was mir wichtig ist, ist, warum der zweite Parametertyp abgeleitet wird Nothing
(dh der untere Typ), anstatt Any
zum Beispiel zu sagen ? Warum ist das so und was ist die Rationalität / Implikation?
Nur um einen Kontext zu geben, hängt dies mit der Definition von Entweder und der Funktionsweise für Links und Rechts zusammen. Beide werden nach dem Muster definiert
final case class X[+A, +B](value: A) extends Either[A, B]
Wo Sie es instanziieren, sagen wir als Right[Int](2)
und der abgeleitete Typ ist Right[Nothing, Int]
und durch ErweiterungEither[Nothing, Int]
EDIT1
Hier gibt es Konsistenz, aber ich kann immer noch das Rationale herausfinden. Unten ist die gleiche Definition mit einem kontravarianten Parameter:
case class E[A, -B](a: A)// class E
E(2) // E[Int, Any] = E(2)
Daher haben wir das Gleiche umgekehrt, wenn es sich um eine Gegenvariante handelt, und das macht die Regel des gesamten Verhaltens oder der Folgerung kohärent. Allerdings bin ich mir nicht sicher, warum das so ist ....
Warum nicht die entgegengesetzte Regel, dh ableiten, Any
wann Co-Variante / Invariante und Nothing
wann Gegenvariante?
EDIT2
Angesichts der sinnvollen @ slouc-Antwort muss ich immer noch verstehen, was und warum der Compiler das tut, was er tut. Das folgende Beispiel veranschaulicht meine Verwirrung
val myleft = Left("Error") // Left[String,Nothing] = Left(Error)
myleft map { (e:Int) => e * 4} // Either[String,Int] = Left(Error)
- Zunächst wird der Compiler fix die Art zu etwas , dass „sicher Arbeit“ den Abschluss @slouc wieder zu verwenden (wenn auch mehr Sinn macht im Zusammenhang mit einer Funktion)
Left[String,Nothing]
- Als nächstes folgt die Kompilierung
myleft
vom Typ Entweder [String, Int]
gegebene Kartendefinition def map[B](f: A => B): Either[E, B]
, (e:Int) => e * 4
kann nur geliefert werden, wenn myleft
tatsächlich Left[String,Int]
oder istEither[String,Int]
Mit anderen Worten, meine Frage ist, wozu der Typ fixiert werden soll, Nothing
wenn er später geändert werden soll.
In der Tat wird das Folgende nicht kompiliert
val aleft: Left[String, Nothing] = Left[String, Int]("Error")
type mismatch;
found : scala.util.Left[String,Int]
required: Left[String,Nothing]
val aleft: Left[String, Nothing] = Left[String, Int]("Error")
Warum sollte ich also auf einen Typ schließen, der mich normalerweise daran hindert, etwas anderes über eine Variable dieses Typs zu tun ( aber sicher funktioniert dies als Inferenz ), um diesen Typ letztendlich zu ändern, damit ich etwas mit einer Variablen davon machen kann abgeleiteter Typ.
EDIT3
Edit2 ist ein bisschen missverstanden und alles wird in @slouc Antwort und Kommentaren geklärt.
Antworten
Kovarianz:
Bei gegebenem TypF[+A]
und BeziehungA <: B
gilt Folgendes:F[A] <: F[B]
Kontravarianz:
Bei gegebenem TypF[-A]
und BeziehungA <: B
gilt Folgendes:F[A] >: F[B]
Wenn der Compiler den genauen Typ nicht ableiten kann, löst er bei Kovarianz den niedrigstmöglichen Typ und bei Kontravarianz den höchstmöglichen Typ auf.
Warum?
Dies ist eine sehr wichtige Regel, wenn es um Varianz bei der Subtypisierung geht. Es kann am Beispiel des folgenden Datentyps von Scala gezeigt werden:
trait Function1[Input-, Output+]
Wenn ein Typ in die Funktions- / Methodenparameter eingefügt wird, bedeutet dies im Allgemeinen, dass er sich in der sogenannten "kontravarianten Position" befindet. Wenn es in Funktions- / Methodenrückgabewerten verwendet wird, befindet es sich in der sogenannten "kovarianten Position". Wenn es in beiden ist, ist es unveränderlich.
Angesichts der Regeln vom Anfang dieses Beitrags schließen wir Folgendes:
trait Food
trait Fruit extends Food
trait Apple extends Fruit
def foo(someFunction: Fruit => Fruit) = ???
wir können liefern
val f: Food => Apple = ???
foo(f)
Funktion f
ist ein gültiger Ersatz für someFunction
:
Food
ist ein Supertyp vonFruit
(Kontravarianz der Eingabe)Apple
ist ein Subtyp vonFruit
(Kovarianz der Ausgabe)
Wir können dies in natürlicher Sprache folgendermaßen erklären:
"Die Methode
foo
benötigt eine Funktion, die a annehmenFruit
und a erzeugen kannFruit
. Dies bedeutetfoo
,Fruit
dass sie eine Funktion hat und eine Funktion benötigt, der sie zugeführt werden kann, und eine Rückgabe erwartetFruit
. Wenn sie eine Funktion erhältFood => Apple
, ist alles in Ordnung - sie kann sie trotzdem fütternFruit
( weil die Funktion jedes Essen nimmt) und es empfangen kannFruit
(Äpfel sind Obst, daher wird der Vertrag eingehalten).
Wenn Sie zu Ihrem anfänglichen Dilemma zurückkehren, erklärt dies hoffentlich, warum der Compiler ohne zusätzliche Informationen auf den niedrigstmöglichen Typ für kovariante Typen und den höchstmöglichen Typ für kontravariante Typen zurückgreift. Wenn wir eine Funktion bereitstellen möchten foo
, gibt es eine, von der wir sicher wissen, dass sie funktioniert : Any => Nothing
.
Varianz im Allgemeinen .
Abweichungen in der Scala-Dokumentation .
Artikel über Varianz in Scala (vollständige Offenlegung: Ich habe es geschrieben).
BEARBEITEN:
Ich glaube ich weiß was dich verwirrt.
Wenn Sie a instanziieren Left[String, Nothing]
, können Sie es später map
mit einer Funktion Int => Whatever
oder String => Whatever
oder ausführen Any => Whatever
. Dies ist genau auf die zuvor erläuterte Kontravarianz der Funktionseingabe zurückzuführen. Deshalb arbeiten Sie map
.
"Was bringt es, den Typ auf Nothing zu fixieren, wenn er später geändert werden soll?"
Ich denke, es ist ein bisschen schwierig, sich mit dem Compiler zu beschäftigen, der den unbekannten Typ Nothing
im Falle einer Kontravarianz repariert . Wenn es den unbekannten Typ Any
im Falle einer Kovarianz festlegt, fühlt es sich natürlicher an (es kann "alles" sein). Aufgrund der zuvor erläuterten Dualität von Kovarianz und Kontravarianz gilt dieselbe Argumentation für Kontravariante Nothing
und Kovariante Any
.
Dies ist ein Zitat aus der Vereinheitlichung der Metaprogrammierung zur Kompilierungs- und Laufzeit in Scala von Eugene Burmako
https://infoscience.epfl.ch/record/226166 (S. 95-96)
Während der Typinferenz sammelt der Typechecker Einschränkungen für fehlende Typargumente aus Grenzen von Typparametern, aus Typen von Begriffsargumenten und sogar aus Ergebnissen impliziter Suche (Typinferenz funktioniert mit impliziter Suche zusammen, da Scala ein Analogon funktionaler Abhängigkeiten unterstützt). Man kann diese Einschränkungen als ein System von Ungleichungen betrachten, bei dem unbekannte Typargumente als Typvariablen dargestellt werden und die Reihenfolge durch die Subtypisierungsrelation vorgegeben wird.
Nach dem Sammeln von Einschränkungen startet der Typechecker einen schrittweisen Prozess, bei dem bei jedem Schritt versucht wird, eine bestimmte Transformation auf Ungleichungen anzuwenden, wodurch ein äquivalentes, aber angeblich einfacheres Ungleichungssystem entsteht. Das Ziel der Typinferenz besteht darin, die ursprünglichen Ungleichungen in Gleichungen umzuwandeln, die eine einzigartige Lösung des ursprünglichen Systems darstellen.
Meistens ist die Typinferenz erfolgreich. In diesem Fall werden fehlende Typargumente auf die von der Lösung dargestellten Typen abgeleitet.
Manchmal schlägt die Typinferenz jedoch fehl. Wenn ein Typparameter
T
beispielsweise ein Phantom ist, dh in den Termparametern der Methode nicht verwendet wird, ist sein einziger Eintrag im UngleichungssystemL <: T <: U
, wobeiL
undU
seine untere bzw. obere Grenze sind. WennL != U
diese Ungleichung keine eindeutige Lösung hat, bedeutet dies einen Fehler bei der Typinferenz.Wenn die Typinferenz fehlschlägt, dh wenn sie keine weiteren Transformationsschritte ausführen kann und ihr Arbeitszustand immer noch einige Ungleichungen enthält, bricht der Typechecker die Pattsituation. Es werden alle noch nicht abgeleiteten Typargumente verwendet, dh diejenigen, deren Variablen immer noch durch Ungleichungen dargestellt werden, und sie werden gewaltsam minimiert , dh sie werden ihren unteren Grenzen gleichgesetzt. Dies führt zu einem Ergebnis, bei dem einige Typargumente genau abgeleitet und andere durch scheinbar willkürliche Typen ersetzt werden. Beispielsweise wird auf uneingeschränkte Typparameter geschlossen
Nothing
, was für Scala-Anfänger häufig Verwirrung stiftet.
Weitere Informationen zur Typinferenz finden Sie in Scala:
Hubert Plociniczak Entschlüsseln der lokalen Typinferenz https://infoscience.epfl.ch/record/214757
Guillaume Martres Scala 3, Typinferenz und Sie! https://www.youtube.com/watch?v=lMvOykNQ4zs
Guillaume Martres Dotty und Typen: die Geschichte bisher https://www.youtube.com/watch?v=YIQjfCKDR5A
Folien http://guillaume.martres.me/talks/
Aleksander Boruch-Gruszecki GADTs in Dotty https://www.youtube.com/watch?v=VV9lPg3fNl8