Verbesserung von Haskell-Typen mit SMT

Werbung
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
Herunterladen