Arbeitsgruppe Programmiersprachen und Übersetzerkonstruktion Institut für Informatik Christian-Albrechts-Universität zu Kiel Seminararbeit Verbesserung von Haskell-Typen mit SMT Mike Tallarek WS 2015/2016 Inhaltsverzeichnis 1. Einführung 1 2. Grundlagen 2 2.1. 2.2. 2.3. 2.4. 2.5. Datentypen, Kinds und GADTs . Typfunktionen . . . . . . . . . . Datentyp-Promotion . . . . . . . Natürliche Zahlen auf Typ-Ebene SMT-Solver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3. Verbesserung von Haskell-Typen mit SMT 3.1. 3.2. 3.3. 3.4. Motivation . . . . . . . . . . . . . . . . GHCs Constraint Solver . . . . . . . . Type-Checker-Plugins . . . . . . . . . Integration eines SMT-Solvers in GHC 3.4.1. Input und Output . . . . . . . 3.4.2. Die Sprache von Constraints . . 3.4.3. Auswahl von Constraints . . . 3.4.4. Kommunikation mit dem Solver 3.4.5. Konsistenz von Constraints . . 3.4.6. Verbesserung von Constraints . 3.4.7. Lösen von Constraints . . . . . 3.5. Erweiterung durch weitere Theorien . 3.6. Beispiel . . . . . . . . . . . . . . . . . 3.7. Fazit . . . . . . . . . . . . . . . . . . . 2 4 7 8 9 11 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 12 14 14 14 15 16 16 17 17 18 18 19 24 A. Installationsanleitung 26 B. Beispiel 27 ii 1. Einführung Diese Seminararbeit befasst sich mit einer Erweiterung für GHC, die dessen type-checker um die Nutzung von SMT-Solvern ergänzt. Der type-checker wird durch einen Algorithmus erweitert, der SMT-Solver integriert, um komplexe Aussagen über Typ-Funktionen auf natürlichen Zahlen machen zu können. Die Erweiterung bildet zudem ein Grundgerüst, um SMT-Solver im allgemeinen zum type-checking zu nutzen, weswegen durchaus auch andere Theorien als die natürlichen Zahlen eingebunden werden können. Diese Ausarbeitung basiert auf einem Paper von Iavor S. Diatchki [Dia15], in dem die Erweiterung vorgestellt wird. 1 2. Grundlagen Zunächst müssen einige Grundlagen erläutert werden, die für das Verständnis dieser Arbeit notwendig sind. Es wird eine Menge von Haskell Erweiterungen und Modulen genutzt, die im Folgenden vorgestellt werden. Des Weiteren werden kurz die Kernfunktionalitäten von SMT-Solvern dargelegt. 2.1. Datentypen, Kinds und GADTs In Haskell gibt es Werte und Datentypen. Jeder Wert ist von einem bestimmten Datentyp. So hat der Wert "Hallo" den Datentyp String und der Wert 3 den Datentyp Num a => a. Auch Funktionen werden wie alle anderen Werte behandelt. Die Funktion \f x -> map f x hat den Typ (a -> b) -> [a] -> [b]. Datentypen werden mit Typkonstruktoren erstellt. Typkonstruktoren nehmen eine bestimmte Anzahl an Datentypen als Parameter und liefern einen Datentyp als Ergebnis. Bekannte Typkonstruktoren sind der Listen-Typkonstruktor [], der einen Typ a nimmt und [a] liefert und der Either-Typkonstruktor, der zwei Typen a und b nimmt und Either a b liefert. Datentypen wie Int oder Bool werden als null-stellige Typkonstruktoren betrachtet. Ebenso wie Werte haben auch Typkonstruktoren einen Typ. Die Typen von Typkonstruktoren werden Kinds genannt. Normale Typen wie String oder Int haben den Kind *. Typkonstruktoren, die nicht null-stellig sind, haben den Kind K -> K', wobei K und K' auch Kinds sind. Beispiele: Int :: ∗ Maybe :: ∗ → ∗ Maybe Bool :: ∗ a → a :: ∗ [] :: ∗ → ∗ (→) :: ∗ → ∗ → ∗ Algebraic Data Types (ADTs) sind Datentypen, die in Haskell mit dem data-Schlüsselwort deklariert werden. data Example a = This a | That a Hier ist Example ein Typkonstruktor. Der Kind von Example ist * -> *. This und That sind Datenkonstruktoren. Die Typen von This und That sehen wie folgt aus: This :: a → Example a That :: a → Example a 2 2. Grundlagen Sie erhalten also einen Wert vom Typ a und erzeugen einen Wert vom Typ Example a. Es ist mit ADTs nicht möglich festzulegen, dass ein Datenkonstruktor einen Wert von einem bestimmten Datentyp erzeugt, wenn der ADT polymorph ist. Es ist bei diesem Beispiel nicht möglich, dass This den Typ String -> Example String hat. Generalized Algebraic Data Types (GADTs) ermöglichen dieses Verhalten. GADTs sind eine Haskell-Spracherweiterung, die durch folgendes Pragma in eine Haskell-Datei eingebunden werden kann: {-# LANGUAGE GADTs #-} Nun kann ein Datentyp beispielsweise auch wie folgt deniert werden: data Example a where This :: String → Example String That :: Bool → Example Int Those :: b → Bool → Example Int Es werden in einem where-Konstrukt die Typen der Datenkonstruktoren angegeben. Der Unterschied zu ADTs ist dabei, dass der Typ des entstehenden Werts angegeben werden kann. This nimmt einen Wert vom Typ String und liefert einen Wert vom Typ Example String. Es ist aber nicht notwendig, dass die Typen auf diese Weise übereinstimmen, wie es bei That und Those zu sehen ist. That liefert Werte vom Typ Example Int, obwohl es einen Wert vom Typ Bool entgegen nimmt. Those nimmt einen Wert eines beliebigen Typs und einen Wert vom Typ Bool und liefert einen Wert vom Typ Example Int. Obwohl der Datentyp an sich polymorph ist, sind die Datenkonstruktoren es im Ergebnis nicht mehr. Beispiele: :t (This "Hallo") (This "Hallo") :: Example String (:t (That True) (That True) :: Example Int True) :: Example Int :t (Those "Hi" True) (Those "Hi" True) :: Example Bool :t (Those 3 True) (Those 3 True) :: Example Bool Ist es wünschenswert, dass der Kind von Example explizit hingeschrieben wird, ist das durch die Haskell-Erweiterung KindSignatures möglich. {-# LANGUAGE KindSignatures #-} Nun kann Example auch wie folgt deniert werden: data Example :: ∗ → ∗ where This :: String → Example String That :: Bool → Example Int Those :: b → Bool → Example Int GADTs sind eine erste Möglichkeit, ein wenig mehr Macht über das Typsystem in Haskell zu erhalten. Eine weitere Möglichkeit sind Typfunktionen. 3 2. Grundlagen 2.2. Typfunktionen Die Erweiterung TypeFamilies erweitert Haskell um Typfunktionen: {-# LANGUAGE TypeFamilies #-} Typfunktionen werden wie folgt deniert: type family Example a1 a2 type instance Example t1 t2 = t3 type instance Example t4 t5 = t6 Es wird zunächst eine Typfamilie deklariert, in der lediglich angegeben muss, wieviele Parameter die Typfunktion nehmen muss. Hierbei hat Example zum Beispiel den Kind * -> * -> *. Example erhält also zwei Typen als Parameter und liefert einen Typ. Es können dann verschiedene Instanzen von Example für verschiedene Kombinationen von Typen erzeugt werden. Wie Typfunktionen sinnvoll genutzt werden können, kann am besten an einem richtigen Beispiel erläutert werden. Ein Vektor ist ein Datentyp, der genau wie eine Liste Werte von einem Typ hintereinander speichert. Zusätzlich enthält der Datentyp noch die Anzahl an gespeicherten Werten. Um diesen Typ darzustellen braucht man also zunächst die natürlichen Zahlen auf der Typ-Ebene: data Z data S a Z steht für die Null und S ist zum Inkrementieren da. So kann zum Beispiel die 4 als S (S (S (S Z))) dargestellt werden. Damit diese Zahlen später auch auf Wert-Ebene zur Verfügung stehen, wird ein zusätzlicher Datentyp deniert, der unsere Zahlen auf Typ-Ebene nutzt. data Nat :: ∗ → ∗ where Z :: Nat Z S :: Nat n → Nat (S n) Nun können Werte n vom Typ Nat n erzeugt werden. Für jeden Typen Z bzw. S a gibt es also genau einen Wert vom Typ Nat Z bzw Nat (S a). Beispiele: :t Z Z :: Nat Z :t S (S (S (S Z))) S (S (S (S Z))) :: Nat (S (S (S (S Z)))) Nat ist also ein sogenannter Singleton -Datentyp. Es wird immer dann von einem SingletonDatentyp gesprochen, wenn es genau einen Wert von diesem Typ gibt. Nun stehen die natürlichen Zahlen auf Typ-Ebene und Wert-Ebene zur Verfügung und der Vektor kann als GADT deniert werden: data Vec a len where Nil :: Vec a Z Cons :: a → Vec a len → Vec a (S len) Ein Wert vom Typ Vec (von nun an werden solche Werte Vektor genannt) ist entweder Nil, enthält also keinen Wert und hat im Typ die Länge Z gegeben, oder ein zusammengesetzter Vektor durch den Cons-Konstruktor. Der Cons-Konstruktor erhält einen Wert 4 2. Grundlagen vom Typ a und einen Vektor. Heraus kommt ein Vektor, bei dem im Typ die Anzahl an Werten inkrementiert wird. Vec könnte ohne die GADT-Erweiterung so nicht deniert werden. Es ist hier essentiell, dass die Typen der Werte, die die Datenkonstruktoren erzeugen, genau angegeben werden können. Beispiele: :t Nil Nil :: Vec e Z :t Cons 3 (Cons 4 (Cons 5 Nil)) Cons 3 (Cons 4 (Cons 5 Nil)) :: Num e => Vec e (S (S (S Z))) Um korrekt mit den natürlichen Zahlen auf Typ-Ebene umgehen zu können, ist es sinnvoll Typ-Funktionen zu denieren. Soll zum Beispiel eine Funktion geschrieben werden, die zwei Vektoren aneinander hängt und der entstehende Vektor soll die korrekte Länge im Typ haben, muss eine Typ-Funktion zum Addieren natürlicher Zahlen deniert werden: type family Add a b type instance Add Z x = x type instance Add (S x) y = S (Add x y) Es gibt hier zwei Instanzen, die implementiert werden müssen. Im ersten Fall ist der erste Typ eine Null, also ist der zweite Typ das Ergebnis. Im zweiten Fall ist der erste Typ eine Inkrementierung, also addiert man den inkrementierten Wert mit dem zweiten Wert und inkrementiert das Ergebnis. Nun kann die gewünschte Funktion implementiert werden: vappend :: Vec a n → Vec a m → Vec a (Add n m) vappend Nil ys = ys vappend (Cons x xs) ys = (Cons x (vappend xs ys)) Hätte man einen Fehler in der Implementierung der Funktion gemacht, sodass die Länge des Vektors nicht stimmt, dann würde Haskell beim Kompilieren einen Typfehler melden. Siehe beispielsweise diese fehlerhafte Implementierung: vappend2 :: Vec a n → Vec a m → Vec a (Add n m) vappend2 Nil ys = ys vappend2 (Cons x xs) ys = (vappend xs ys) Haskell wirft bei dieser Implementierung folgenden Fehler: Could not deduce (Add len m ~ S (Add len m)) Die Längen im Typ passen nicht zusammen. Normalerweise würde solch ein Fehler erst zur Laufzeit auallen. Eine weitere Verwendung von Typfunktionen ist die Nutzung in Constraints. Ein Beispiel dafür sind Funktionen, die eine bestimmte Länge von Vektoren voraussetzen. Dazu wird folgende Typfunktion implementiert: type type type type family GreaterEqualThan a instance GreaterEqualThan instance GreaterEqualThan instance GreaterEqualThan b Z (S x) = False x Z = True (S x) (S y) = GreaterEqualThan a b Hier fällt auf, dass True und False eigentlich keine Typen sind. Diese wurden für diese Typfunktion deniert: 5 2. Grundlagen data True data False Mit Hilfe dieser Typfunktion können Funktionen mit Constraints deniert werden, die ein bestimmtes Verhältnis von natürlichen Zahlen voraussetzen. Als Beispiel wird eine Funktion implementiert, die das n-te Element eines Vektors ausgibt: vNth :: (GreaterEqualThan n m ~ True, GreaterEqualThan m (S Z) ~ True) => Vec a n → Nat m → a vNth (Cons x _) (S Z) = x vNth (Cons _ xs) (S (S m)) = vNth xs (S m) Hier werden die am Anfang denierten natürlichen Zahlen auf Wert-Ebene verwendet. Welcher Wert vom Vektor ausgegeben werden soll, wird als Parameter vom Typ Nat m angegeben. Auf diese Weise kann die Gröÿe des Vektors mit diesem Parameter auf TypEbene verglichen werden. Diese Funktion verlangt, dass die Parameter zwei Constraints erfüllen. Zum einen muss n gröÿer gleich m sein und zum anderen muss m gröÿer gleich Eins sein. Durch diese Constraints werden Fehler schon beim Kompilieren erkannt, die ohne diese Constraints erst zur Laufzeit aufgetreten wären. Beispiele: Korrekt: vNth (Cons 3 (Cons 4 Nil)) (S (S Z)) 4 Vektor nicht gross genug: vNth (Cons 3 (Cons 4 Nil)) (S (S (S Z))) Couldn't match type False with True Expected type: True Actual type: GreaterEqualThan (S (S Z)) (S (S (S Z))) Zweiter Parameter ist Null: vNth (Cons 3 (Cons 4 Nil)) Z Couldn't match type False with True Expected type: True Actual type: GreaterEqualThan Z (S Z) Ein Problem bei diesem Ansatz ist, dass Typkonstruktoren bis auf die Stelligkeit ungetypt sind. Bei dem Nat-Datentyp kann nicht deniert werden, welche Typen als Parameter des Nat-Typkonstruktors erlaubt sind. Gewünscht wäre etwas in dieser Art: data Nat :: KindOfZAndN → ∗ where Z :: Nat Z S :: Nat n → Nat (S n) Nat soll also ein Typkonstruktor sein, der nur Typen als Parameter akzeptiert, die natürliche Zahlen repräsentieren. Diese Funktionalität wird durch Datentyp-Promotion erreicht. 6 2. Grundlagen 2.3. Datentyp-Promotion Unsere Beispiele aus dem vorherigen Kapitel hatten das Problem, dass Typkonstruktoren ungetypt sind. Mit dem Typkonstruktor data S a könnten sinnlose Typen erstellt werden, wie zum Beispiel S Bool oder S Int. Ebenso können sinnlose Typen wie Vec Bool Bool genutzt werden. Beispiel: vec :: Vec Bool Bool → Vec Bool Bool vec = id Ein Programm mit dieser Funktion kompiliert ohne Fehler. Um dieses Verhalten zu verbessern, kann die Haskell-Erweiterung DataKinds [YWC+ 12] genutzt werden: {-# LANGUAGE DataKinds #-} Durch diese Erweiterung ist es möglich, die Datentypen auf diese Weise umzuschreiben: data Nat = Z | S Nat data Vec :: ∗ → Nat → ∗ where Nil :: Vec a Z Cons :: a → Vec a len → Vec a (S len) data UNat :: Nat → ∗ where UZero :: UNat Z USucc :: UNat n → UNat (S n) Die Erweiterung sorgt dafür, dass Datentypen zu Kinds befördert (englisch: promoted) werden und Werte zu Datentypen. So können Nat automatisch auf Kind-Ebene und Z sowie S Nat auf Typ-Ebene genutzt werden. Sie bestehen allerdings weiterhin auf ihrer ursprünglichen Ebene. Dies kann zu Namenskonikten führen, wenn zum Beispiel folgender Datentyp betrachtet wird: data T = T Int T in einem Typ kann der Typkonstruktor T vom Kind * sein oder der promotete Datenkonstruktor vom Kind Int -> T. In diesem Fall ist ein T ohne Änderung der Typkonstruktor und 'T mit einem einzelnen Anführungszeichen der promotete Datenkonstruktor. Die natürlichen Zahlen auf Typebene und Vec sind nun korrekt getypt und unerwünschte Typen werden abgelehnt. vec :: Vec Bool Bool → Vec Bool Bool vec = id → The second argument of Vec should have kind Nat, but Bool has kind ∗ In the type signature for vec: vec :: Vec Bool Bool → Vec Bool Bool badNat :: UNat Bool → UNat Bool badNat = id → 7 2. Grundlagen The first argument of UNat should have kind Nat, but Bool has kind ∗ In the type signature for badNat: badNat :: UNat Bool → UNat Bool Das Ganze ist jetzt schon sehr gut nutzbar. Eine Sache, die allerdings ein wenig unhandlich ist, ist die Darstellung der natürlichen Zahlen. Für dieses Problem gibt es aber auch eine Lösung. 2.4. Natürliche Zahlen auf Typ-Ebene Mit dem Modul GHC.TypeLits müssen die natürlichen Zahlen auf Typ-Ebene nicht mehr selbst deniert werden. Das Modul stellt diese in ihrer natürlichen Form zur Verfügung. Sie sind dabei weiterhin vom Kind Nat. Es werden zudem schon einige Typ-Funktionen wie Addition (+), Subtraktion (-), Multiplikation (*), Potenz () und Gröÿenvergleich (<= oder <=?) zur Verfügung gestellt. Unser Vektor aus den vorherigen Kapiteln kann dann wie folgt dargestellt werden: data Vec :: ∗ → Nat → ∗ where Nil :: Vec a 0 Cons :: a → Vec a len → Vec a (1 + len) Die natürlichen Zahlen auf Wert-Ebene müssen weiterhin wie zuvor dargestellt werden, können allerdings auch verbessert werden: data UNat :: Nat → ∗ where UZero :: UNat 0 USucc :: UNat n → UNat (1 + n) Eine Funktion aus dem Modul ermöglicht aber einen besseren Umgang mit diesen Zahlen. Die Funktion natVal nimmt einen Wert, der eine natürliche Zahl im Typ hat (zum Beispiel einen Vektor vom Typ Vec) und liefert die entsprechende Zahl als Integer: natVal :: forall n proxy. KnownNat n => proxy n → Integer Für jede natürliche Zahl gibt es eine Instanz der Klasse KnownNat, die die Zahl auf Typ-Ebene mit der Integer-Repräsentation verbindet. So ist es möglich, die Länge eines Vektors auszugeben: natVal (Cons 3 (Cons 7 (Cons 8 (Cons 3 Nil)))) 4 Oder besser als Funktion: vLength :: KnownNat n => Vec e n → Integer vLength vec = natVal vec An unseren bisherigen Funktionen ändert sich wenig, auÿer dass unsere eigenen Funktionen durch die aus dem Modul ersetzt werden. vappend :: Vec e n → Vec e m → Vec e (n + m) vappend Nil l = l 8 2. Grundlagen vappend (Cons x xs) ys = (Cons x (vappend xs ys)) vNth :: (m <= n, 1 <= m) => Vec e n → UNat m → e vNth (Cons x _) (USucc UZero) = x vNth (Cons _ xs) (USucc (USucc rest)) = vNth xs (USucc rest) Hier fällt lediglich auf, dass die Vergleichs-Constraints kein ~True am Ende mehr haben. Das liegt daran, dass <= lediglich ein Typ-Synonym ist: type (<=) x y = (x <=? y) ~ True Hierbei ist <=? die eigentlich Typfunktion für den Gröÿenvergleich, bei der ein Typ vom Kind Bool herauskommt. Die Funktionen und Datentypen sehen soweit nun schon sehr schön aus. Ein Versuch, ein Programm mit diesen Funktionen zu kompilieren, schlägt nun allerdings fehl. Es werden einige Fehler geworfen, dass manche der Constraints nicht abgeleitet werden können oder dass Typen nicht zusammenpassen, obwohl die Denitionen alle logisch korrekt sind. Wie dieses Problem gelöst wird, wird im Hauptteil erläutert. Eine weitere Grundlage, die dafür notwendig ist, sind SMT-Solver, die nun noch in Bezug auf Haskell erläutert werden sollen. 2.5. SMT-Solver SMT-Solver, wie zum Beispiel CVC4 [BCD+ 11] oder Z3 [DMB08], welcher im Folgenden benutzt wird, implementieren verschiedene Entscheidungs-Prozeduren, die zusammen ein bestimmtes Problem lösen. Wie diese Probleme genau von den Solvern gelöst werden, kann dem Nutzer im Prinzip egal sein. Der Nutzer stellt lediglich unbelegte Konstanten und Aussagen auf und lässt den Solver überprüfen, ob diese Aussagen erfüllbar sind. Eine Aussage ist erfüllbar, wenn die Konstanten so belegt werden können, dass die Aussage stimmt. Liegen zum Beispiel die Konstanten x und y sowie die Behauptung 2x = y vor, dann ist das Ganze erfüllbar mit der Belegung x = 2; y = 4. Ein Solver kann auf drei verschiedene Weisen Antworten: • sat: Eine erfüllende Belegung existiert • unsat: Eine erfüllende Belegung existiert nicht • unknown: Keine erfüllende Belegung wurde gefunden, es könnte aber eine existieren In der Arbeit, mit der sich diese Seminararbeit hauptsächlich befasst, wird mit dem SMTLIB-Standard gearbeitet. Die wichtigsten Funktionalitäten davon sollen kurz erläutert werden. Konstanten werden mit declare-fun und Aussagen mit assert deklariert. Soll eine Menge von Aussagen überprüft werden, muss check-sat genutzt werden. (declare-fun x () Int) (declare-fun y () Int) (assert (= (∗ 2 x) y)) (check-sat) 9 2. Grundlagen Eine weitere Funktion, die viele Solver unterstützen, ist es, einen Zustand zu speichern und wieder herzustellen. Auf diese Weise können mehrere Kombinationen von Aussagen auf einmal getestet werden. Dabei wird der Zustand mit push gespeichert und mit pop wieder geladen. (declare-fun x () Int) (declare-fun y () Int) (assert (= (∗ 2 x) y)) (assert (>= y 1)) (push 1) (assert (>= 0 x)) (check sat) (pop 1) (check-sat) Zunächst werden zwei Konstanten und Aussagen deklariert. Dieser Zustand wird als Zustand 1 gespeichert. Bis zu dieser Stelle ist es noch möglich, eine korrekte Belegung zu nden. (x = 2; y = 4) Daraufhin wird zusätzlich gefordert, dass x negativ sein soll und der Solver wird gefragt, ob es eine Lösung gibt. Der Solver wird melden, dass es keine gibt. Durch pop 1 wird zurück zum Zustand gewechselt, in dem noch nicht gefordert wurde, dass x negativ ist und es wird wieder eine Lösung gesucht. Nun wird der Solver melden, dass es eine korrekte Lösung gibt. Diese Funktionalität ist sehr hilfreich, wenn viele Aussagen getestet werden sollen, welche alle sehr ähnlich sind und sich nur in kleinen Details unterscheiden. So können alle diese Aussagen in einem Durchlauf getestet werden. Nun sind alle Grundlagen erläutert worden, die zum Verständnis des Haupteils notwendig sind. 10 3. Verbesserung von Haskell-Typen mit SMT 3.1. Motivation In Kapitel 2 wurde ein Vektor-Datentyp und entsprechende Funktionen vorgestellt: data Vec :: ∗ → Nat → ∗ where Nil :: Vec a 0 Cons :: a → Vec a len → Vec a (1 + len) data UNat :: Nat → ∗ where UZero :: UNat 0 USucc :: UNat n → UNat (1 + n) vappend :: Vec e n → Vec e m → Vec e (n + m) vappend Nil l = l vappend (Cons x xs) ys = (Cons x (vappend xs ys)) vNth :: (m <= n, 1 <= m) => Vec e n → UNat m → e vNth (Cons x _) (USucc UZero) = x vNth (Cons _ xs) (USucc (USucc rest)) = vNth xs (USucc rest) Das Problem ist nun, dass das Programm nicht kompiliert, obwohl es logisch korrekt ist. Es kommt zu Fehlermeldungen wie: Couldn't match type n with 1 + (n - 1) Could not deduce ((n + m) ~ (1 + (len + m))) from the context (n ~ (1 + len)) Could not deduce ((1 <=? n) ~ (m <=? n)) from the context (m <= n, 1 <= m) Das n = 1 + (n -1) gilt ist eigentlich klar und leicht verizierbar. Ebenso ist recht einfach zu sehen, dass n = 1 + len => n + m = 1 + len + m gilt, wenn auf der rechten Seite n durch 1 + len substituiert wird. Im Dritten Fall kann (m <=? n) durch True ersetzt werden, da das aus m <= n folgt. Auch (1 <=? n) kann durch True ersetzt werden, da n gröÿer gleich m und m gröÿer gleich 1 ist. Das Ziel des Plugins, das in Diatchkis Paper vorgestellt wird, ist es, dass GHC in der Lage ist, solche Schlussfolgerung selbst ziehen zu können. Zunächst soll dazu erläutert werden, wie GHCs Constraint Solver arbeitet. 11 3. Verbesserung von Haskell-Typen mit SMT 3.2. GHCs Constraint Solver In diesem Teil werden die relevanten Aspekte von GHCs Constraint Solver vorgestellt. Den vollen Umfang [VPjSS11] vorzustellen wäre zu komplex und ist für diese Seminararbeit nicht vonnöten. Währen der Typinferenz arbeitet GHC mit sogenannten Implication Constraints. Diese haben ungefähr die folgende Form: G → W Hierbei ist W eine Menge von Constraints die erfüllt sein müssen, damit ein Programm als korrekt angesehen werden kann. Sie werden Wanted Constraints genannt. G ist eine Menge an an gegebenen Aussagen, die genutzt werden können, um Constraints aus W zu beweisen. Sie werden Given Constraints genannt. W und G können so betrachtet werden, dass W Constraints enthält, die gesammelt wurden während ein Stück Programm-Code geprüft wurde und G lokale Aussagen enthält, die nur in diesem spezischen Stück Code genutzt werden können. Das Ganze kann gut an einem Beispiel erläutert werden: data E :: ∗ → ∗ where EInt :: Int → E Int EString :: String → E String EAny :: a → E a isGreater :: (E a) → (E b) → Bool isGreater (EInt x) (EInt y) =x>y isGreater (EString x) (EString y) = x > y Bei diesem Beispiel gibt es jeweils einen Implication Constraint für die beiden isGreaterFälle. (a ~ Int, b ~ Int) => (Ord a, Ord b, a ~ b) (a ~ String, b ~ String) => (Ord a, Ord b, a ~ b) Beide Fälle haben den gleichen Wanted Constraint. Beide Typvariablen a und b müssen aus der Klasse Ord stammen und es muss gelten, dass a und b gleich sind. Das ist zu erkennen, wenn man sich den Typ von (>) anschaut. (>) :: Ord a => a → a → Bool Die Given Constraints ergeben sich, da beim Pattern Matching die Konstruktoren des GADTs genutzt werden, welcher die Typen der Variablen genau vorgibt. Da a und b jeweils gleich sind und Int sowie String beide die Klasse Ord implementieren, akzeptiert GHC dieses Programm. Dieses Beispiel wird von GHC nicht akzeptiert: isGreater :: (E a) → (E b) → Bool isGreater (EInt x) (EAny y) =x>y → Could not deduce (b ~ Int) from the context (a ~ Int) Da EAny keine Aussage über den Typ von b gibt, kann dieses Programm nicht akzeptiert werden. In der Motivation wurden Fehler gezeigt, die GHC beim Vektor-Beispiel wirft: 12 3. Verbesserung von Haskell-Typen mit SMT Could not deduce ((n + m) ~ (1 + (len + m))) from the context (n ~ (1 + len)) Auch hier haben wir es mit einem Implication Constraint zu tun: (n ~ (1 + len)) => ((n + m) ~ (1 + (len + m))) Die Aufgabe des Plugins ist es nun, solche Constraints zu lösen, wenn GHC dazu nicht in der Lage war. Implications Constraints werden durch zwei Aufrufe des Constraint Solver gelöst: Im Ersten werden alle Given Constraints gesammelt und im zweiten werden die Wanted Constraints abgearbeitet. Der Constraint Solver hat dabei zwei Dinge, die seinen Zustand ausmachen. Er hat eine Work Queue und ein Inert Set. Die Work Queue beinhaltet Constraints, die noch bearbeitet werden müssen und das Inert Set Constraints, die schon bearbeitet wurden. Es wird ein Constraint nach dem anderen aus der Work Queue genommen und bearbeitet mit Hilfe des aktuellen Zustands. Dabei können Constraints gelöst werden, neue Constraints erzeugt oder nicht lösbare Constraints gemeldet werden. Wenn nichts passiert, wird das Constraint lediglich in das Inert Set gegeben. Constraints im Inert Set können dabei auch wieder in umgeschriebener Form aktiviert und in die Work Queue geschoben werden. Ein Aufruf des Constraint Solvers arbeitet solange, bis keine Constraints mehr in der Work Queue enthalten sind. Constraints können verbessert werden, indem ihre Variablen mit Typen instantiiert werden. Das Ganze sollte nur geschehen, wenn die Instantiierung zwei Regeln befolgt. Zum Einen müssen verbesserte Constraints die originalen Constraints implizieren, damit keine Eigenschaften der Constraints verloren gehen. Zum Anderen müssen die originalen Constraints die verbesserten implizieren, da die Constraints sonst spezischer werden und Allgemeingültigkeit verlieren. Wird die erste Regel verletzt, dann könnten falsche Programme akzeptiert werden. Im zweiten Fall könnten korrekte Programme abgelehnt werden, die ohne die Verbesserung akzeptiert werden würden. GHC verbessert Constraints mit Hilfe von Equality Constraints. Es gibt drei Quellen von Equality Constraints: • Given Equalities durch Given Constraints • Wanted Equalities durch Wanted Constraints • Derived Equalities durch Given und Wanted Constraints Es ist nicht möglich, mit allen Kategorien von Equality Constraints andere Constraints zu verbessern. Given Equalities sind bewiesen, da sie von Given Constraints stammen. Diese können daher benutzt werden, um beliebige Constraints zu verbessern. Wanted Constraints können allerdings nicht genutzt werden, um Given Constraints zu ändern, da sie noch bewiesen werden müssen. Sie können aber benutzt werden, um andere Wanted Constraints oder Derived Constraints zu ändern. Derived Constraints dürfen in Beweisen direkt gar nicht benutzt werden, da es sonst zu zyklischer Argumentation kommen kann. Derived Constraints werden stattdessen genutzt, um die Instanziierung von unication variables zu steuern. Da Derived Constraints von Given und Wanted Constraints impliziert werden, werden dadurch auch keine korrekten Programme abgelehnt, da keine 13 3. Verbesserung von Haskell-Typen mit SMT Allgemeingültigkeit verloren geht. Wurde nun zum Beispiel das Derived Constraints x ~Int berechnet und Eq x ist ein Given Constraint, dann kann Eq x zu Eq Int umgeschrieben werden. Dieses Constraint muss nun weiterhin versucht gelöst zu werden. Wie der GHC Constraint Solver nun erweitert wird, wird im folgenden Kapitel behandelt. 3.3. Type-Checker-Plugins Die Erweiterung des Papers wurde mit einer type-checker plug-in-API implementiert, welche in Kollaboration mit anderen Wissenschaftlern entwickelt wurde, die auch an Erweiterungen für das Typsystem von GHC arbeiten. Diese API ist noch neu und nicht stabil, kann allerdings schon genutzt werden. Da die API sehr mächtig ist, ist es auch möglich die Typsicherheit zu verletzen, wenn man nicht gründlich damit arbeitet. Eine Frage, die sich beim Implementieren dieser API gestellt hat, ist es, an welcher Stelle des GHC-Typsystems sie greifen soll. Es wurde sich dafür entschieden, dass die mit der API integrierten Plugins dann aufgerufen werden, wenn GHCs Constraint Solver einen intert state erreicht hat. Also dann, wenn GHCs Constraint Solver es nicht geschat hat, ein Programm als korrekt getypt zu verizieren, es aber auch nicht abgelehnt hat. Die Plugins können so mit allen Constraints auf einmal arbeiten nachdem die meiste Arbeit schon von GHCs Solver erledigt wurde. Die Plugins bearbeiten dann diesen erreichten Zustand der Constraints und versuchen einen Fortschritt zu erreichen. Schaen sie es, dann wird GHCs Solver erneut aufgerufen und der Prozess startet von vorne. Das geschieht solange, bis ein Programm akzeptiert/abgelehnt wird, oder kein Fortschritt mehr auftritt. 3.4. Integration eines SMT-Solvers in GHC 3.4.1. Input und Output Die Erweiterung wird anhand der Theorie von linearer Arithmetik über die natürlichen Zahlen beschrieben, da eine konkrete Theorie es einfacher macht, die Konzepte der Erweiterung zu erläutern. Da die Erweiterung mit der API aus dem vorherigen Kapitel implementiert wurde, nimmt sie als Input eine Menge an Constraints, die GHC zuvor mit dem eigenen Solver bearbeitet hat, aber nicht komplett verizieren konnte, also das Inert Set. Dieses ist in die drei zuvor erläuterten Gruppen aufgeteilt: Given Constraints, Wanted Constraints und Derived Constraints. Der Output der Erweiterung ist vom Typ der gleiche. Es sollen allerdings soviele Wanted Constraints wie möglich gelöst werden oder als inkonsistent erkannt werden und es sollen wenn möglich neue Given sowie Derived Constraints erzeugt werden, die anderen Solvern helfen können, die Constraints zu lösen. Das Lösen von Wanted Constraints ist die oensichtlichste Aufgabe des Ganzen. Aber auch die anderen Aufgaben sind sehr sinnvoll. Als Beispiel für Inkonsistenz sollen folgende 14 3. Verbesserung von Haskell-Typen mit SMT Funktionen dienen: nonsenseFun n m = (nonsenseHelper1 n m) && (nonsenseHelper2 n) nonsenseHelper1 :: (n + m) ~ 3 => UNat n → UNat m → Bool nonsenseHelper1 _ _ = True nonsenseHelper2 :: (4 <= n) => UNat n → Bool nonsenseHelper2 _ = True Der Typ von nonsenseFun wird nicht angegeben, also inferiert GHC ihn. Da die beiden Funktionen nonsenseHelper1 und nonsenseHelper1 genutzt werden und beide Constraints haben, inferiert GHC folgenden Typ: nonsenseFun :: ((n + m) ~ 3, (4 <=? n) ~ 'True) => UNat n → UNat m → Bool Dieser Typ erzeugt nun allerdings die inkonsistenten Wanted Constraints (n + m) ~3 und 4 <= n. Es gibt also keine Werte, für die diese Funktion Sinn macht und wenn sie benutzt wird, wird für jeden Wert ein Fehler geworfen. GHC akzeptiert dieses Programm allerdings. Es macht Sinn, solche Inkonsistenzen zu erkennen und schon zur Übersetzungszeit zu melden. Neue Gleichungen zu berechnen ist auch sehr hilfreich, da verschiedene Plugins und GHC so interagieren können. Ein Plugin kann neue Gleichungen erzeugen, die von einem anderen Plugin oder GHC genutzt werden können, um weitere Constraints zu lösen. forall x. (x + 5) ~ 8 => KnownNat x In diesem Beispiel kann der SMT-Solver die neue Given Equality x ~3 berechnen. GHC kann daraufhin das Wanted Constraint KnownNat x zu KnownNat 3 umschreiben, welches dann von dem Solver für die KnownNat-Klasse gelöst werden kann. 3.4.2. Die Sprache von Constraints Man muss sich genau überlegen, welche Arten von Constraints überhaupt relevant sind. In der aktuellen Implementierung der Erweiterung werden lediglich Gleichheiten und Ungleichheiten von natürlichen Zahlen auf Typ-Ebene mit Kind Nat betrachtet. Nat enthält unendlich viele Typ-Konstanten, also alle natürliche Zahlen, welche mit Typfunktionen aus dem TypeLits-Modul kombiniert werden können. Folgende Typfunktionen sind mit dieser Erweiterung nutzbar: type family (+) :: Nat → Nat → Nat type family (∗) :: Nat → Nat → Nat type family (<=?) :: Nat → Nat → Bool Diese Typfunktionen haben keine nutzer-denierte Denitionen. Stattdessen wurde der core GHC simplier erweitert, sodass konkrete Ausdrücke wie 2 + 3 direkt ausgewertet werden. Das hat den Vorteil, dass die Constraints sich nur auf komplexere Dinge mit Variablen beschränken. Alle unterstützen Constraints werden durch folgende Grammatik beschrieben: c = e ∼N e | e ∼B e 15 3. Verbesserung von Haskell-Typen mit SMT eN = α | n | e + e | n ∗ e eB = α | F alse | T rue | e <=?e n = 0 | 1 | ... Es gibt Gleichheits-Constraints mit zwei Ausdrücken, wobei beide entweder eine natürliche Zahl oder beide einen booleschen Wert repräsentieren. Ein Ausdruck, der eine natürliche Zahl darstellt, ist entweder eine Variable, eine Konstante natürliche Zahl oder zwei Ausdrücke addiert beziehungsweise multipliziert. Ein Ausdruck, der einen booleschen Wert darstellt, ist entweder eine Variable, False, True oder ein Vergleich zwischen zwei natürlichen Zahlen. 3.4.3. Auswahl von Constraints Es ist zunächst wichtig zu schauen, welche der Constraints überhaupt relevant für die Erweiterung sind. Alle Constraints, die der Grammatik entsprechen, sollen behandelt werden. Zusätzlich können aber auch komplexere Constraints von Interesse sein, die nicht auf Anhieb der Grammatik entsprechen. Nutzer können Constraints mit Typ-Funktionen nutzen, die zu Typen vom Kind Nat ausgewertet werden. Auch solche Constraints sollten von der Erweiterung bearbeitet werden. Ausdrücke mit nutzerdenierten Typfunktionen müssen allerdings umgeschrieben werden. Das Constraint (F 3 + 4) ~7, wobei F eine Typfunktion ist, die zu Nat auswertet, wird zum Beispiel als (x + 4) ~7 bearbeitet. Wenn solche Constraints später wieder an GHC zurückgegeben werden, muss das x wieder durch F 3 ersetzt werden. Ausdrücke mit nutzerdenierten Typfunktionen werden also einfach als bisher unbenutzte Variable behandelt und später wieder umgeschrieben. Ein weiterer Aspekt, der beachtet werden muss, ist das SMT-Solver mit Integern arbeiten, die Erweiterung allerdings mit natürlichen Zahlen. Es muss also immer darauf geachtet werden und es müssen entsprechende Constraints geschrieben werden, dass Variablen nicht negativ sein dürfen. 3.4.4. Kommunikation mit dem Solver Es stellt sich die Frage, wie mit SMT-Solvern kommuniziert werden soll. Es gibt zum einen die Möglichkeit, die SMT-Solver direkt anzusprechen und ihre spezische API zu nutzen. Diese Variante wäre ezient, schränkt die Erweiterung aber auf bestimmte unterstützte SMT-Solver ein, für die man sich entscheiden müsste. Da die Erweiterung exibel sein soll, wurde sich dafür entschieden, eine Haskell-API für SMT-Solver zu entwickeln, welche dann von der Erweiterung genutzt wird. Die API basiert auf der SMTLIB-Sprache, die im Grundlagenkapitel vorgestellt wurde. Alle SMT-Solver, die diese Sprache unterstützen, können dann genutzt werden. Die API sieht wie folgt aus: data Solver :: ∗ data Expr :: ∗ data Result = Sat | Unsat solverAssert :: Solver → Expr → IO () solverCheck :: Solver → IO Result solverPush :: Solver → IO () 16 3. Verbesserung von Haskell-Typen mit SMT solverPop :: Solver → IO () Sie lehnt sich also direkt an die SMTLIB-Sprache an. Diese API wird in den folgenden Kapiteln genutzt. 3.4.5. Konsistenz von Constraints Eine der Aufgaben der Erweiterung ist es, die Constraints auf Konsistenz zu überprüfen. Diese Aufgabe wird so gelöst, dass alle Constraints mit solverAssert gesetzt werden. Dann wird der SMT-Solver gefragt, ob es eine Lösung für diese Constraints gibt. Ist das der Fall, können die nächsten Schritte der Erweiterung ausgeführt werden. Die nächsten Schritte müssen noch ausgeführt werden, da eine erfolgreiche Lösung an dieser Stelle noch nicht bedeutet, dass das Programm korrekt ist, da es unter Umständen Typfunktionen gab, die zu einer Variable umbenannt wurden. Es muss noch gezeigt werden, dass Constraints mit solchen Typfunktionen lösbar mit der gefundenen Belegung sind. Gibt es keine Lösung, kann das Programm abgelehnt werden. Viele SMT-Solver sind in der Lage einen Kern an Constraints zu nennen, der dafür verantwortlich ist, dass eine Inkonsistenz besteht. Für den Nutzer ist es durchaus sinnvoll, zu erfahren, an welche Stelle das Problem liegt. Da aber nicht alle SMT-Solver die Eigenschaft besitzen, wurde ein Algorithmus geschrieben, der diese Aufgabe übernimmt. Eine genaue Beschreibung von diesem Algorithmus ist im Paper über diese Erweiterung zu nden. 3.4.6. Verbesserung von Constraints Durch die Prüfung der Konsistenz gibt es nun schon eine lösende Belegung aller Variablen. Es ist allerdings durchaus noch möglich, diese Belegung zu verbessern und neue Equality Constraints zu erzeugen. Wenn alle Constraints, die genutzt werden, Given Constraints sind, dann werden Given Equalities erzeugt, sonst Derived Equalities. Es gibt drei Arten von Verbesserungen. Die erste Variante ist es, zu schauen, ob Variablen als Konstante gesetzt werden können. Wird bei der Prüfung der Konsistenz eine Belegung für eine Variable gefunden, dann wird geprüft, ob diese Belegung die einzige mögliche Belegung ist. Es wird dafür geprüft, ob die Constraints auch lösbar sind, wenn die Variable ungleich ihrer zuvor gefunden Belegung ist. Ist dies nicht möglich, dann ist diese Belegung die einzig mögliche und es kann als Equality Constraint gesetzt werden, dass diese Variable gleich der Belegung ist. Die zweite Variante ist es, zu schauen, ob eine Variable gleich einer anderen Variable ist. Wenn zwei Variablen in der gefundenen Belegung den gleichen Wert haben, kann mit dem SMT-Solver getestet werden, ob sie immer den gleichen Wert haben. Ist das der Fall, kann das Equality Constraint gesetzt werden, das aussagt, dass beide Variablen gleich sind. Die dritte Variante ist es, zu schauen, ob zwei Variablen in einer linearen Relation zueinander stehen. Sind x und y Variablen, wird also versucht eine Relation der Form y = A*x + B zu nden. Diese Variante wird nur genutzt, wenn die ersten beiden fehlschlugen. 17 3. Verbesserung von Haskell-Typen mit SMT Diese Relation zu nden ist nur nützlich, wenn y eine Typvariable ist, die noch instantiiert werden muss. Ansonsten sind solche Relationen eher nicht brauchbar, da andere Constraint Solver von GHC nichts mit den Typfunktionen wie + und * dieser Erweiterung anfangen können. A und B können berechnet werden, wenn es zwei Belegungen für x und y gibt. Da x und y keine Konstanten sind, gibt es diese immer. Wurden A und B gefunden, muss die Relation noch vom SMT-Solver geprüft werden, um zu schauen, ob sie immer gilt. Beispiel: f :: Proxy (a + 1) f = Proxy g :: Proxy (b + 2) g=f Hier muss das Constraint (?a + 1) ~(b + 2) gelöst werden. Hierbei ist a eine Variable, die einen Typ darstellt, der noch inferiert werden muss. Die Erweiterung kann nun herausnden, dass ?a := b + 1 eine sichere Instanziierung von a ist und das Constraint umschreiben. Neben diesen drei Varianten ist es zudem noch sinnvoll, das System durch spezielle Regeln zum umschreiben von Constraints zu erweitern, die nicht nur ausschlieÿlich die Fähigkeiten eines SMT-Solvers nutzen. So können zum Beispiel bestimmte Arten von Constraints auf eine spezielle Weise umgeschrieben werden, um die Erweiterung mächtiger und/oder performanter zu machen. 3.4.7. Lösen von Constraints Das Lösen von Constraints geschieht durch einen simplen Aufruf des SMT-Solvers. Es müssen lediglich alle Aussagen (solverAssert) entfernt werden, die bei der Prüfung der Konsistenz und der Verbesserung von Constraints hinzugefügt wurden. Diese Aussagen waren nur dafür notwendig, diese Aufgaben zu lösen und sollen bei der Lösung der Constraints nicht vorhanden sein. Nur die ursprünglichen Given Constraints sollen vorhanden sein. Alle Wanted Constraints, die zu der Erweiterung passen (siehe "Auswahl von Constraints"), werden in die SMTLIB Sprache übersetzt, woraufhin sie an den SMT-Solver geschickt werden, welcher versucht sie zu lösen. Beim Lösen von Constraints erwartet GHC eigentlich, dass ein Beweis der Lösung mitgeliefert wird. Im Moment erzeugt die Erweiterung allerdings noch keine bedeutungsvolle Beweise, auÿer dass die Lösung von einem SMT-Solver gefunden wurde. Nicht alle SMT-Solver unterstützen eine Funktion, die einen Beweis liefert und wenn, dann sind die Beweise nicht in einem standardisiertem Format. Damit sind alle wichtigen Teile erläutert wurden, die für die Integration eines SMTSolvers in GHC notwendig sind. 3.5. Erweiterung durch weitere Theorien Bisher funktioniert die Erweiterung nur für natürliche Zahlen. SMT-Solver unterstützen allerdings auch andere Datentypen, weswegen es Sinn macht, die Erweiterung in der Zukunft um weitere Theorien zu erweitern. 18 3. Verbesserung von Haskell-Typen mit SMT Eine Theorie, die sinnvoll wäre, ist die Theorie für boolesche Werte. GHC selbst kann mit booleschen Werten im Typsystem nicht gut umgehen. Dass zum Beispiel beim Constraint And x y ~True beide Variablen True sein müssen, kann GHC nicht erkennen. Da SAT-Solver meist ein Teil von SMT-Solvern sind, könnten solche Constraints gut von dieser Erweiterung gelöst werden. Da im SMT-Solver mit Integern gearbeitet wird, auch wenn diese Erweiterung mit natürlichen Zahlen arbeitet, wäre die Theorie über Integer auch eine interessante Erweiterung. Es muss lediglich eine Notation eingeführt werden, damit mit Integern und weiterhin mit natürlichen Zahlen gearbeitet werden kann. (Unterschiedliche Namen für die Typfunktionen) Da SMT-Solver sehr gut mit Bit-Vektoren umgehen können und viele Operationen für sie anbietet, ist die Theorie für diese auch eine sehr interessante Idee. 3.6. Beispiel Mit der Erweiterung kann unser Beispiel aus dem Grundlagen-Kapitel nun kompiliert werden. Es soll nun aufbauend auf diesem Beispiel ein gröÿeres Beispiel vorgestellt werden, um zu zeigen wie mächtig die Erweiterung ist und worauf geachtet werden muss. Ein sehr interessantes Beispiel sind Matrizen und verschiedene Funktionen auf ihnen. Zunächst wird aber das Vektorenbeispiel noch ein wenig erweitert. Die Funktion vNth hat einen Wert vom Typ UNat m als Parameter. vNth :: ((m + 1) <= n) => Vec a n → UNat m → a vNth (Cons x _) UZero = x vNth (Cons _ xs) (USucc rest) = vNth xs rest (Dies ist eine leicht veränderte Variante, die nur noch verlangt, dass (m + 1) <= n gilt und bei Null anfängt zu zählen) Es werden sicherlich noch weitere Funktionen hinzu kommen, die mit solchen Werten arbeiten. Da es umständlich ist, UNat-Werte von Hand zu schreiben, wäre es schön, wenn diese generiert werden könnten. Da bei UNat-Werten die repräsentierte natürliche Zahl im Typ steht, ist das allerdings nicht ganz so simpel. Die Funktion muss vom Typ her passen. Aus diesem Grund wird zunächst ein Proxy-Datentyp erstellt: data NatProxy (n :: Nat) = NatProxy Dieser Datentyp hat nur einen möglichen Wert, dieser Wert kann allerdings verschiedene Typen haben, der die natürlichen Zahlen darstellt. Die Funktion kann dann folgenden Typ haben: toUNat :: NatProxy n → UNat n Wie soll diese Funktion nun allerdings implementiert werden? NatProxy n kann nur den Wert NatProxy annehmen, mit Pattern Matching kann also nicht gearbeitet werden. Das Ergebnis kommt auf auf den Typ von dem NatProxy-Wert an. Nun wäre es eine Idee, dafür eine Typklasse zu erstellen. Das Problem ist, dass für jede einzelne natürliche Zahl eine Instanz dieser Typklasse gebildet werden müsste. Da diese Instanzen alle das gleiche 19 3. Verbesserung von Haskell-Typen mit SMT Muster haben, könnten sie leicht bis zu einer bestimmten Zahl generiert und in einem Modul ausgelagert werden. Damit diese Arbeit nicht notwendig ist, wird ein zugegeben leicht unschöner Trick angewendet, der durch das Modul Unsafe.Coerce möglich gemacht wird. toUNat :: KnownNat n => NatProxy n → UNat n toUNat p = convert (natVal p) where convert :: Integer → UNat m convert 0 = unsafeCoerce $ UZero convert x = unsafeCoerce $ USucc $ convert $ x - 1 Es wird der Wert von Typ NatProxy mit natVal in einen Integer-Wert umgewandelt, woraufhin dieser mit einer Hilfsfunktion in einen Wert vom Typ UNat n umgewandelt wird. Hier kommt folgende Funktion ins Spiel: unsafeCoerce :: a → b Diese Funktion sorgt dafür, dass der Wert den gewünschten Ergebnistyp hat. Diese Funktion ist höchst unsicher. Da die Funktion toUNat allerdings sehr simpel und leicht zu validieren ist, soll das für unsere Zwecke hier in Ordnung sein. Die Funktion funktioniert beispielsweise folgendermaÿen: vNth (Cons 2 (Cons 7 (Cons 3 (Cons 9 (Cons 2 (Cons 10 (Cons 3 Nil))))))) (toUNat (NatProxy :: NatProxy 5)) → 10 Nun können einige hilfreiche Funktionen für Vektoren deniert werden, die keine weitere Erklärung benötigen sollten: vMap :: Vec a n → (a → b) → Vec b n vMap Nil _ = Nil vMap (Cons x xs) f = Cons (f x) (vMap xs f) vFoldr :: (a → b → b) → b → Vec a n → b vFoldr _ b Nil =b vFoldr f b (Cons a xs) = f a (vFoldr f b xs) vSum :: Num a => Vec a n → a vSum vec = vFoldr (+) 0 vec vLengthU :: KnownNat n => Vec a n → UNat n vLengthU _ = toUNat (NatProxy :: NatProxy n) vZipWith :: Vec a n → Vec b n → (a → b → c) → Vec c n vZipWith Nil Nil _ = Nil vZipWith (Cons x xs) (Cons y ys) f = Cons (f x y) (vZipWith xs ys f) Eine Funktion, die noch einmal ganz schön die Fähigkeiten der Erweiterung zeigt, ist folgende: vSplitAt :: (1 <= m, nat <= m, (nat + n) ~ m) => Vec a m → UNat nat → (Vec a nat, Vec a n) 20 3. Verbesserung von Haskell-Typen mit SMT vSplitAt (Cons x xs) (USucc UZero) = ((Cons x Nil), xs) vSplitAt (Cons x xs) (USucc (USucc rest)) = let (left, right) = vSplitAt xs (USucc rest) in ((Cons x left), right) Die Funktion vSplitAt teilt einen Vektor in zwei Vektoren auf, wobei der erste die ersten nat Werte des ursprünglichen Vektors beinhaltet und der zweite die restlichen n Werte. Ohne die Erweiterung wäre es zwar auch möglich, diesen Typ zu deklarieren, GHC wäre aber nicht in der Lage, zu verizieren, dass die Funktion zu dieser Signatur passt beziehungsweise typkorrekt ist. Ein kleines Beispiel, dass einige dieser Funktionen nutzt: vectorOne = (Cons 3 (Cons 2 (Cons 1 (Cons 7 (Cons 9 (Cons 3 (Cons 8 Nil))))))) vectorTwo = (Cons 3 (Cons 8 (Cons 2 (Cons 4 (Cons 8 (Cons 1 Nil)))))) (vectorThree, vectorFour) = vSplitAt vectorOne (toUNat (NatProxy :: NatProxy 3)) (vectorFive, vectorSix) = vSplitAt vectorTwo (toUNat (NatProxy :: NatProxy 2)) vectorSeven = vAppend vectorThree vectorSix vectorEight = vAppend (vAppend vectorFour vectorFive) (Cons 1 Nil) vectorSum = vSum (vZipWith vectorSeven vectorEight (+)) Es werden einige Vektoren deniert und durch einige Funktionen werden daraus neue Vektoren gebaut. Am Ende werden vZipWith und vSum genutzt, um die Werte von zwei Vektoren zu summieren. Hier wird von der Erweiterung richtig erkannt, dass die Vektoren vectorSeven und vectorEight die gleiche Länge besitzen. Ansonsten würde es zu einer Fehlermeldung kommen, da vZipWith verlangt, dass beide Vektoren die gleiche Länge haben. Dieses Beispiel ist nun ein statisches Programm. Es gibt keine Eingabe und keine Ausgabe. Sicherlich ist es wünschenswert, dass die Erweiterung auch bei Programmen genutzt werden kann, die mit IO arbeiten. Dabei stellt sich allerdings die Frage, was für Eingaben vom Nutzer möglich sind. Wie soll die Erweiterung entscheiden, ob ein Programm korrekt getypt ist, wenn ein Nutzer zum Beispielen Vektoren als Eingabe liefert? Die Längen der Vektoren sind zur Übersetzungszeit dann nicht bekannt. Mit ein paar Überlegungen ist es allerdings möglich, hier einiges zu erreichen. Eine sehr wichtige Funktion dafür bietet das typelits-Modul: data SomeNat = forall n ◦ KnownNat n => SomeNat (Proxy n) someNatVal :: Integer → Maybe SomeNat Die Funktion someNatVal nimmt einen Integer-Wert und liefert einen Wert vom Typ SomeNat zurück, wobei der Typ vom Proxy-Wert diesen Integer-Wert enthält. Es hebt also einen Wert von der Wert-Ebene in die Typ-Ebene. Der Nutzer kann also Werte als Eingabe haben und basierend auf diesen Werten kann in der Typ-Ebene gearbeitet 21 3. Verbesserung von Haskell-Typen mit SMT werden. So könnte dem Nutzer ermöglicht werden, Vektoren als Liste einzugeben, welche zu einem Vektor umgewandelt werden. Dazu wurde folgende Funktion implementiert: vCreate vCreate vCreate vCreate vCreate :: UNat n → [a] → a → Vec a (n+1) UZero (x:_) _ = (Cons x Nil) UZero [] a = (Cons a Nil) (USucc rest) (x:xs) a = (Cons x $ vCreate rest xs a) (USucc rest) [] a = (Cons a $ vCreate rest [] a) Diese Funktion erzeugt immer einen Vektor von Länge n+1, basierend auf einer Liste. Für den Fall, dass die Liste nicht lang genug ist, muss ein Wert mitgegeben werden, mit dem der Vektor dann aufgefüllt wird. Die Länge des Vektors ist n+1, damit für jedes n sichergestellt werden kann, dass der Vektor mindestens die Länge 1 besitzt. Darauf muss bei der Nutzung geachtet werden. Nun kann beispielsweise folgende IO-Funktion geschrieben werden: vectorZipSumIO :: IO () vectorZipSumIO = do line1 ← getLine line2 ← getLine let readVec1 = (reads line1 :: [([Int], String)]) let readVec2 = (reads line2 :: [([Int], String)]) case (readVec1 , readVec2) of (((vec1list, _):_), ((vec2list,_):_)) → do case (length vec1list == length vec2list) of True → do let l = toInteger (length vec1list) let Just someNatL = someNatVal (l-1) case someNatL of SomeNat (_ :: Proxy n) → do let vec1 = vCreate (toUNat (NatProxy :: NatProxy n)) vec1list 0 let vec2 = vCreate (toUNat (NatProxy :: NatProxy n)) vec2list 0 print $ vSum $ vZipWith vec1 vec2 (+) _ → putStr "Fehler" _ → putStr "Fehler" Es werden zwei Eingaben gelesen und dann wird geschaut, ob diese Listen mit Ints entsprechen. Ist das nicht der Fall, bricht die Funktion ab und gibt "Fehler" aus. Im korrekten Fall wird getestet, ob beide Listen die gleiche Länge haben und auch hier wird im False-Fall abgebrochen. Ist alles korrekt, wird die Länge der Listen in einen Integer-Wert verwandelt, welcher dann in die Typ-Ebene gehoben wird. Mit einem case-Ausdruck kann dieser Typ auch genutzt werden, um zwei Vektoren mit vCreate zu initialisieren. Das n von Proxy n ist das gleiche wie das n bei NatProxy. Typvariablen auf diese Art zu nutzen, dass n in beiden Fällen tatsächlich die gleiche Typvariable ist, funktioniert durch die Erweiterung ScopedTypeVariables. Da nun sichergestellt ist, dass beide Vektoren die gleiche Länge haben, kann vZipWith aufgerufen werden. Schon zur Übersetzungszeit ist sichergestellt, dass diese beiden Vektoren auf jeden Fall die gleiche Länge besitzen. Die Summe der Elemente beider Vektoren wird dann ausgegeben. Es ist durchaus auch möglich, den Test bezüglich der Länge der Listen wegzulassen, da vCreate 22 3. Verbesserung von Haskell-Typen mit SMT auf jeden Fall Vektoren der gewünschten Länge erzeugt. Diese Vektoren wären dann aber mit Nullen gefüllt, oder manche Elemente der Listen wären nicht enthalten. Mit Vektoren kann nun schon ganz gut programmiert werden. Der nächste Schritt ist es, Matrizen zu denieren. Da Matrizen als zweidimensionale Vektoren betrachtet werden können, sieht die Denition wie folgt aus: type Matrix a m n = Vec (Vec a n) m Matrizen haben m Reihen, die alle n Einträge haben. Es können dann auch schon sofort einfache Funktionen für Matrizen deniert werden: mZipWith :: Matrix a m n → Matrix b m n → (a → b → c) → Matrix c m n mZipWith xss yss f = vZipWith xss yss (\xs ys → vZipWith xs ys f) mMap :: Matrix a m n → (a → b) → Matrix b m n mMap xss f = vMap xss (\xs → vMap xs f) mAdd :: (Num a) => Matrix a m n → Matrix a m n → Matrix a m n mAdd xss yss = mZipWith xss yss (+) mNthRow :: ((l + 1) <= m) => Matrix a m n → UNat l → Vec a n mNthRow = vNth mNthCol :: ((l + 1) <= n) => Matrix a m n → UNat l → Vec a m mNthCol Nil nat = Nil mNthCol (Cons row rows) nat = Cons (vNth row nat) (mNthCol rows nat) mTail :: Matrix a m n → Matrix a m (n - 1) mTail (Cons (Cons a xs) Nil) = (Cons xs Nil) mTail (Cons (Cons a xs) ys) = Cons xs (mTail ys) mTranspose :: (1 <= m, 1 <= n) => Matrix a m n → Matrix a n m mTranspose mat@(Cons (Cons _ Nil) _) = (Cons (mNthCol mat UZero) Nil) mTranspose mat@(Cons (Cons _ (Cons _ _)) _) = Cons (mNthCol mat UZero) (mTranspose (mTail mat)) Die Funktion mTranspose liefert die transponierte Version einer gegebenen Matrix, wobei alle Reihen zu Spalten werden und umgekehrt. Die jeweils nte Spalte wird zur nten Reihe. Die Funktion, die ein wenig interessanter wird, ist die Matrizenmultiplikation. Sie hat folgenden Typ: mMult :: (Num a, 1 <= m, 1 <= n, 1 <= p) => Matrix a m n → Matrix a n p → Matrix a m p Die Matrizen sollen mindestens 1×1 Matrizen sein. Der Rest der Signatur ist die normale Signatur einer Matrizenmultiplikation. Die Implementierung sieht wie folgt aus: mMult m n = let nTrans = (mTranspose n) in vMap m (\vec → mMult' vec nTrans) where mMult' :: Vec a n → Matrix a q n → Vec a q mMult' vec Nil = Nil 23 3. Verbesserung von Haskell-Typen mit SMT mMult' vecA (Cons vecB vecs) = Cons (vSum (vZipWith vecA vecB (∗))) (mMult' vecA vecs) Für jede Reihe von der Matrix m wird die Hilfsfunktion mMult' mit der transponierten Variante von der Matrix n aufgerufen. In der Hilfsfunktion wird die eigentliche Matrizenmultiplikation ausgeführt. Durch ScopedTypeVariables kann in der Hilfsfunktion auf die Typvariablen der Signatur zugegrien werden. Beispielnutzung: mVectorOne = Cons 3 (Cons 2 (Cons 1 Nil)) mVectorTwo = Cons 1 (Cons 0 (Cons 2 Nil)) mVectorThree = Cons 1 (Cons 2 Nil) mVectorFour = Cons 0 (Cons 1 Nil) mVectorFive = Cons 4 (Cons 0 Nil) matrixOne = Cons mVectorOne (Cons mVectorTwo Nil) matrixTwo = Cons mVectorThree (Cons mVectorFour (Cons mVectorFive Nil)) matrixMult = mMult matrixOne matrixTwo matrixMult → 7, 8 9, 2 Auch bei Matrizen kann natürlich mit IO gearbeitet werden. Da das aber sehr ähnlich zu den Vektoren ist, wird das hier nicht gezeigt. Im Anhang ist aber der gesamte Code und auch ein Beispiel dafür zu sehen. 3.7. Fazit Es wurde in dieser Seminararbeit eine Erweiterung vorgestellt, die das Typsystem von Haskell mit der Hilfe von SMT-Solvern mächtiger macht. Es wurden zunächst wichtige Grundlagen erläutern und dann im Hauptteil die Erweiterung und ihre Implementierung dargelegt. Anhand eines Beispiels wurde gezeigt, was mit dieser Erweiterung gemacht werden kann. Die Erweiterung ist eine durchaus sinnvolle Ergänzung für das Typsystem von Haskell. Sie hat keine wirklichen Nachteile, da sie das type-checking von GHC nur mächtiger macht. Es werden einem also nur weitere Möglichkeiten gegeben. Dass diese Möglichkeiten schnell notwendig werden, kann an den Beispielen aus der Einleitung gesehen werden. Selbst unkomplizierte Funktionen mit natürlichen Zahlen werden ohne die Erweiterung nicht akzeptiert. Bisher wird zwar nur die Theorie über natürliche Zahlen angeboten, aber selbst damit können schon sehr schöne Dinge implementiert werden. Weitere Theorien würden die Erweiterung dann noch mächtiger machen. Das Programmieren mit Datentypen, die natürliche Zahlen im Typ nutzen, ist interessant. Es ermöglicht eine weiteren Grad an Sicherheit rein durch das Typsystem. Funktionen, die zuvor eine Fehlerbehandlung benötigten oder schlicht für manche Werte nicht deniert waren, können Werte, für die die Funktion nicht deniert ist, durch die Signatur ausschlieÿen. Das macht das Programmieren solcher Funktion um einiges schöner, da nicht über falsche Werte nachgedacht werden muss. Es ist lediglich manchmal 24 3. Verbesserung von Haskell-Typen mit SMT ein wenig schwerer, den Compiler davon zu überzeugen, dass eine Funktion auch wirklich korrekt ist. Wenn eine Funktion zum Beispiel verlangt, dass einer ihrer Werte mindestens Länge 1 hat und diese Funktion rekursiv aufgerufen wird, muss es klar gemacht werden, dass dieser Wert zuvor mindestens die Länge 2 hatte. An solche Dinge gewöhnt man sich allerdings schnell. Eine weitere Sache, die ein wenig komplizierter wird, ist der Input von Werten durch den Nutzer. An dieser Stelle ist es am schwersten dafür zu sorgen, dass alles richtig getypt ist, wie es am Beispiel zu sehen ist. Allerdings sorgt diese Art der Programmierung dafür, dass der Programmierer gezwungen wird, sich über die Eingaben genaue Gedanken zu machen, da das Programm sonst nicht kompiliert. Daher kann das auch als Vorteil gesehen werden. Dadurch kommt es auch dazu, dass die meiste Fehlerbehandlung schon am Anfang des Programms beim Input abgehandelt wird. Wenn die Typen an dieser Stelle stimmen und alle Funktionen gute Constraints haben, dann sollte es nicht mehr zu Fehlern kommen, die mit dem Wert eines Parameters zu tun haben. Insgesamt bietet diese Art der Programmierung sehr viele Vorteile und es wäre schön, wenn die Erweiterung noch ausgebaut und weitere Theorien implementiert werden. 25 A. Installationsanleitung Die Erweiterung kann sehr einfach installiert werden, indem sie bei GitHub1 heruntergeladen und dann per makefile installiert wird. Genutzt wird sie dann in Programmen, indem {-# OPTIONS_GHC -fplugin=TypeNatSolver #-} an den Anfang des Programmes gesetzt wird. Es muss dazu noch der SMT-Solver Z3 installiert werden. Dieser kann auch auch GitHub2 heruntergeladen werden. Die Installationsanweisungen benden sich in der README-Datei. 1 2 https://github.com/yav/type-nat-solver https://github.com/Z3Prover/z3 26 B. Beispiel Hier ist das komplette Beispiel, das in dieser Seminararbeit häug genutzt wurde. Es ist auch auf GitHub1 zu nden. {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE FlexibleInstances #-} {-# OPTIONS_GHC -fplugin=TypeNatSolver #-} module Example where import import import import GHC.TypeLits Data.Proxy Unsafe.Coerce Control.Monad vectorZipSumIO :: IO () vectorZipSumIO = do line1 ← getLine line2 ← getLine let readVec1 = (reads line1 :: [([Int], String)]) let readVec2 = (reads line2 :: [([Int], String)]) case (readVec1 , readVec2) of (((vec1list, _):_), ((vec2list,_):_)) → do case (length vec1list == length vec2list) of True → do let l = toInteger (length vec1list) let Just someNatL = someNatVal (l-1) case someNatL of SomeNat (_ :: Proxy n) → do let vec1 = vCreate (toUNat (NatProxy :: NatProxy n)) vec1list 0 let vec2 = vCreate (toUNat (NatProxy :: NatProxy n)) vec2list 0 print $ vSum $ vZipWith vec1 vec2 (+) _ → putStr "Fehler" _ → putStr "Fehler" matMultIO1 :: IO () matMultIO1 = do 1 https://github.com/mitall/typed-vectors-matrices 27 B. Beispiel m ← readLn :: IO Integer n ← readLn :: IO Integer p ← readLn :: IO Integer line1 ← getLine let mat1list = (read line1 :: [[Int]]) line2 ← getLine let mat2list = (read line2 :: [[Int]]) let Just someNatM = someNatVal (m-1) let Just someNatN = someNatVal (n-1) let Just someNatP = someNatVal (p-1) case (someNatM, someNatN, someNatP) of (SomeNat (_ :: Proxy m), SomeNat (_ :: Proxy n), SomeNat (_ :: Proxy p)) → do let natM = NatProxy :: NatProxy m let natN = NatProxy :: NatProxy n let natP = NatProxy :: NatProxy p let mat1 = mCreate (toUNat natM) (toUNat natN) mat1list 0 let mat2 = mCreate (toUNat natN) (toUNat natP) mat2list 0 print (mMult mat1 mat2) matMultIO2 :: IO () matMultIO2 = do line1 ← getLine let mat1list = (read line1 :: [[Int]]) line2 ← getLine let mat2list = (read line2 :: [[Int]]) let m = toInteger $ length mat1list let n = toInteger $ length (head mat1list) let p = toInteger $ length (head mat2list) let Just someNatM = someNatVal (m-1) let Just someNatN = someNatVal (n-1) let Just someNatP = someNatVal (p-1) case (someNatM, someNatN, someNatP) of (SomeNat (_ :: Proxy m), SomeNat (_ :: Proxy n), SomeNat (_ :: Proxy p)) → do let natM = NatProxy :: NatProxy m let natN = NatProxy :: NatProxy n let natP = NatProxy :: NatProxy p let mat1 = mCreate (toUNat natM) (toUNat natN) mat1list 0 let mat2 = mCreate (toUNat natN) (toUNat natP) mat2list 0 print (mMult mat1 mat2) data Vec :: ∗ → Nat → ∗ where Nil :: Vec a 0 Cons :: a → Vec a len → Vec a (1 + len) instance {-# OVERLAPPABLE #-} (Show a) => Show (Vec a n) where 28 B. Beispiel show Nil = "Nil" show (Cons a Nil) = show a show (Cons a xs) = (show a) ++ ", " ++ (show xs) type Matrix a m n = Vec (Vec a n) m instance {-# OVERLAPPING #-} (Show a) => Show (Vec (Vec a n) m) where show Nil = "" show (Cons vec xs) = show vec ++ "\n" ++ show xs data UNat :: Nat → ∗ where UZero :: UNat 0 USucc :: UNat n → UNat (1 + n) instance Show (UNat n) where show UZero = "UZero" show (USucc x) = "USucc " ++ (show x) data NatProxy (n :: Nat) = NatProxy toUNat :: KnownNat n => NatProxy n → UNat n toUNat p = convert (natVal p) where convert :: Integer → UNat m convert 0 = unsafeCoerce $ UZero convert x = unsafeCoerce $ USucc $ convert $ x - 1 -- Creates a vector of length (n+1) where the elements are -- taken from the list -- When the list is empty it will fill -- the rest with the third argument vCreate :: UNat n → [a] → a → Vec a (n+1) vCreate UZero (x:_) _ = (Cons x Nil) vCreate UZero [] a = (Cons a Nil) vCreate (USucc rest) (x:xs) a = (Cons x $ vCreate rest xs a) vCreate (USucc rest) [] a = (Cons a $ vCreate rest [] a) -- Creates a matrix of size (m+1) x (n+1) -- where the elements are taken from the lists -- When a list is empty it will fill the rest -- with the fourth argument mCreate :: UNat m → UNat n → [[a]] → a → Matrix a (m+1) (n+1) mCreate UZero nat (xs:_) a = Cons (vCreate nat xs a) Nil mCreate UZero nat [] a = Cons (vCreate nat [] a) Nil mCreate (USucc rest) nat (xs:xss) a = Cons (vCreate nat xs a) $ (mCreate rest nat xss a) mCreate (USucc rest) nat [] a = Cons (vCreate nat [] a) $ (mCreate rest nat [] a) -- Vectorfold 29 B. Beispiel vFoldr :: (a → b → b) → b → Vec a n → b vFoldr _ b Nil =b vFoldr f b (Cons a xs) = f a (vFoldr f b xs) -- Vectorsum vSum :: Num a => Vec a l → a vSum vec = vFoldr (+) 0 vec -- Vectorlength as Integer vLength :: KnownNat n => Vec a n → Integer vLength vec = natVal vec -- Vectorlength as UNat vLengthU :: KnownNat n => Vec a n → UNat n vLengthU _ = toUNat (NatProxy :: NatProxy n) -- Append two vAppend :: Vec vAppend Nil l vAppend (Cons vectors a n → Vec a m → Vec a (n + m) =l x xs) ys = (Cons x (vAppend xs ys)) -- Vector map vMap :: Vec a n → (a → b) → Vec b n vMap Nil _ = Nil vMap (Cons x xs) f = Cons (f x) (vMap xs f) -- zipWith for vectors vZipWith :: Vec a n → Vec b n → (a → b → c) → Vec c n vZipWith Nil Nil _ = Nil vZipWith (Cons x xs) (Cons y ys) f = Cons (f x y) (vZipWith xs ys f) -- Split a vector at position nat so that the -- first vector has the first nat elements -- and the second vector has the rest vSplitAt :: (1 <= l, nat <= l, (nat + n) ~ l) => Vec a l → UNat nat → (Vec a nat, Vec a n) vSplitAt (Cons x xs) (USucc UZero) = ((Cons x Nil), xs) vSplitAt (Cons x xs) (USucc (USucc rest)) = let (left, right) = vSplitAt xs (USucc rest) in ((Cons x left), right) -- get the vNth :: ((m vNth (Cons vNth (Cons nth element of a vector + 1) <= n) => Vec a n → UNat m → a x _) UZero = x _ xs) (USucc rest) = vNth xs rest -- reverse a vector vReverse :: Vec a n → Vec a n vReverse Nil = Nil 30 B. Beispiel vReverse (Cons x xs) = vAppend (vReverse xs) (Cons x Nil) -- zipWith for matrices mZipWith :: Matrix a m n → Matrix b m n → (a → b → c) → Matrix c m n mZipWith xss yss f = vZipWith xss yss (\xs ys → vZipWith xs ys f) -- Matrix map mMap :: Matrix a m n → (a → b) → Matrix b m n mMap xss f = vMap xss (\xs → vMap xs f) -- Add two matrices mAdd :: (Num a) => Matrix a m n → Matrix a m n → Matrix a m n mAdd xss yss = mZipWith xss yss (+) -- Get the nth row of a matrix mNthRow :: ((l + 1) <= m) => Matrix a m n → UNat l → Vec a n mNthRow = vNth -- Get the nth column of a matrix mNthCol :: ((l + 1) <= n) => Matrix a m n → UNat l → Vec a m mNthCol Nil nat = Nil mNthCol (Cons row rows) nat = Cons (vNth row nat) (mNthCol rows nat) -- Matrix Multiplication mMult :: forall a m n p ◦ (Num a, 1 <= m, 1 <= n, 1 <= p) => Matrix a m n → Matrix a n p → Matrix a m p mMult m n = let nTrans = (mTranspose n) in vMap m (\vec → mMult' vec nTrans) where mMult' :: Vec a n → Matrix a q n → Vec a q mMult' vec Nil = Nil mMult' vecA (Cons vecB vecs) = Cons (vSum (vZipWith vecA vecB (∗))) (mMult' vecA vecs) -- Cut the first row from a matrix mTail :: Matrix a m n → Matrix a m (n - 1) mTail (Cons (Cons a xs) Nil) = (Cons xs Nil) mTail (Cons (Cons a xs) ys) = Cons xs (mTail ys) -- Transpose a matrix mTranspose :: (1 <= m, 1 <= n) => Matrix a m n → Matrix a n m mTranspose mat@(Cons (Cons _ Nil) _) = (Cons (mNthCol mat UZero) Nil) mTranspose mat@(Cons (Cons _ (Cons _ _)) _) = Cons (mNthCol mat UZero) (mTranspose (mTail mat)) --------------------------Test Data-------------------------------vectorOne = (Cons 3 (Cons 2 (Cons 1 (Cons 7 31 B. Beispiel (Cons 9 (Cons 3 (Cons 8 Nil))))))) = (Cons 3 (Cons 8 (Cons 2 (Cons 4 (Cons 8 (Cons 1 Nil)))))) (vectorThree, vectorFour) = vSplitAt vectorOne (toUNat (NatProxy :: NatProxy 3)) (vectorFive, vectorSix) = vSplitAt vectorTwo (toUNat (NatProxy :: NatProxy 2)) vectorSeven = vAppend vectorThree vectorSix --Length 7 vectorEight = vAppend (vAppend vectorFour vectorFive) (Cons 1 Nil) vectorSum = vSum (vZipWith vectorSeven vectorEight (+)) vectorTwo mVectorOne = Cons 3 (Cons 2 (Cons 1 Nil)) mVectorTwo = Cons 1 (Cons 0 (Cons 2 Nil)) mVectorThree = Cons 1 (Cons 2 Nil) mVectorFour = Cons 0 (Cons 1 Nil) mVectorFive = Cons 4 (Cons 0 Nil) matrixOne = Cons mVectorOne (Cons mVectorTwo Nil) matrixTwo = Cons mVectorThree (Cons mVectorFour (Cons mVectorFive Nil)) matrixMult = mMult matrixOne matrixTwo 32 Literaturverzeichnis [BCD+ 11] [Dia15] Barrett, Clark ; Conway, Christopher L. ; Deters, Morgan ; HadaLiana ; Jovanovi¢, Dejan ; King, Tim ; Reynolds, Andrew ; Tinelli, Cesare: CVC4. In: Proceedings of the 23rd International Conference on Computer Aided Verication. Berlin, Heidelberg : Springer-Verlag, 2011 (CAV'11). ISBN 9783642221095, 171177 rean, Iavor S.: Improving Haskell Types with SMT. In: Proceedings of the 2015 ACM SIGPLAN Symposium on Haskell. New York, NY, USA : Diatchki, ACM, 2015 (Haskell '15). ISBN 9781450338080, 110 [DMB08] De Moura, Leonardo ; Bjørner, Nikolaj: Z3: An Ecient SMT Solver. In: Proceedings of the Theory and Practice of Software, 14th International Conference on Tools and Algorithms for the Construction and Analysis of Systems. Berlin, Heidelberg : Springer-Verlag, 2008 (TACAS'08/ETAPS'08). ISBN 3540787992, 9783540787990, 337340 [VPjSS11] Vytiniotis, Dimitrios ; Peyton jones, Simon ; Schrijvers, Tom ; Sulzmann, Martin: Outsidein(x) Modular Type Inference with Local Assumptions. In: J. Funct. Program. 21 (2011), September, Nr. 45, 333412. http://dx.doi.org/10.1017/S0956796811000098. DOI 10.1017/S0956796811000098. ISSN 09567968 [YWC+ 12] Yorgey, Brent A. ; Weirich, Stephanie ; Cretin, Julien ; Peyton Jones, Simon ; Vytiniotis, Dimitrios ; Magalhães, José P.: Giving Haskell a Promotion. In: Proceedings of the 8th ACM SIGPLAN Workshop on Types in Language Design and Implementation. New York, NY, USA : ACM, 2012 (TLDI '12). ISBN 9781450311205, 5366 33