Angenommen, ich möchte eine Bibliothek schreiben, die sich mit Vektoren und Matrizen befasst. Ist es möglich, die Dimensionen in die Typen zu backen, sodass Operationen inkompatibler Dimensionen beim Kompilieren einen Fehler erzeugen?
Zum Beispiel möchte ich, dass die Signatur des Punktprodukts so ähnlich ist
dotprod :: Num a, VecDim d => Vector a d -> Vector a d -> a
Dabei d
enthält der Typ einen einzelnen ganzzahligen Wert (der die Dimension dieser Vektoren darstellt).
Ich nehme an, dass dies getan werden kann, indem (von Hand) ein separater Typ für jede Ganzzahl definiert und in einer Typklasse mit dem Namen gruppiert wird VecDim
. Gibt es einen Mechanismus, um solche Typen zu "erzeugen"?
Oder vielleicht eine bessere / einfachere Möglichkeit, dasselbe zu erreichen?
haskell
type-systems
type-safety
mitchus
quelle
quelle
tensor
Bibliothek dies auf sehr elegante Weise mit einer rekursivendata
Definition erreichen: noaxiom.org/tensor-documentation#ordinalsAntworten:
Um die Antwort von @ KarlBielefeldt zu erweitern, finden Sie hier ein vollständiges Beispiel für die Implementierung von Vektoren - Listen mit einer statisch bekannten Anzahl von Elementen - in Haskell. Halte an deinem Hut fest ...
Wie Sie der langen Liste der
LANGUAGE
Direktiven entnehmen können, funktioniert dies nur mit einer neueren Version von GHC.Wir brauchen eine Möglichkeit, Längen innerhalb des Typensystems darzustellen. Per Definition ist eine natürliche Zahl entweder Null (
Z
) oder der Nachfolger einer anderen natürlichen Zahl (S n
). So würde zum Beispiel die Nummer 3 geschrieben werdenS (S (S Z))
.Mit der DataKinds Erweiterung , diese
data
Erklärung stellt eine Art genanntNat
und zwei Typ Bauer genanntS
undZ
- in anderen Worten , wir haben Typ-Ebene natürliche Zahlen. Beachten Sie, dass die TypenS
undZ
keine Mitgliedswerte haben - nur Arten von Typen*
werden von Werten bewohnt.Nun führen wir eine GADT ein, die Vektoren mit bekannter Länge darstellt. Beachten Sie die Art-Signatur:
Vec
Erfordert eine ArtNat
(dh eineZ
oder eineS
Art), um ihre Länge darzustellen.Die Definition von Vektoren ähnelt der von verknüpften Listen mit einigen zusätzlichen Informationen zur Länge auf Typebene. Ein Vektor ist entweder
VNil
, in welchem Fall er eine Länge vonZ
(ero) hat, oder es ist eineVCons
Zelle, die ein Element zu einem anderen Vektor hinzufügt. In diesem Fall ist seine Länge eins länger als der andere Vektor (S n
). Beachten Sie, dass es kein Konstruktorargument vom Typ gibtn
. Es wird nur zur Kompilierungszeit verwendet, um Längen zu verfolgen. Es wird gelöscht, bevor der Compiler Maschinencode generiert.Wir haben einen Vektortyp definiert, der statisches Wissen über seine Länge enthält. Fragen wir die Art einiger
Vec
Sekunden ab, um ein Gefühl dafür zu bekommen, wie sie funktionieren:Das Skalarprodukt verhält sich wie bei einer Liste:
vap
, der 'flink' einen Vektor von Funktionen auf einen Vektor von Argumenten anwendet, istVec
anwendbar<*>
; Ich habe es nicht in eineApplicative
Instanz gestellt, weil es chaotisch wird . Beachten Sie auch, dass ich diefoldr
vom Compiler generierte Instanz von verwendeFoldable
.Probieren wir es aus:
Groß! Beim Versuch,
dot
Vektoren zu erstellen, deren Länge nicht übereinstimmt , wird ein Kompilierungsfehler angezeigt.Hier ist ein Versuch einer Funktion, Vektoren miteinander zu verketten:
Die Länge des Ausgangsvektors wäre die Summe der Längen der beiden Eingangsvektoren. Wir müssen dem Typprüfer beibringen, wie man
Nat
s addiert . Hierfür verwenden wir eine Funktion auf Typebene :Diese
type family
Deklaration führt eine Funktion für Typen ein, die aufgerufen werden. Mit:+:
anderen Worten, es ist ein Rezept für die Typprüfung, um die Summe zweier natürlicher Zahlen zu berechnen. Es wird rekursiv definiert - immer wenn der linke Operand größer alsZ
ero ist, fügen wir dem Ausgang einen hinzu und reduzieren ihn im rekursiven Aufruf um eins. (Es ist eine gute Übung, eine Typfunktion zu schreiben, die zweiNat
Sekunden multipliziert .) Nun können wir+++
kompilieren:So verwenden Sie es:
So weit so einfach. Was ist, wenn wir das Gegenteil von Verkettung machen und einen Vektor in zwei Teile teilen wollen? Die Längen der Ausgabevektoren hängen vom Laufzeitwert der Argumente ab. Wir möchten so etwas schreiben:
aber leider lässt Haskell uns das nicht zu. Wenn der Wert des
n
Arguments im Rückgabetyp angezeigt werden soll (dies wird im Allgemeinen als abhängige Funktion oder Pi-Typ bezeichnet ), sind abhängige Typen mit "vollem Spektrum" erforderlich, wohingegenDataKinds
uns nur hochgestufte Typkonstruktoren zur Verfügung stehen. Anders ausgedrückt, die TypkonstruktorenS
und werdenZ
nicht auf der Wertebene angezeigt. Wir müssen uns mit Singleton-Werten begnügen, um eine bestimmte Darstellung zur Laufzeit zu erhaltenNat
. *Für einen bestimmten Typ
n
(mit ArtNat
) gibt es genau einen TypbegriffNatty n
. Wir können den Singleton-Wert als Laufzeitzeugen für Folgendes verwendenn
: Wenn Sie etwas über a lernen, lernen SieNatty
etwas über dessen Wertn
und umgekehrt.Lass es uns mal ausprobieren:
Im ersten Beispiel haben wir einen Vektor mit drei Elementen an Position 2 erfolgreich aufgeteilt. Dann ist ein Tippfehler aufgetreten, als wir versucht haben, einen Vektor an einer Position nach dem Ende zu teilen. Singletons sind die Standardtechnik, um einen Typ von einem Wert in Haskell abhängig zu machen.
* Die
singletons
Bibliothek enthält einige Template Haskell-Helfer, um Singleton-Werte wieNatty
für Sie zu generieren .Letztes Beispiel. Was ist, wenn Sie die Dimensionalität Ihres Vektors nicht statisch kennen? Was ist zum Beispiel, wenn wir versuchen, einen Vektor aus Laufzeitdaten in Form einer Liste zu erstellen? Der Typ des Vektors hängt von der Länge der Eingabeliste ab. Anders ausgedrückt, wir können keinen
foldr VCons VNil
Vektor erstellen, da sich der Typ des Ausgabevektors mit jeder Iteration der Falte ändert. Wir müssen die Länge des Vektors vor dem Compiler geheim halten.AVec
ist ein existentieller Typ : Die Typvariablen
erscheint nicht im Rückgabetyp desAVec
Datenkonstruktors. Wir können es verwenden eine zu simulieren abhängig Paar :fromList
Sie können nicht sagen , der Länge des Vektors statisch, aber es kann Ihnen etwas zurückgeben können Muster-Match auf lernen die Länge des Vektors - dieNatty n
im ersten Element des Tupels . Wie Conor McBride es in einer verwandten Antwort formuliert : "Sie betrachten eine Sache und lernen dabei etwas über eine andere".Dies ist eine übliche Technik für existenziell quantifizierte Typen. Da Sie mit Daten, für die Sie den Typ nicht kennen, eigentlich nichts
data Something = forall a. Sth a
anfangen können - versuchen Sie, eine Funktion zu schreiben -, werden Existenzdaten häufig mit GADT-Beweisen gebündelt, die es Ihnen ermöglichen, den ursprünglichen Typ durch Durchführung von Pattern-Matching-Tests wiederherzustellen. Andere gebräuchliche Muster für Existentials sind das Packen von Funktionen zum Verarbeiten Ihres Typs (data AWayToGetTo b = forall a. HeresHow a (a -> b)
), was eine gute Möglichkeit ist, erstklassige Module zu erstellen, oder das Einrichten eines Typklassenwörterbuchs (data AnOrd = forall a. Ord a => AnOrd a
), mit dessen Hilfe der Polymorphismus von Subtypen emuliert werden kann.Abhängige Paare sind nützlich, wenn die statischen Eigenschaften von Daten von dynamischen Informationen abhängen, die zur Kompilierungszeit nicht verfügbar sind. Hier ist
filter
für Vektoren:Zu
dot
zweitAVec
müssen wir GHC beweisen, dass ihre Längen gleich sind.Data.Type.Equality
definiert eine GADT, die nur erstellt werden kann, wenn ihre Typargumente identisch sind:Wenn Sie die Musterübereinstimmung
Refl
aktivieren, weiß GHC dasa ~ b
. Es gibt auch einige Funktionen, die Ihnen bei der Arbeit mit diesem Typ helfen: Wir werdengcastWith
zwischen äquivalenten Typen konvertieren undTestEquality
feststellen, ob zweiNatty
s gleich sind.Um die Gleichheit zweier zu testen
Natty
s, sind wir nach Bedarf zu nutzen die Tatsache geht , dass , wenn zwei Zahlen gleich sind, dann ihre Nachfolger auch gleich sind (:~:
ist deckungsgleich überS
):Der Mustervergleich
Refl
auf der linken Seite zeigt dies GHC ann ~ m
. Mit diesem Wissen ist es trivialS n ~ S m
, also lässt uns GHCRefl
sofort ein neues zurückgeben .Jetzt können wir
TestEquality
durch einfache Rekursion eine Instanz von schreiben . Wenn beide Zahlen Null sind, sind sie gleich. Wenn beide Zahlen Vorgänger haben, sind sie gleich, wenn die Vorgänger gleich sind. (Wenn sie nicht gleich sind, kehren Sie einfach zurückNothing
.)Jetzt können wir die Teile zu
dot
einem PaarAVec
unbekannter Länge zusammenfügen.Zuerst wird eine Musterübereinstimmung im
AVec
Konstruktor durchgeführt, um eine Laufzeitdarstellung der Längen der Vektoren zu erhalten. Stellen SietestEquality
nun fest, ob diese Längen gleich sind. Wenn ja, werden wir habenJust Refl
;gcastWith
wird diesen Gleichheitsnachweis verwenden, um sicherzustellen, dass erdot u v
gut typisiert ist, indem er seine impliziten ~ m
Annahme erfüllt .Beachten Sie, dass wir die Listenversion von effektiv neu implementiert haben, da ein Vektor ohne statische Kenntnis seiner Länge im Grunde genommen eine Liste ist
dot :: Num a => [a] -> [a] -> Maybe a
. Der Unterschied besteht darin, dass diese Version in Bezug auf die Vektoren implementiert istdot
. Hier ist der Punkt: vor dem Typ - Checker Sie können anrufendot
, Sie getestet haben müssen , ob die Eingangslisten haben die gleiche Länge verwendentestEquality
. Ichif
neige dazu, -Anweisungen falsch herum zu bekommen , aber nicht in einer von der Schreibmaschine abhängigen Umgebung!Sie können es nicht vermeiden, existenzielle Wrapper an den Rändern Ihres Systems zu verwenden, wenn Sie mit Laufzeitdaten arbeiten, aber Sie können abhängige Typen überall in Ihrem System verwenden und die existenziellen Wrapper an den Rändern belassen, wenn Sie eine Eingabevalidierung durchführen.
Da dies
Nothing
nicht sehr informativ ist, können Sie die Art derdot'
Rückgabe eines Beweises, dass die Längen nicht gleich sind (in Form eines Beweises, dass ihre Differenz nicht 0 ist), im Fehlerfall weiter verfeinern . Dies ist ziemlich ähnlich der Standard-Haskell-Technik, mitEither String a
der möglicherweise eine Fehlermeldung zurückgegeben wird, obwohl ein Beweisbegriff weitaus rechenintensiver ist als eine Zeichenfolge!Damit endet diese Pfeifentour mit einigen der Techniken, die bei der Programmierung mit Haskell-Typen üblich sind. Das Programmieren mit solchen Typen in Haskell ist wirklich cool, aber gleichzeitig sehr umständlich. Das Aufteilen all Ihrer abhängigen Daten in viele Darstellungen, die dasselbe bedeuten -
Nat
Typ,Nat
Art,Natty n
Singleton - ist sehr umständlich, obwohl Code-Generatoren vorhanden sind, die bei der Erstellung des Boilerplates helfen. Derzeit gibt es auch Einschränkungen, was auf die Typstufe heraufgestuft werden kann. Es ist zwar verlockend! Der Verstand wundert sich über die Möglichkeiten - in der Literatur gibt es in Haskell Beispiele für stark typisierteprintf
Datenbankschnittstellen, UI-Layout-Engines ...Wenn Sie mehr darüber lesen möchten, gibt es eine wachsende Zahl von Literatur zu Haskell, die abhängig von der Schreibweise geschrieben wurde, sowohl veröffentlicht als auch auf Websites wie Stack Overflow. Ein guter Ausgangspunkt ist das Hasochismus- Papier - das Papier geht genau dieses Beispiel durch (unter anderem) und erörtert die schmerzhaften Teile ausführlich. Das Singleton- Paper demonstriert die Technik von Singleton-Werten (wie z. B.
Natty
). Weitere Informationen zum abhängigen Tippen im Allgemeinen finden Sie im Agda- Lernprogramm. auch, Idris ist eine Sprache , in der Entwicklung , die (grob) entwickelt , um „Haskell mit abhängigen Arten“ ist.quelle
Das nennt man abhängiges Tippen . Sobald Sie den Namen kennen, können Sie mehr Informationen darüber finden, als Sie sich jemals wünschen könnten. Es gibt auch eine interessante haskell-artige Sprache namens Idris , die sie von Haus aus verwendet. Sein Autor hat ein paar wirklich gute Präsentationen zu dem Thema gemacht, das Sie auf Youtube finden können.
quelle
newtype Vec2 a = V2 (a,a)
,newtype Vec3 a = V3 (a,a,a)
und so weiter, aber das ist nicht, was die Frage stellt.)Pi (x : A). B
eine Funktion, von derA
bisB x
wox
das Argument der Funktion ist. Hier hängt der Rückgabetyp der Funktion von dem als Argument angegebenen Ausdruck ab. All dies kann jedoch gelöscht werden, es ist nur Kompilierungszeit