Abgesehen von der syntaktischen Bequemlichkeit bedeutet die Kombination von Singleton-Typen, pfadabhängigen Typen und impliziten Werten, dass Scala die abhängige Typisierung überraschend gut unterstützt, wie ich versucht habe, in formlos zu demonstrieren .
Die eigentliche Unterstützung von Scala für abhängige Typen erfolgt über pfadabhängige Typen . Diese ermöglichen es einem Typ, von einem Auswahlpfad durch ein Objekt- (dh Wert-) Diagramm wie folgt abhängig zu sein.
scala> class Foo { class Bar }
defined class Foo
scala> val foo1 = new Foo
foo1: Foo = Foo@24bc0658
scala> val foo2 = new Foo
foo2: Foo = Foo@6f7f757
scala> implicitly[foo1.Bar =:= foo1.Bar] // OK: equal types
res0: =:=[foo1.Bar,foo1.Bar] = <function1>
scala> implicitly[foo1.Bar =:= foo2.Bar] // Not OK: unequal types
<console>:11: error: Cannot prove that foo1.Bar =:= foo2.Bar.
implicitly[foo1.Bar =:= foo2.Bar]
Meiner Ansicht nach sollte das oben Genannte ausreichen, um die Frage zu beantworten: "Ist Scala eine abhängig getippte Sprache?" positiv: es ist klar, dass wir hier Typen haben, die sich durch die Werte unterscheiden, die ihre Präfixe sind.
Es wird jedoch häufig beanstandet, dass Scala keine "vollständig" abhängige Typsprache ist, da es keine abhängigen Summen- und Produkttypen gibt, wie sie in Agda oder Coq oder Idris als intrinsische Elemente zu finden sind. Ich denke, dies spiegelt in gewissem Maße eine Fixierung auf die Form gegenüber den Grundlagen wider. Dennoch werde ich versuchen zu zeigen, dass Scala diesen anderen Sprachen viel näher ist, als normalerweise anerkannt wird.
Trotz der Terminologie sind abhängige Summentypen (auch als Sigma-Typen bezeichnet) einfach ein Wertepaar, wobei der Typ des zweiten Werts vom ersten Wert abhängt. Dies ist direkt in Scala darstellbar,
scala> trait Sigma {
| val foo: Foo
| val bar: foo.Bar
| }
defined trait Sigma
scala> val sigma = new Sigma {
| val foo = foo1
| val bar = new foo.Bar
| }
sigma: java.lang.Object with Sigma{val bar: this.foo.Bar} = $anon$1@e3fabd8
Tatsächlich ist dies ein entscheidender Teil der Codierung abhängiger Methodentypen, die erforderlich ist, um vor 2.10 aus der 'Bakery of Doom' in Scala zu entkommen (oder früher über die experimentelle Scala-Compileroption -Ydependent-Methodentypen).
Abhängige Produkttypen (auch bekannt als Pi-Typen) sind im Wesentlichen Funktionen von Werten bis zu Typen. Sie sind der Schlüssel zur Darstellung statisch großer Vektoren und der anderen Aushängeschilder für abhängig typisierte Programmiersprachen. Wir können Pi-Typen in Scala mithilfe einer Kombination aus pfadabhängigen Typen, Singleton-Typen und impliziten Parametern codieren. Zuerst definieren wir ein Merkmal, das eine Funktion von einem Wert vom Typ T bis zu einem Typ U darstellen wird.
scala> trait Pi[T] { type U }
defined trait Pi
Wir können dann eine polymorphe Methode definieren, die diesen Typ verwendet,
scala> def depList[T](t: T)(implicit pi: Pi[T]): List[pi.U] = Nil
depList: [T](t: T)(implicit pi: Pi[T])List[pi.U]
(Beachten Sie die Verwendung des pfadabhängigen Typs pi.U
im Ergebnistyp List[pi.U]
). Bei einem Wert vom Typ T gibt diese Funktion eine (n leere) Liste von Werten des Typs zurück, die diesem bestimmten T-Wert entsprechen.
Definieren wir nun einige geeignete Werte und implizite Zeugen für die funktionalen Beziehungen, die wir halten möchten.
scala> object Foo
defined module Foo
scala> object Bar
defined module Bar
scala> implicit val fooInt = new Pi[Foo.type] { type U = Int }
fooInt: java.lang.Object with Pi[Foo.type]{type U = Int} = $anon$1@60681a11
scala> implicit val barString = new Pi[Bar.type] { type U = String }
barString: java.lang.Object with Pi[Bar.type]{type U = String} = $anon$1@187602ae
Und jetzt ist hier unsere Pi-Typ-Funktion in Aktion:
scala> depList(Foo)
res2: List[fooInt.U] = List()
scala> depList(Bar)
res3: List[barString.U] = List()
scala> implicitly[res2.type <:< List[Int]]
res4: <:<[res2.type,List[Int]] = <function1>
scala> implicitly[res2.type <:< List[String]]
<console>:19: error: Cannot prove that res2.type <:< List[String].
implicitly[res2.type <:< List[String]]
^
scala> implicitly[res3.type <:< List[String]]
res6: <:<[res3.type,List[String]] = <function1>
scala> implicitly[res3.type <:< List[Int]]
<console>:19: error: Cannot prove that res3.type <:< List[Int].
implicitly[res3.type <:< List[Int]]
(Beachten Sie, dass wir hier den <:<
Subtyp-Zeugen-Operator von Scala verwenden, anstatt =:=
weil res2.type
und res3.type
Singleton-Typen sind und daher genauer als die Typen, die wir auf der RHS überprüfen).
In der Praxis würden wir in Scala jedoch nicht damit beginnen, Sigma- und Pi-Typen zu codieren und dann von dort fortzufahren, wie wir es in Agda oder Idris tun würden. Stattdessen würden wir pfadabhängige Typen, Singleton-Typen und Implizite direkt verwenden. Sie finden zahlreiche Beispiele dafür, wie sich dies in formlosen Spielen auswirkt : Typen mit Größe , erweiterbare Datensätze , umfassende HL-Listen , Verschrottung Ihrer Heizplatte , generische Reißverschlüsse usw. usw.
Der einzige verbleibende Einwand, den ich sehen kann, ist, dass bei der obigen Codierung von Pi-Typen die Singleton-Typen der abhängigen Werte zum Ausdruck gebracht werden müssen. Leider ist dies in Scala nur für Werte von Referenztypen möglich und nicht für Werte von Nichtreferenztypen (insbesondere z. B. Int). Das ist schade, aber keine intrinsische Schwierigkeit: Scala Typprüfer intern die Singleton Arten von Nicht-Referenzwerten dar, und es gibt ein gewesen Paar von Experimenten direkt ausdrückbar zu machen. In der Praxis können wir das Problem mit einer ziemlich standardmäßigen Codierung der natürlichen Zahlen auf Typebene umgehen .
Auf jeden Fall glaube ich nicht, dass diese geringfügige Domain-Einschränkung als Einwand gegen Scalas Status als abhängig typisierte Sprache verwendet werden kann. Wenn dies der Fall ist, könnte das Gleiche für die abhängige ML gesagt werden (die nur Abhängigkeiten von natürlichen Zahlenwerten zulässt), was eine bizarre Schlussfolgerung wäre.