Nach dieser Frage ist Scalas Typensystem vollständig . Welche Ressourcen stehen zur Verfügung, mit denen ein Neuling die Möglichkeiten der Programmierung auf Typebene nutzen kann?
Hier sind die Ressourcen, die ich bisher gefunden habe:
- Daniel Spiewaks Hohe Zauberei im Land der Scala
- Apocalisps Typ-Level-Programmierung in Scala
- Jespers hList
Diese Ressourcen sind großartig, aber ich habe das Gefühl, dass mir die Grundlagen fehlen, und ich habe keine solide Grundlage, auf der ich aufbauen kann. Wo gibt es zum Beispiel eine Einführung in Typdefinitionen? Welche Operationen kann ich für Typen ausführen?
Gibt es gute Einführungsressourcen?
Antworten:
Überblick
Die Programmierung auf Typebene hat viele Ähnlichkeiten mit der herkömmlichen Programmierung auf Wertebene. Im Gegensatz zur Programmierung auf Wertebene, bei der die Berechnung zur Laufzeit erfolgt, erfolgt die Berechnung bei der Programmierung auf Typebene jedoch zur Kompilierungszeit. Ich werde versuchen, Parallelen zwischen der Programmierung auf Wertebene und der Programmierung auf Typebene zu ziehen.
Paradigmen
Bei der Programmierung auf Typebene gibt es zwei Hauptparadigmen: "objektorientiert" und "funktional". Die meisten von hier verlinkten Beispiele folgen dem objektorientierten Paradigma.
Ein gutes, ziemlich einfaches Beispiel für die Programmierung auf Typebene im objektorientierten Paradigma findet sich in der hier replizierten Apocalisp- Implementierung des Lambda-Kalküls :
Wie im Beispiel zu sehen ist, läuft das objektorientierte Paradigma für die Programmierung auf Typebene wie folgt ab:
trait Lambda
die garantiert , dass die folgenden Arten vorhanden ist :subst
,apply
undeval
.trait App extends Lambda
die mit zwei Typen parametrisiert sind (S
undT
beide müssen Untertypen von seinLambda
),trait Lam extends Lambda
mit einem Typ parametrisiert sind (T
) undtrait X extends Lambda
(der nicht parametrisiert ist).#
(der dem Punktoperator sehr ähnlich ist:.
für Werte). Im MerkmalApp
des Lambda-Kalkül-Beispiels wird der Typeval
wie folgt implementiert :type eval = S#eval#apply[T]
. Dies bedeutet im Wesentlichen, deneval
Typ des Parameters des MerkmalsS
aufzurufen und das Ergebnisapply
mit dem Parameter aufzurufenT
. Beachten Sie, dassS
garantiert eineval
Typ vorhanden ist, da der Parameter angibt, dass es sich um einen Untertyp handeltLambda
. In ähnlicher Weise muss das Ergebnis voneval
einenapply
Typ haben, da es als Subtyp von angegeben wirdLambda
, wie im abstrakten Merkmal angegebenLambda
.Das funktionale Paradigma besteht darin, viele parametrisierte Typkonstruktoren zu definieren, die nicht in Merkmalen zusammengefasst sind.
Vergleich zwischen Programmierung auf Wertebene und Programmierung auf Typebene
abstract class C { val x }
trait C { type X }
C.x
(Verweis auf Feldwert / Funktion x in Objekt C)C#x
(Referenzieren des Feldtyps x in Merkmal C)def f(x:X) : Y
type f[x <: X] <: Y
(Dies wird als "Typ-Konstruktor" bezeichnet und tritt normalerweise im abstrakten Merkmal auf.)def f(x:X) : Y = x
type f[x <: X] = x
a:A == b:B
implicitly[A =:= B]
assert(a == b)
implicitly[A =:= B]
A <:< B
, wird nur kompiliert, wennA
es sich um einen Subtyp von handeltB
A =:= B
, wird nur kompiliert, wennA
es sich um einen Subtyp vonB
undB
um einen Subtyp von handeltA
A <%< B
, ("sichtbar als") wird nur kompiliert, wennA
sichtbar alsB
(dh es gibt eine implizite Konvertierung vonA
in einen Subtyp vonB
)Konvertieren zwischen Typen und Werten
In vielen Beispielen sind Typen, die über Merkmale definiert werden, häufig sowohl abstrakt als auch versiegelt und können daher weder direkt noch über eine anonyme Unterklasse instanziiert werden. Daher ist es üblich,
null
einen Platzhalterwert zu verwenden, wenn eine Berechnung auf Wertebene mit einem bestimmten Interesse durchgeführt wird:val x:A = null
, woA
ist der Typ, den Sie interessierenAufgrund der Typlöschung sehen parametrisierte Typen alle gleich aus. Darüber hinaus sind (wie oben erwähnt) die Werte, mit denen Sie arbeiten, in der Regel alle
null
, sodass eine Konditionierung des Objekttyps (z. B. über eine Übereinstimmungsanweisung) unwirksam ist.Der Trick besteht darin, implizite Funktionen und Werte zu verwenden. Der Basisfall ist normalerweise ein impliziter Wert und der rekursive Fall ist normalerweise eine implizite Funktion. In der Tat werden bei der Programmierung auf Typebene Implikationen stark genutzt.
Betrachten Sie dieses Beispiel ( entnommen aus metascala und apocalisp ):
Hier haben Sie eine Peano-Codierung der natürlichen Zahlen. Das heißt, Sie haben einen Typ für jede nicht negative Ganzzahl: einen speziellen Typ für 0, nämlich
_0
; und jede ganze Zahl größer als Null hat einen Typ der FormSucc[A]
, wobeiA
der Typ eine kleinere ganze Zahl darstellt. Der Typ, der 2 darstellt, wäre beispielsweise:Succ[Succ[_0]]
(Nachfolger wird zweimal auf den Typ angewendet, der Null darstellt).Wir können verschiedene natürliche Zahlen für eine bequemere Referenz aliasisieren. Beispiel:
(Dies ähnelt der Definition von a
val
als Ergebnis einer Funktion.)Nehmen wir nun an, wir möchten eine Funktion auf Wertebene definieren,
def toInt[T <: Nat](v : T)
die einen Argumentwert annimmt, der einer Ganzzahlv
entsprichtNat
und diese zurückgibt, die die natürliche Zahl darstellt, die inv
's Typ codiert ist . Wenn wir beispielsweise den Wertval x:_3 = null
(null
vom TypSucc[Succ[Succ[_0]]]
) haben, möchten wirtoInt(x)
zurückkehren3
.Zur Implementierung verwenden
toInt
wir die folgende Klasse:Wie wir unten sehen werden, wird es
TypeToValue
für jedeNat
von_0
bis zu (z._3
, und jedes wird die Wertdarstellung des entsprechenden TypsTypeToValue[_0, Int]
speichern (dh wird den Wert speichern0
,TypeToValue[Succ[_0], Int]
wird den Wert speichern1
usw.). Hinweis:TypeToValue
wird durch zwei Typen parametrisiert:T
undVT
.T
entspricht dem Typ, dem wir Werte zuweisen möchten (in unserem BeispielNat
), undVT
entspricht dem Werttyp, den wir ihm zuweisen (in unserem BeispielInt
).Nun machen wir die folgenden zwei impliziten Definitionen:
Und wir implementieren
toInt
wie folgt:Um zu verstehen, wie es
toInt
funktioniert, betrachten wir einige Eingaben:Wenn wir anrufen
toInt(z)
, sucht der Compiler nach einem impliziten Argumentttv
vom TypTypeToValue[_0, Int]
(daz
es vom Typ ist_0
). Es findet das Objekt_0ToInt
, ruft diegetValue
Methode dieses Objekts auf und kehrt zurück0
. Der wichtige Punkt ist, dass wir dem Programm nicht angegeben haben, welches Objekt verwendet werden soll. Der Compiler hat es implizit gefunden.Nun wollen wir überlegen
toInt(y)
. Diesmal sucht der Compiler nach einem impliziten Argumentttv
vom TypTypeToValue[Succ[_0], Int]
(day
es vom Typ istSucc[_0]
). Es findet die FunktionsuccToInt
, die ein Objekt des entsprechenden Typs (TypeToValue[Succ[_0], Int]
) zurückgeben und auswerten kann. Diese Funktion selbst verwendet ein implizites Argument (v
) vom TypTypeToValue[_0, Int]
(d. H. A.TypeToValue
wenn der erste Typparameter eins weniger hatSucc[_]
). Der Compiler liefert_0ToInt
(wie in dertoInt(z)
obigen Auswertung ) und erstelltsuccToInt
ein neuesTypeToValue
Objekt mit Wert1
. Auch hier ist zu beachten, dass der Compiler alle diese Werte implizit bereitstellt, da wir keinen expliziten Zugriff darauf haben.Überprüfen Sie Ihre Arbeit
Es gibt verschiedene Möglichkeiten, um zu überprüfen, ob Ihre Berechnungen auf Typebene das tun, was Sie erwarten. Hier sind einige Ansätze. Machen Sie zwei Arten
A
undB
, die Sie überprüfen möchten, sind gleich. Überprüfen Sie dann Folgendes:Equal[A, B]
Equal[T1 >: T2 <: T2, T2]
( entnommen aus apocolisp )implicitly[A =:= B]
Alternativ können Sie den Typ in einen Wert konvertieren (wie oben gezeigt) und eine Laufzeitprüfung der Werte durchführen. ZB
assert(toInt(a) == toInt(b))
woa
ist vom TypA
undb
ist vom TypB
.Zusätzliche Ressourcen
Den vollständigen Satz verfügbarer Konstrukte finden Sie im Abschnitt Typen des Scala-Referenzhandbuchs (pdf) .
Adriaan Moors hat mehrere wissenschaftliche Artikel über Typkonstruktoren und verwandte Themen mit Beispielen aus Scala:
Apocalisp ist ein Blog mit vielen Beispielen für die Programmierung auf Typebene in Scala.
ScalaZ ist ein sehr aktives Projekt, das Funktionen bereitstellt, die die Scala-API mithilfe verschiedener Programmierfunktionen auf erweitern. Es ist ein sehr interessantes Projekt, das eine große Anhängerschaft hat.
MetaScala ist eine Bibliothek auf Typebene für Scala, einschließlich Metatypen für natürliche Zahlen, Boolesche Werte, Einheiten, HList usw. Es ist ein Projekt von Jesper Nordenberg (sein Blog) .
Der Michid (Blog) hat einige großartige Beispiele für die Programmierung auf Typebene in Scala (aus anderen Antworten):
Debasish Ghosh (Blog) hat auch einige relevante Beiträge:
(Ich habe einige Nachforschungen zu diesem Thema angestellt und hier ist, was ich gelernt habe. Ich bin noch neu darin. Bitte weisen Sie auf Ungenauigkeiten in dieser Antwort hin.)
quelle
Neben den anderen Links hier gibt es auch meine Blog-Beiträge zur Meta-Programmierung auf Typebene in Scala:
quelle
Wie auf Twitter vorgeschlagen: Shapeless: Eine Untersuchung der generischen / polytypischen Programmierung in Scala von Miles Sabin.
quelle
quelle
Scalaz hat Quellcode, ein Wiki und Beispiele.
quelle