Warum Scala den unteren Typ ableiten, wenn der Typparameter nicht angegeben ist?

Nov 23 2020

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 Anyzum 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, Anywann Co-Variante / Invariante und Nothingwann 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)

  1. 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]
  2. Als nächstes folgt die Kompilierung myleftvom Typ Entweder [String, Int]

gegebene Kartendefinition def map[B](f: A => B): Either[E, B], (e:Int) => e * 4kann nur geliefert werden, wenn mylefttatsächlich Left[String,Int]oder istEither[String,Int]

Mit anderen Worten, meine Frage ist, wozu der Typ fixiert werden soll, Nothingwenn 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

4 slouc Nov 23 2020 at 17:33
  • Kovarianz:
    Bei gegebenem Typ F[+A]und Beziehung A <: Bgilt Folgendes:F[A] <: F[B]

  • Kontravarianz:
    Bei gegebenem Typ F[-A]und Beziehung A <: Bgilt 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 fist ein gültiger Ersatz für someFunction:

  • Foodist ein Supertyp von Fruit(Kontravarianz der Eingabe)
  • Appleist ein Subtyp von Fruit(Kovarianz der Ausgabe)

Wir können dies in natürlicher Sprache folgendermaßen erklären:

"Die Methode foobenötigt eine Funktion, die a annehmen Fruitund a erzeugen kann Fruit. Dies bedeutet foo, Fruitdass sie eine Funktion hat und eine Funktion benötigt, der sie zugeführt werden kann, und eine Rückgabe erwartet Fruit. Wenn sie eine Funktion erhält Food => Apple, ist alles in Ordnung - sie kann sie trotzdem füttern Fruit( weil die Funktion jedes Essen nimmt) und es empfangen kann Fruit(Ä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 mapmit einer Funktion Int => Whateveroder String => Whateveroder 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 Nothingim Falle einer Kontravarianz repariert . Wenn es den unbekannten Typ Anyim 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 Nothingund Kovariante Any.

1 DmytroMitin Nov 23 2020 at 20:54

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 Tbeispielsweise ein Phantom ist, dh in den Termparametern der Methode nicht verwendet wird, ist sein einziger Eintrag im Ungleichungssystem L <: T <: U, wobei Lund Useine untere bzw. obere Grenze sind. Wenn L != Udiese 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