Erweiterungen des Typsystems von Haskell

Werbung
Erweiterungen des Typsystems von Haskell
Fortgeschrittene Konzepte der funktionalen Programmierung
Daniel Stüwe
Fakultät für Informatik
Technische Universität München
E-Mail: [email protected]
12. Juni 2015
Diese Seminararbeit stellt eine Auswahl an Erweiterungen des Haskell-2010Typsystems anhand von Beispielen vor. Der Schwerpunkt liegt dabei auf
der Befähigung des Lesers, Real-World-Haskell-Code, der diese Extensions
benutzt, nachvollziehen zu können. Theoretische Fundamente und Konsequenzen der Veränderungen des ursprünglichen Systems werden nicht näher
behandelt, insofern sie den Zweck der Programmierung nicht tangieren.
Extensions in diesem Dokument: GADTs, ExistentialQuantification, MultiParamTypeClasses, FunctionalDependencies, UndecidableInstances, FlexibleInstances, DataKinds, KindSignatures, PolyKinds, TypeFamilies, TypeOperators, ScopedTypeVariables, StandaloneDeriving.
1 Einleitung
Haskells Typsystem basiert auf dem let-Polymorphismus des Inferenzverfahrens nach
Hindley und Milner (HM ), welcher wiederum eine Teilmenge an Typen des Systems F
erlaubt.1
GHC verwendete schon früh intern System Fω mit „Extras“ (insbesondere algebraischen
Datentypen), welches Typfunktionen und damit eine größere Vielfalt an Kinds erlaubt
als HM. 2006 wurde dieses jedoch durch das System FC ersetzt, das die Möglichkeit
hinzufügt, type constraints als Nutzer direkt im Quellcode anzugeben. Eine ausführlichere Beschreibung kann man in [Eis13; Sul+07] finden. Dies war eine Zäsur in der
Entwicklung von Erweiterungen für Haskell, denn so wurde der Weg frei, um die Inferenzfähigkeiten des GHC-Type-Checkers vollständig ausnutzen zu können.
In dieser Seminararbeit wird eine Auswahl an Erweiterungen des Haskell-Typsystems
anhand von Beispielen vorgestellt. Der Fokus liegt dabei ganz klar auf der Befähigung
des Lesers, Real-World-Haskell-Code, der diese Extensions benutzt, nachvollziehen zu
können. Es werden dafür allerdings sehr gute Haskell-Grundkenntnissen vorausgesetzt.
Insbesondere mit den Konzepten hinter Typklassen, Datentypen und Typinferenz sollte
der Leser bereits vertraut sein.
Aktiviert werden Spracherweiterungen durch das im Haskell-2010-Report definierte Pragma {-# LANGUAGE NameOfExtension #-} . Sämtlicher Code wurde mit GHC 7.8.3 erfolgreich kompiliert.
Tabellarischer Überblick über die durch die jeweilige Extension
freigeschaltete Funktionalität in knappen Stichpunkten
GADTs
ExistentialQuantification
MultiParamTypeClasses
FunctionalDependencies
FlexibleInstances
UndecidableInstances
DataKinds
KindSignatures
PolyKinds
TypeFamilies
TypeOperators
ScopedTypeVariables
StandaloneDeriving
1
Generalisierte algebraische Datentypen, ermöglichen eine
genauere Deklaration von Datentypen
Interessanter Spezialfall der GADTs
Deklaration von Typklassen mit mehreren Parametern
Deklaration funktionaler Abhängigkeiten zwischen Typklassenparametern; aktiviert MultiParamTypeClasses
Entfernt Einschränkungen bei der Deklaration von Typklasseninstanzen, garantiert dabei dennoch weiterhin, dass
Typüberprüfung terminiert
Erlaubt Typklasseninstanzen, deren Überprüfung gegebenenfalls nicht mehr terminiert
Kompakte Deklaration von Kinds und zugehörigen Typen
Annotation von Kindsignaturen an Typvariablen
Typvariablen polymorphen Kinds durch explizite Signatur
Vielseitig, im Kern Definition von Typfunktionen
Syntaktische Erweiterung, Infixnotation für TypeFamilies
Vergrößert Scope von Typvariablen aus der Typsignatur in
den Funktionsrumpf mittels explizieter Quantifizierung
Erweitert den Automatismus zur Herleitung von Instanzen
Es ist für das Verständnis dieser Arbeit nicht notwendig, diese und nachfolgend genannte Kalküle zu
kennen. Sie stellen dennoch die theoretische Basis für viele Erweiterungen dar und sollen deswegen
nicht unerwähnt bleiben. Der interessierte Leser kann sich in [Pie02] umfassend damit beschäftigen.
Eine kürzere Darstellung findet man natürlich auch hier [Wik15c; Wik15e].
1
2 Container
2.1 Funktionale Abhängigkeiten
Im ersten Abschnitt wenden wir uns einem Beispiel aus [McB02] zu. Dortige Problemstellung: Entwurf einer Typklasse für beliebige Datenstrukturen, die das sequentielle
Hinzufügen von Elementen unterstützen, sowie die Möglichkeit bieten, zu überprüfen,
ob ein Element bereits enthalten ist. Monomorphe Datentypen sollen dabei auch instanziiert werden können. Ein erster Versuch mit Mitteln des Haskell-2010-Standards:
class Container’ c where
insert’ :: e -> c -> c
member’ :: e -> c -> Bool
Versucht man jedoch eine eine Instanz für ByteString, einem sehr effizientem Container für Word8-Sequenzen, zu registrieren, wird GHC dies natürlich mit einer Fehlermeldung zurückweisen, da BS.cons den zu speziellen Typ Word8 -> ByteString ->
ByteString und nicht, wie gefordert, e -> ByteString -> ByteString hat.
import qualified Data.ByteString as BS
import Data.Word
instance Container’ BS.ByteString where
insert = BS.cons
member = BS.elem
Ganz offensichtlich ist der Elementtyp, der eingefügt werden kann, vom Containerdatentyp abhängig. Eine Reihe von historisch gewachsenen Erweiterungen ermöglicht uns,
diese Informationen in unserer Typklasse abzubilden und zugehörige Instanzen zu registrieren [GHC15c; Pal08].
{-# LANGUAGE
MultiParamTypeClasses,
FunctionalDependencies,
FlexibleInstances
#-}
MultiParamTypeClasses erlaubt uns, Typklassen mit mehr als einer Typvariablen zu
schreiben und mittels FunctionalDependencies lässt sich dann eine Beziehung zwischen
den Typvariablen deklarieren. Das Konzept stammt aus der Theorie relationaler Datenbanken, wo ursprünglich funktionale Abhängigkeiten in Relationenschemata beschrieben
wurden.
class Container ce e | ce -> e where
insert :: e -> ce -> ce
member :: e -> ce -> Bool
instance Container BS.ByteString Word8 where ...
Nun akzeptiert der Compiler diesen Code ohne Probleme, da ByteString explizit mit
Word8 als Elementdatentyp registriert wird. Ohne den Zusatz ce -> e kann es Auflösungskonflikt geben. Beispielsweise würde insert ’a’ [], Instanz siehe unten, nicht
kompilieren. Wegen der Rechtseindeutigkeit von Funktionen [Wik15d], die durch die
funktionale Abhängigkeit gegeben wird, kann GHC die Typen dann doch herleiten, da
jedem ce genau ein e zugeordnet wird.
2
FlexibleInstances wird in diesem Zusammenhang oft benötigt, da Standardhaskell sehr
restriktiv handhabt, welche Constraints für die Instanzen aufgestellt und wie Typvariablen benutzt werden dürfen. Beispielsweise ist es ohne FlexibleInstances, wie nachfolgend, nicht erlaubt, eine Variable zweimal zu verwenden.
instance Eq a => Collection [a] a where
insert = (:)
member = elem
2.2 TypeFamilies
Eine neuere Herangehensweise ist durch das System Fω selbst motiviert. Mit dem Pragma {-#LANGUAGE TypeFamilies #-} kann diese umfangreiche Erweiterung freigeschaltet werden, auf die wir im weiteren Verlauf noch öfter eingehen werden, da sie in mehreren Varianten vorkommt. Sie ermöglicht uns prinzipiell, eigene Typfunktionen festzulegen.
Wir benutzen sie hier in ihrer einfachsten Form, um lediglich in der Klasse Container
den Elementtyp abspeichern zu können. Elem kommt dabei einer Funktion auf Typebene
gleich, die den Instanzentyp auf den Elementtyp abbildet. Sie ist jedoch partiell, da
natürlich nicht jeder Typ Instanz dieser Typklasse sein kann.
Nachfolgend bildet Elem also ByteString auf Word8 ab und kann zur Deklaration der
Funktionssignaturen genutzt werden.
class Container cr where
type Elem
insert :: Elem -> cr -> cr
member :: Elem -> cr -> Bool
instance Container ByteString where
type Elem = Word8
insert
= BS.cons
member
= BS.elem
3 Maps
Eine Variation der vorherigen Problemstellung ist, eine Typklasse für Maps2 zu erstellen,
sodass es möglich ist, auf den Schlüsseltyp spezialisierte Strukturen zu verwenden, um
zugeordnete Werte effizient ablegen und finden zu können.
Auch hier benutzen wir wieder TypeFamilies. Aber diesmal stellt Map k nicht nur ein
simples Synonym dar, sondern verlangt, einen neuen Datentyp in der Instanz zu konstruieren. So kann dort sämtlicher Code gebündelt werden, der für ihre Implementierung
notwendig ist.
class MapWithKey key where
data Map key val
empty :: Map key val
insert :: k -> v -> Map key val -> Map key val
lookup :: k -> Map key val -> Map key val
Da Bool lediglich zwei Werte beinhaltet, mithin die Größe der Map endlich ist, kommt
der Datentyp Map Bool ohne rekursive Felder aus. (Ob es überhaupt sinnvoll ist, Bool
als Schlüssel zu verwenden, ist für diese Beispielinstanz nebensächlich.)
2
Datentypen, die Schlüssel-Werte-Paare speichern
3
instance MapWithKey Bool where
data Map Bool val
= MkBoolMap (Maybe val) (Maybe val)
= MkBoolMap Nothing Nothing
empty
insert key val (MkBoolMap t f)
| key
-> MkBoolMap (Just val) f
| otherwise -> MkBoolMap t (Just val)
lookup True (MkBoolMap t _) = t
lookup _
(MkBoolMap _ f) = f
4 Domänenspezifische Sprachen
4.1 Problembeschreibung
Funktionale Programmiersprachen bieten sich an, wenn man eine domänenspezifische
Sprache3 als Prototypen implementieren möchte. Das Schreiben eines Interpreters wird
nämlich dadurch erleichtert, dass Unterschiede zwischen Daten und lauffähigem Code
nicht so stark ausgeprägt sind. (Konstruktoren eines Datentyps sind schließlich auch
Funktionen u. Ä.)
Betrachten wir stellvertretend sehr einfache Terme über den ganzen Zahlen und booleschen Werten.
data Expr =
|
|
|
|
|
I Int
B Bool
Add Expr
Mul Expr
Eq Expr
Not Expr
deriving
-- Konstanten
Expr
Expr
Expr
-- Operatoren
(Read, Show)
Da es einen einfachen Parser und Pretty-Printer in Gestalt von automatischen Readund Show-Instanzen gratis zur Deklaration dazu gibt, verbleibt bloß noch der Interpreter
zu programmieren. Ein erster Versuch könnte eval :: Expr -> Either Int Bool vorsehen. Da es jedoch möglich ist, nicht sinnvolle Terme wie B True ‘Add‘ I 5 :: Expr
zu schreiben, muss die Signatur angepasst werden, insofern man eine totale Funktion
erhalten möchte:
:: Expr -> Maybe (Either Int Bool)
(I n)
= Just $ Left n
(B b)
= Just $ Right b
(Add a b) = case (eval a, eval b) of
(Just (Left i), Just (Left j)) -> Just $ Left (i+j)
_
-> Nothing
eval’ (Mul a b) = ...
eval’ (Eq a b) = ...
eval’ (Not a)
= ...
eval’
eval’
eval’
eval’
3
Sprachen, die speziell für ein Einsatzgebiet entworfen wurden, im Gegensatz zu General Purpose Languages. Beispiele sind SVG, SQL, RegExs etc.
4
Nicht nur ist dieser Code überladen mit Fallunterscheidungen, diese setzen sich auch
noch auf natürliche Weise in Folgefunktionen fort. Ursprung dieses Dilemmas scheint zu
sein, dass Expr eigentlich sowohl von der Art Int als auch Bool sein kann und dies nicht
aus dem Typ ableitbar ist. Entsprechend akzeptiert read auch unzulässige (im Sinne
der Interpreterfunktion) Eingaben.
4.2 Phantomtypen
Folglich ist es eine gute Idee, den Datentyp so abzuändern, dass man sofort erkennt, ob
es sich hierbei um einen Expr Int oder Expr Bool handelt, indem man eine Typvariable
ausschließlich dafür hinzufügt. Man nennt a in Expr a einen Phantomtyp [Wik15b], da
in keinem Konstruktor ein Wert vom Typ a gespeichert wird.
data Expr a =
|
|
|
|
|
I Int
B Bool
Add (Expr
Mul (Expr
Eq (Expr
Not (Expr
Int) (Expr Int)
Int) (Expr Int)
Int) (Expr Int)
Bool)
Auf den ersten Blick scheinen wir am Ziel, doch haben die Konstruktoren nicht ganz den
gewünschten Typ (Add :: Expr Int -> Expr Int -> Expr a). Man mag einwenden,
dass man dies mit smart constructors beheben könnte.
add :: Expr Int -> Expr Int -> Expr Int
add = Add
Nun würde GHC zu b True ‘add‘ i 5, wobei b und i auch smart constructors sind,
einen Typfehler ausgeben. Grundsätzlich hilft uns dies nicht für unsere Interpreterfunktion weiter, da man auf smart constructors kein pattern matching durchführen kann.
4.3 GADTs
Genau dort setzen Generalized Algebraic Datatypes (GADTs) an. Sie erlauben es, den
Typ eines Konstruktors selbst festzulegen, sozusagen den Standardkonstruktor gleich
durch seine smarte Version zu ersetzen und damit pattern matching auf ihm ausführen
zu dürfen. Listen sehen dabei in der GADT -Syntax wie nachstehend aus:
{-# LANGUAGE GADTs #-}
data List a where
Nil :: List a
Cons :: a -> List a -> List a
Dieses Konzept stellt sich als eine gute Lösung für unser Problem heraus. Es erlaubt
uns, im Datentyp gleich das Ergebnis eines einzelnen Konstruktors zu spezifizieren.
data Expr
I
::
B
::
Add ::
Mul ::
Eq ::
Not ::
a where
Int -> Expr
Bool -> Expr
Expr Int ->
Expr Int ->
Expr Int ->
Expr Bool ->
Int
Bool
Expr
Expr
Expr
Expr
Int -> Expr Int
Int -> Expr Int
Int -> Expr Bool
Bool
5
Nun ist es nicht mehr möglich, unsinnige Terme zusammenzusetzen und die Fallunterscheidungen können eliminiert werden. Die Interpreterfunktion sieht entsprechend eleganter aus. Aus einem Programm, das keine offensichtlichen Fehler hatte, wurde eines,
das offensichtlich keine Fehler hat.
eval
eval
eval
eval
eval
eval
eval
:: Expr a -> a
(I n) = n
(B b) = b
(Add e1 e2) = eval e1 +
(Mul e1 e2) = eval e1 *
(Eq e1 e2) = eval e1 ==
(Not e1)
= not $ eval
eval e2
eval e2
eval e2
e1
5 Heterogene Listen - Existentiell Quantifiziert
GADTs erlauben dem Programmierer (fast) jeden erdenklichen Funktionstyp anzugeben.
Es ist unter anderem auch möglich Typvariablen einzuführen, die nicht im Datentyp
deklariert sind.
data Showable’ where Box’ :: Show s => s -> Showable’
Man nennt diese Typen existentiell quantifiziert. Man weiß, dass der Wert im Konstruktorfeld mindestens einen Typ hat – er existiert.4 (Hier mit der Bedingung, Instanz von
Show zu sein.5 ) Das Beispiel ist [Wik15a] entnommen.
Existentielle Typen sind bereits vor GADTs in Haskell eingeführt worden und wurden
schon sehr früh in der Typtheorie untersucht, siehe [Pie02]. Sie können daher separat
aktiviert werden. Darüber hinaus besteht ein enger Zusammenhang mit RankNTypes,
auf den wir hier leider nicht genauer eingehen können.
{-# LANGUAGE ExistentialQuantification #-}
data Showable = forall s . Show s => Box s
In diesem Fall gilt, dass Showable ∼
= Showable’.
Wir können für Showable auch eine Show-Instanz registrieren. Da wir ja schließlich vom
verborgenem Typ wissen, dass er das Constraint Show erfüllt.
instance Show Showable where show (Box s) = show s
Man kann nun den Typ eines Wertes abstrahieren, indem man Box :: Show s => s
-> Showable auf ihn anwendet. So können Werte mit ursprünglich unterschiedlichen
Typen, aber gleicher Abstraktion, in eine Liste einfügt werden.
ghci > let xs = [Box 1, Box True, "Hello World"]
ghci > show xs
"[1,True,\"Hello World\"]"
4
5
Formal: (∀s . P (s) ⇒ Q) ⇐⇒ ((∃s . P (s)) ⇒ Q)
Also P (s) = s ∈ Show
6
6 ADTs durch existentielle Typen
Ein interessanter Aspekt existentieller Typisierung ist, dass sie Built-In-Unterstützung
für abstrakte Datentypen überflüssig werden lässt [Pie02]. Das zeigt, dass Typsysteme nicht nur dafür geeignet sind, Fehler beim sogenannten Programmieren im Kleinen
aufzuspüren, sondern auch zur Datenkapselung verwenden werden können. Der anschließende untersuchte Typ ist [GHC15d] entliehen.
data Counter =
_this
_inc
_display
}
forall self. MkCounter {
:: self,
:: self -> self,
:: self -> IO ()
Dabei hat MkCounter den etwas länglichen Typ a -> (a -> a) -> (a -> IO ()) ->
Counter. Man erkennt, dass die Tyvariable a in Counter verborgen wird und damit
unzugänglich ist, für den Benutzer dieses Pseudoobjektes.
Um ihm die Nutzung des Datentyps zu erleichtern, kann man die nötigen Wrapperfunktionen bereitstellen, die auf die internen Datenfelder zugreifen. Diese sind etwas umständlicher, da hier auf den Records nur mit pattern matching gearbeitet werden darf
und direkte Ersetzungen wie in inc’ counter@(MkCounter this incr _) = counter
{ _this = incr this} untersagt sind.
inc :: Counter -> Counter
inc (MkCounter this incr display) =
MkCounter { _this = incr this,
_inc = incr,
_display = display }
display :: Counter -> IO ()
display (MkCounter t _ display) = display t
Exemplarisch seien hier zwei Counterwerte definiert. Es ist für einen Anwender, der keinen Zugang zum Code von counterA oder counterB hat, nicht möglich herauszufinden,
welche interne Repräsentation verwendet wird.
counterA = MkCounter {
_this = 0,
_inc = (1+),
_display = putStrLn $
replicate _this ’#’
}
counterB = MkCounter {
_this = "",
_inc = (’#’:),
_display = putStrLn
}
ghci > display (inc counterA)
#
ghci > display (inc counterB)
#
7 Physikalische Einheiten
7.1 Wiederholung: Kinds
Die vorherigen Abschnitte haben sich damit beschäftigt, in welchen Beziehungen Typen
in Klassen zueinander stehen und wie man Typinformationen hinzufügt oder verbirgt.
7
In dieser Passage wird es darum gehen, gänzlich neue Typen einzuführen. In Haskell2010 kann dies nur durch data DataTypeName oder newtype NewTypeName geschehen.
Man kann Typen anhand ihres Kinds 6 unterscheiden. Befragt man GHC zu den Kinds
bestimmter Typen,
ghci >
Int ::
ghci >
Int ->
:kind Int
*
:kind Int -> Int
Int :: *
ghci > :kind Maybe
Maybe :: * -> *
ghci > :kind Maybe (Int -> Int)
Maybe :: *
erhält man teils bemerkenswerte Antworten. Maybe ist eine Typfunktion, die noch ein
Typargument erwartet, stellt selbst aber keinen Typ dar, der als Ergebnis einer Funktion
zurückgeben werden kann. Nur Typen vom Kind * haben Laufzeitwerte!
7.2 Motivation
Es gibt zahlreiche Unfälle, teilweise mit beträchtlichen Sach- und auch Personenschaden,
die darauf zurückzuführen sind, dass falsche Annahmen über Einheiten im Code getroffen wurden. Beispielsweise kann vergessen worden sein, Werte eines Messgerätes, das
nicht mit SI-Einheiten arbeitet, zu konvertieren, oder man hat versehentlich die falsche
Umrechnungsfunktion aufgerufen usw.
7.3 Implementierung
Die Erweiterung DataKinds promotet (engl. to promote, „erhöhen“) alle Datentypen im
Scope (und damit insbesondere auch die Datentypen des Prelude).
data LengthUnit = Kilometer | Mile
Das bedeutet an diesem Beispiel anschaulich, dass Kilometer und Mile nun Haskelltypen sind vom Kind LengthUnit!
Diese neuen, leeren7 Typen können wir nun benutzen, um einen Datentyp zu bilden
(Distance), der sowohl die Länge als Gleitkommazahl, als auch die Einheit speichert.
Mit KindSignatures können wir sicherstellen, dass Distance nur mit passenden Einheiten versehen wird, indem wir die entsprechende Kindsignatur an die Typvariable
annotieren.
data Distance (l :: LengthUnit) = Distance Double
Unsere Herangehensweise hat sowohl einen dokumentarischen Zweck – man erkennt am
Typen sofort die Einheit, vor allem jedoch wird bereits zur Kompilierzeit die Einheitenüberprüfung durchgeführt. (Also deutlich vor dem Start eines Flugzeuges etc.) Würde
eine Funktion Distance Mile von einem Argument verlangen, wäre marathonDistance
unzulässig. Ebenso kann auf Distance Mile nur einmal die Kovertierungsfunktion
kmToMiles angewendet werden.
marathonDistance :: Distance Kilometer
marathonDistance = Distance 42.195
kmToMiles :: Distance Kilometer -> Distance Mile
kmToMiles (Distance km) = Distance (0.621371 * km)
6
7
Zur Erinnerung: Das sind quasi Typen der Typen.
Typen ohne Laufzeitwert
8
8 Traditionelle Typarithmetik
8.1 Motivation
Die wohl häufigste und gleichzeitig eine der gefährlichsten Ursachen von Programmfehlern ist der unzulässige Speicherzugriff. Es besteht daher großes Interesse, diesen bereits
bei der Programmübersetzung festzustellen und somit zu verhindern. In diesem und dem
nächsten Abschnitt werden dazu Werkzeuge der Programmierung mit dependent types
vorgestellt, die benutzt werden können, um dies zu ermöglichen.
Es gibt in Haskell zwei Weisen um dependent types zu erhalten. Die erstere nutzt dabei MultiParamTypeClasses, FunctionalDependencies, FlexibleInstances und UndecidableInstances. Letztere Erweiterung ist immer dann notwendig, wenn die Typüberprüfung
möglicherweise nicht mehr terminiert. Es wird in diesem Abschnitt auf die Verwendung
von DataKinds verzichtet, da es eine Erweiterung einer neuerer Generation ist und hier
in das „klassische“ System eingeführt werden soll.
8.2 Grundlagen
Als einfaches, aber sehr wichtiges Beispiel sollen hier natürliche Zahlen (Modellierung
nach Peanos Axiomen) auf Typebene entwickelt werden [Wik13; KJS10]. Dazu beginnen
wir mit folgenden Definitionen:
data Zero
= Z
data Succ a = S a
class Nat a
instance Nat Zero
instance Nat n => Nat (Succ n)
Die erste Instanz ist so zu lesen, dass Zero eine natürliche Zahl ist. Die zweite bedeutet,
falls n natürlich ist, dann ist es ihr Nachfolger Succ n ebenso. Soweit, so gewöhnlich.
Man könnte type Two = Succ (Succ Zero) abkürzend für Zwei festlegen.
Einen erster, naiver Versuch, einen sicheren sequentiellen Datentypen zu definieren, sähe
womöglich in etwa so aus:
data Vector a n where Vector :: Nat n => [a] -> Vector a n
Man wird aber recht schnell feststellen, dass diese Implementierung unzureichend ist,
da der Zusammenhang zwischen n und der Länge von [a] nicht festgeschrieben wird. n
hätte also nur einen rein dokumentierenden Charakter.
8.3 Typfunktionen mittels Typklassen und FunctionalDependencies
Wenden wir uns also zunächst wieder den Zahlen zu und nun einem sehr einfachem,
einführenden Beispiel ins Type-Level-Programming in Haskell.
class Nat n => Even n
class Nat n => Odd n
instance Even Zero
instance Even n => Odd (Succ n)
instance Odd n => Even (Succ n)
Der Code ist wie folgend zu lesen: Es gibt gerade und ungerade Zahlentypen. Zero ist
gerade. Für jede gerade Zahl gilt, dass ihr Nachfolger ungerade ist und umgekehrt.
Der Typechecker wird diese Informationen jedoch in der umgekehrten Reihenfolge bearbeiten; gesucht ist der Nachweis dafür, dass n (Instanz von) Even ist, falls Odd (Succ
n) gelten soll usw.
9
Ein komplexeres Beispiel ist die Addition, die benötigt würde, wenn wir zwei Vektoren
aneinanderfügen wollten.
class TAdd a b c | a b -> c
instance TAdd Zero n n
instance TAdd n m k =>
TAdd (Succ n) m (Succ k)
Die Typklassendefition legt fest, dass die Typen a, b und c in der Beziehung TAdd stehen.
Typ a und b bestimmen dabei eindeutig den Typ c. Kurz: a plus b ergibt c.
Die erste „Gleichung“ besagt also, dass Zero plus n gleich n ergibt. Wir benötigen hier
FlexibleInstances, da n doppelt vorkommt.
Schlussendlich ist (Succ n) plus m gleich (Succ k), falls k das Ergebnis von n plus
m ist. Hier benötigen wir zusätzlich UndecidableInstances, da die sogenannte CoverageBedingung nicht erfüllt ist; denn GHC kann nicht mehr herleiten, dass k eindeutig durch
n und m bestimmt ist.
class TMult a b c | a b -> c
instance TMult Zero n Zero
instance (TMult n m k, TAdd m k k’) => TMult (Succ n) m k’
Die Multiplikation verfährt nach dem selben Prinzip.
Die letzte Instanz besagt, (Succ n) mal m ergibt k’, falls k’ gleich (n mal m) plus m ist.
9 Singletons
9.1 Einführung
Diese Art dependent types in Haskell zu gestalten, ist offensichtlich syntaktisch nicht
ideal. Auch das relationale Vorgehen ist ungewöhnlich für funktionale Programme. Mit
GHC 7.4 (2012) wurden daher einige, wichtige der nachfolgend verwendeten Extensions
vorgestellt.
{-# LANGUAGE DataKinds, KindSignatures, PolyKinds,
GADTs, StandaloneDeriving,
TypeFamilies, UndecidableInstances, FlexibleInstances #-}
Skizze des Vorgehens: Kreieren der Datentypen, die anschließend promotet werden und
die Funktionen werden direkt mittels TypeFamilies dargestellt.
Wie zuvor beschrieben sind allerdings nur Typen vom Kind * nicht leer, promotete
hingegen schon. Dies ist problematisch, falls wir zur Laufzeit doch einen Repräsentanten
des Typs benötigen. (Beispiel: für (!!) soll mittels des Typs vorab überprüft werden,
ob der Zugriff auf die n-te Position eines Vektors zulässig ist. Zur Laufzeit benötigt man
dann dieselbe Information, um die Liste zu traversieren.)
10
9.2 Grundlagen
Einen Ausweg bieten dabei Singletons [EW13].
data Nat = Zero | Succ Nat deriving Show
data family Sing (a :: k)
data instance Sing (n :: Nat) where
SZ :: Sing Zero
SN :: Sing n -> Sing (Succ n)
type SNat (n :: Nat) = Sing n
deriving instance Show (SNat n)
Sing (n :: k) ist eine GADT family. Man nennt sie kind indexed, da nur Typen eines
fixierten Kinds k sie instanziieren können, wobei k eine Kindvariable ist. Dies wird
ermöglicht durch PolyKinds.
Die anschließende Instanz beschreibt für die promoteten Typen des Kinds Nat ihren zugehörigen, einzigartigen Laufzeitstellvertreter (engl. Runtime-Witness). Daher die Bezeichnung Singleton. Zero wird hier bspw. auf Sing Zero abgebildet, deren einzig möglicher Wert SZ ist. Analog dazu wird Succ (Succ Zero) dem Typ Sing (Succ (Succ
Zero)) mit dem Laufzeitwert SN (SN SZ) zugewiesen. Die Eindeutigkeit folgt aus der
Konstruktion des GADTs.
Um einen Laufzeitrepräsentanten automatisch generieren zu können, benutzen wir die
Klasse SingI (singleton introduction). Dies ähnelt dabei dem Template Programming in
C++.
class SingI (a :: k) where
sing :: Sing a
instance SingI Zero where
sing = SZ
instance SingI n => SingI (Succ n) where
sing = SN sing
Man beachte, dass diese Erzeugung zur Kompilierzeit abläuft!
ghci > sing :: Sing (Succ (Succ Zero))
SN (SN SZ) :: SNat (Succ (Succ Zero))
Der Vollständigkeit halber sei hier die Klasse SingE (singleton elemination) angegeben.
Die Demotion führt den promoteten Kind auf seinen Ausgangstyp zurück. Dasselbe gilt
für die promoteten Typen, die dann zu Werten werden.
class SingE (a :: k) where
type Demote
fromSing :: Sing a -> Demote
11
instance SingE (a :: Nat) where
type Demote = Nat
fromSing SZ = Zero
fromSing (SN n) = Succ (fromSing n)
Dieser Code wird zur Laufzeit ausgeführt und ähnelt einem type erasure, da der Typ
von SN (SN SZ) detailreicher ist, als der des Ergebnisses Succ (Succ Zero).
ghci > fromSing $ SN (SN SZ)
Succ (Succ Zero) :: Nat
9.3 Funktionspromotion
Nun wenden wir uns der Promotion der zugehörigen Funktionen zu. Man benötigt diese,
um Tests durchzuführen (Ist der Zugriff an dieser Stelle möglich?) oder um Veränderungen in der Datenstruktur widerspiegeln zu können (Konkatenation zweier Listen). Wir
werden dazu auch die rein syntaktische Erweiterung TypeOperators verwenden, um eine
besonders intuitive Darstellung zu erreichen. Die Präzedenz des Operators wird dabei
praktischerweise aus dem Prelude übernommen.
type family (m :: Nat) + (n :: Nat) :: Nat where
Zero
+ n = n
Succ m + n = Succ (m + n)
Man sagt, dass (+) wohlgekindet ist, da durch die Kindannotation überhaupt nur sinnvolle Typen zugelassen sind. Dies ist auch ein wichtiger Vorteil gegenüber der althergebrachten Methode mit MultiParamTypeClasses und FunctionalDependencies, von der
Einfachheit durch den funktionalen Stil ganz abgesehen.
Durch DataKinds werden, wie bereits erwähnt, alle im Prelude befindlichen Datentypen
promotet, darunter natürlich auch Bool. So definiert man ohne Probleme:
type family (n :: Nat) < (m :: Nat) :: Bool where
n
< Zero
= False
Zero
< Succ m = True
Succ n < Succ m = n < m
Noch ein Stück abstrakter ist die nachstehende Typfunktion.
type family If (b :: Bool) (x :: k) (y :: k) :: k where
If True t f = t
If False t f = f
Man beachte ihren interessanten Kind.
ghci > :kind If
If :: Bool -> k -> k -> k
12
10 Vektoren
Dass dies alles nicht nur Spielerei ist, zeigt sich jetzt in diesem Abschnitt. Wir wollen
nun unser eigentliches Problem angehen, gerüstet mit Typzahlen, Vektoren zu programmieren. Dazu werden wir weiterhin die zahlreichen Erweiterungen aus dem vorherigem
Teil verwenden.
Zunächst zeigen wir noch eine primitivere, abgespeckte Version, in der wir lediglich
zwischen leeren und nicht-leeren Listen unterscheiden. Die Definition ist unkompliziert;
wir benutzen einfach einen Phantomtyp vom Kind Fullness, um den GADT NEList
(NonEmptyList) zu erweitern.
data Fullness = Empty | NonEmpty
data NEList a :: Fullness -> * where
Nil’ :: NEList a Empty
Cons’ :: a -> NEList a anyFullness
-> NEList a NonEmpty
Mit diesem Ansatz können einige partiellen Listenfunktionen des Prelude in eine totale Version konvertiert werden. Funktionen, die die Länge unverändert lassen, können
ebenfalls übernommen werden. Für take oder (!!) reichen die Informationen hingegen
nicht aus.
safeHead :: NEList a NonEmpty -> a
safeHead (Cons’ x _) = x
safeMap :: (a -> b) -> NEList a s -> NEList b s
Durch das Austauschen von Fullness durch Typen vom Kind Nat erhalten wir den
gesuchten Datentyp Vector.
data Vector a (n :: Nat) where
Nil :: Vector a Zero
Cons :: a -> Vector a m -> Vector a (Succ m)
deriving instance Show a => Show (Vector a n)
Um GHC etwas auf die Sprünge zu helfen, wie denn unser Datentyp darzustellen ist,
geben wir explizit an, welche der Typen überhaupt Instanz von Show sind. Wie zuvor
lassen sich simple Funktionen schnell konvertieren.
vecHead :: Vector a (Succ n) -> a
vecHead (Cons x _) = x
vecMap :: (a -> b) -> Vector a n -> Vector b n
vecMap _ Nil
= Nil
vecMap f (Cons x xs) = f x ‘Cons‘ sMap f xs
Aber auch jene, die die Länge des Vektors verändern! replicate’ ist dabei ein gutes
Beispiel für die Verwendung eines Laufzeitrepräsentanten. Wir benötigen die Zahleninformationen sowohl im Typ zur Kompilierzeit, als auch zur Laufzeit, wenn die Funktion
ausgeführt wird.
13
append :: Vector e n -> Vector e m -> Vector e (n + m)
append Nil ys
= ys
append (Cons x xs) ys = x ‘Cons‘ sApp xs ys
replicate’ :: SNat n -> a -> Vector a n
replicate’ SZ
_ = Nil
replicate’ (SN n) x = x ‘Cons‘ replicate n x
Die Erweiterung ScopedTypeVariables vergrößert den Gültigkeitsbereich explizit allquantifizierter Typvariablen über die Funktionssignaturen hinaus in den Rumpf hinein.
Das benötigen wir für die Verwendung eines Proxy, einem speziellen, polykindeten Datentyp, der ausschließlich Typinformationen transportiert. Man verwendet ihn, damit
Typen auch direkt als Ein- und Ausgabewerte einer Funktion dienen können.
data Proxy (n :: k) = Proxy
replicate’’ :: forall n a. SingI n => Proxy n -> a -> Vector a n
replicate’’ _ = replicate’ (sing :: SNat n)
ghci > type Two = Succ (Succ Zero)
ghci > replicate’’ (Proxy :: Proxy Two) 8
Cons 8 (Cons 8 Nil)
Es wird deutlich, dass der Nutzer eigentlich durch den „Eingabetyp“ die Länge schon
zur Kompilierzeit festlegt. Anstatt also erst einen Proxy zu erzeugen, um ihn den gewünschten Typ beizufügen, könnte man gleich die Länge in den Typ des Ergebnisses
schreiben.
replicate :: forall n a. SingI n => a -> Vector a n
replicate = replicate’ (sing :: SNat n)
ghci > replicate 8 :: Vector Int Two
Cons 8 (Cons 8 Nil)
Für unser endgültiges Ziel, bei der Übersetzung festzustellen, ob ein indexbasierter Zugriff auf unseren Vektor gültig ist, brauchen wir das Constraint t1 ~ t28 . GHC überprüft dabei für uns, ob sich t1 und t2 unifizieren lassen.
nth :: (k < n) ~ True => SNat k -> Vector a n -> a
nth SZ
(Cons x _ ) = x
nth (SN k’) (Cons _ xs) = nth k’ xs
11 Heterogene Listen
Wir haben bereits im Abschnitt 5 gesehen, wie sich heterogene Listen (HLists) gestalten
lassen. Allerdings wurde dies dort durch das Reduzieren an Typinformationen erreicht.
Hier sollen nun „echte“ HLists entworfen werden [KLS04]. Sie generalisieren Tupel in
Haskell und erlauben beispielsweise typsichere Zugriffe auf Datenbanksysteme.
DataKinds promotet auch den Datentyp der Listen [a] inklusive Syntax. Davon machen
wir nun Gebrauch. Wir erstellen ähnlich wie zuvor einen korrespondieren Laufzeitrepräsentanten zur Typliste. Es sei darauf hingewiesen, dass alle Typen einer solchen Liste,
den selben Kind k haben müssen. Es handelt sich dabei um keine Singleton-Beziehung,
da Typen vom Kind * mehrere sinnvolle Laufzeitwerte besitzen.
8
Siehe I. Einleitung
14
infixr 5 :::
data HList (xs :: [k]) where
HNil
:: HList ’[]
(:::)
:: x -> HList xs -> HList (x ’: xs)
xs = ("foo" ::: True ::: 42 ::: HNil)
ghci > :type xs
xs :: HList ’[[Char], Bool, Integer]
Das Hochkomma vor ’[] signalisiert GHC, dass hier der Typ [] vom Kind [k] gemeint
ist und eben nicht der Typkonstruktor [] vom Kind * -> *, wie in [Int].
Es lassen sich natürlich auch die gewohnten Funktionen auf Listen promoten.
type family Length (xs :: [k]) :: Nat where
Length ’[]
= Zero
Length (x ’: xs) = Succ (Length xs)
Der Zugriff via Indizes ist hier allerdings etwas komplizierter, schließlich hängt der Ergebnistyp von der Stelle ab, die wir erhalten wollen. Mit Nth bestimmen eben diesen.
type family Nth (n :: Nat) (xs :: [k]) :: k where
Nth Zero
(x ’: xs) = x
Nth (Succ n) (x ’: xs) = Nth n xs
Damit können wir nun auch für heterogene Listen sicher auf das n-te Element zugreifen.
nth :: (n < Length xs) ~ True => SNat n -> HList xs -> Nth n xs
nth SZ
(x ::: _ ) = x
nth (SN n’) (_ ::: xs) = nth n’ xs
ghci > nth
42 :: Int
ghci > nth
42 :: Int
(SN (SN SZ)) xs
(sing :: Sing Two) xs
Auch hier kann man wieder eine Version mit Proxy angeben, wobei der Typ doch etwas
wuchtig ist.
nth’ :: forall n xs . ((n < Length xs) ~ True, SingI n) =>
Proxy n -> HList xs -> Nth n xs
nth’ _ = nth (sing :: SNat n)
ghci > nth’ (Proxy :: Proxy Two) xs
42 :: Int
Genau wie es Funktionen höherer Ordnung gibt, so gibt es konsequenterweise durch den
Kind-Polymorphismus auch Typfunktionen höherer Ordnung, wenngleich diese deutlich
weniger nützlich sind. Eine interessante Anwendung findet man in [Eis+13] bzw. [Eis12]
bei der Umsetzung n-stelliger Funktionen, konkret zipWith.
Hier sei nur ein einfaches Beispiel angegeben: Man könnte Filter dazu benutzen, wenn
man an den Anwendungsfall Datenbankanfrage denkt, um uninteressante Felder auszublenden.
15
type family Filter (p :: k -> Bool) (xs :: [k]) :: [k] where
Filter pred ’[]
= ’[]
Filter pred (x ’: xs) =
If (pred x) (x ’: Filter pred xs) (Filter pred xs)
Man kann dies alles noch deutlich weiter ausbauen. Es ist sogar möglich Records flexibler Größe zu programmieren und Typen als Indexmenge zu verwenden [Ole15a]. GHC
enthält seit Version 4.6 ein internes Modul GHC.TypeLits [GHC15a] in dem Funktionen
bereitgestellt werden, mit denen man sowohl Strings zu Typen promoten kann, als auch
Zahlenliterale.
12 Typbeweise
12.1 Einleitung
Zum Abschluss soll gezeigt werden, wie man GHC davon überzeugen kann, dass eigene,
dependently typed Funktionen typkorrekt sind. Dazu bedarf es nämlich gegebenfalls eines
Beweises. Dieses Beispiel ist aus [Die15] übernommen.
reverse :: Vector a n -> Vector a n
-- Could not deduce (n1 ~ (n1 + ’Zero))
reverse xs = go Nil xs
where
go :: Vector a n -> Vector a m -> Vector a (n + m)
go acc Nil
= acc
-- Could not deduce
-- ((n1 + ’Succ m1) ~ ’Succ (n1 + m1))
go acc (Cons x xs) = go (Cons x acc) xs
Wieso gibt GHC für Zeile drei bzw. neun diese Fehlermeldung aus? Dazu müssen wir
uns die Gleichungen für die Typaddition noch einmal genauer anschauen.
type family (m :: Nat) + (n :: Nat) :: Nat where
Zero
+ n = n
Succ m + n = Succ (m + n)
Dabei stellen wir fest, dass es der Compiler tatsächlich nicht durch einfaches Umschreiben inferieren kann, dass (+) mit sich selbst, aber auch mit Succ kommutiert.
12.2 Vorbereitungen
infixr 4 :=:
data a :=: b where Refl :: a :=: a
Dieser GADT hilft uns dabei, Typgleichheit zu beweisen. Refl wird in gewisser Weise
als Basisfall dienen, wenn wir etwas durch Induktion zeigen wollen. Wir beauftragen
GHC dann damit, ggf. noch übrigen Umformungen von a nach b selbst zu erledigen.
Um die beiden Lemmata herleiten und anwenden zu können, müssen wir zunächst ein
paar logische Regeln in Haskell notieren.
1. (a ∼
= b) ⇒ (f (a) ∼
= f (b)), wenn f eine totale Funktion ist. Alle Haskell Typkonstruktoren sind total; man kann für das a in Maybe a jeden Typ9 einsetzen.
9
vom Kind *
16
cong :: a :=: b -> f a :=: f b
cong Refl = Refl
2. (a ∼
= b) ⇒ (p(a) ⇒ (p(b))), sind a und b gleich, so gilt die Eigenschaft p(b), wenn
p(a) gilt. Wir werden subst später als Castingfunktion nutzen; falls GHC p(a) ∼
= p(b)
∼
nicht zeigen kann, setzten wir den Beweis für a = b ein.
subst :: a :=: b -> p a -> p b
subst Refl = id
12.3 Beweise
Lemma 1. n + Zero = n
Beweis. Induktion über n
Basisfall: n = Zero, es kann aus der Definition direkt abgeleitet werden, dass Zero +
Zero = Zero gilt. (Daher kann Refl benutzt werden.)
Schritt: n = succ(n0 ), IH: n0 + Zero = n0
n + Zero = succ(n0 ) + Zero
nach Def. n
0
nach Def. (+)
0
= succ(n )
nach IH
= n
nach Def. n
= succ(n + Zero)
Die „Schwierigkeit“ für GHC liegt dabei im Anwenden der Induktionshypothese; diese
wird durch cong eingesetzt.
plus_zero :: forall n . SNat n -> n + Zero :=: n
plus_zero SZ
= Refl
plus_zero (SN n) = cong (plus_zero n)
Lemma 2. n + succ(m) = succ(n + m)
Beweis. Induktion über n
Basisfall: n = Zero, Zero + succ(m) = succ(Zero + m) erhält man durch zweimaliges
Anwenden der Definition von (+).
Schritt: n = succ(n0 ), IH: n0 + succ(m) = succ(n0 + m)
n + succ(m) = succ(n0 ) + succ(m)
0
= succ(n + succ(m))
nach Def. n
nach Def. (+)
0
nach IH
0
= succ(succ(n ) + m)
nach Def. (+)
= succ(n + m)
nach Def. n
= succ(succ(n + m))
plus_suc :: forall n m. SNat n -> SNat m -> n + Succ m :=: Succ (n + m)
plus_suc SZ m
= Refl
plus_suc (SN n) m = cong (plus_suc n m)
17
12.4 Anwendung
Damit wir einen Laufzeitrepräsentanten der Länge unseres Vektors dann in unseren
Beweis substituieren können, um eine Verbindung zwischen Lemma und unserem konkretem Fall herzustellen, benötigen wir zusätzlich noch die Funktion size.
size :: Vector a n -> SNat n
size Nil
= SZ
size (Cons _ xs) = SN $ size xs
In der dritten Zeile zeigen wir auf diese Weise, dass der Typ von go Nil xs gleich
Vector a n ist. Entsprechendes gilt für die vorletzte Zeile.10
reverse ::
reverse xs
where
go
go
go
forall n a. Vector a n -> Vector a n
= subst (plus_zero (size xs)) $ go Nil xs
:: Vector a m -> Vector a k -> Vector a (k + m)
acc Nil
= acc
acc (Cons x xs) = subst (plus_suc (size xs) (size acc)) $
go (Cons x acc) xs
13 Ausblick
Dependent types erweitern die Möglichkeiten einer Programmiersprache fundamental
und mit DataKinds und TypeFamilies fand eine praktikable Umsetzung dessen Einzug
in Haskell. Die Kapitel 8 bis 11 verdeutlichen dies. Sie korrespondieren teilweise mit
Paketen, die auf Hackage zur Verfügung stehen:
Das HList-Package [Ole15b] nutzt beide vorgestellten Techniken, ist allerdings unzureichend dokumentiert. Es stellt darüber hinausgehend jedoch auch erweiterbare Records
vor, welche sich zum Beispiel nach Typ filtern lassen.
Modern gestaltet ist das Unit-Package [Eis15]. Es beinhaltet umfangreiche Möglichkeiten, um mit Einheiten umzugehen. Beispielsweise können zusammengesetzte Typen (i.
d. R. durch Multiplikation beziehungsweise Division) wie Length %/ Time erstellt werden, die automatisch von ihren Basistypen Konvertierungsfunktionen, SI-Einheit etc.
ableiten.
Eine weitere bemerkenswerte Möglichkeit von dependent types ist, dass man damit Datenstrukturinvarianten vom Compiler überprüfen lassen kann. Weirich nutzt dies beispielsweise, um halbautomatisch eine Red-Black-Tree-Implementierung als korrekt zu
beweisen [Wei].
In den letzten Jahren hat Haskell große Schritte in Richtung einer „echten“ dependently typed Sprache genommen. Diese Entwicklung wird mit der neusten Version von
GHC (7.10, April 2015) [GHC15b] noch weiter gestärkt, da ein experimentelles PlugIn-Interface zum Type-Checker zur Verfügung gestellt wird. Numerische Constraints
könnten jetzt beispielsweise außerhalb durch einen SMT-Solver (Satisfiability Modulo
Theories) gelöst werden, sodass einfache Beweise wie jene in Kaptel 12 nicht mehr nötig
sind. Haskell rückt damit näher an Sprachen wie Agda und Idris heran, ohne legacy
Code aufzugeben.
10
Die Beweise werden leider nicht von GHC im optimierten Code entfernt.
18
Literatur
[McB02]
Conor McBride. „Faking it – Simulating dependent types in Haskell“. In:
Journal of functional programming 12.4-5 (2002), S. 375–392.
[Pie02]
Benjamin C Pierce. Types and programming languages. MIT press, 2002.
[KLS04]
Oleg Kiselyov, Ralf Lämmel und Keean Schupke. „Strongly typed heterogeneous collections“. In: Proceedings of the 2004 ACM SIGPLAN workshop
on Haskell. ACM. 2004, S. 96–107.
[Sul+07]
Martin Sulzmann u. a. „System F with type equality coercions“. In: Proceedings of the 2007 ACM SIGPLAN international workshop on Types in
languages design and implementation. ACM. 2007, S. 53–66.
[Pal08]
Luke Palmer. Undecidable instances. Apr. 2008. url: https://lukepalmer.
wordpress.com/2008/04/08/stop-using-undecidable-instances/.
[KJS10]
Oleg Kiselyov, Simon Peyton Jones und Chung-chieh Shan. „Fun with type functions“. In: Reflections on the Work of CAR Hoare. Springer, 2010,
S. 301–331.
[Eis12]
Richard A Eisenberg. Variable-arity zipWith. 2012. url: https://typesandkinds.
wordpress.com/2012/11/26/variable-arity-zipwith/.
[Eis13]
Richard Eisenberg. System FC: equality constraints and coercion. 2013. url:
https://ghc.haskell.org/trac/ghc/wiki/Commentary/Compiler/FC.
[EW13]
Richard A Eisenberg und Stephanie Weirich. „Dependently typed programming with singletons“. In: ACM SIGPLAN Notices 47.12 (2013), S. 117–
130.
[Eis+13]
Richard A Eisenberg u. a. „Closed type families with overlapping equations
(extended version)“. In: (2013).
[Wik13]
WikiBooks. Haskell/Phantom types. Jan. 2013. url: http://en.wikibooks.
org/wiki/Haskell/Phantom_types.
[Die15]
Stephen Diehl. What I Wish I Knew When Learning Haskell. 2015. url:
http://dev.stephendiehl.com/hask/#advanced-proofs.
[Eis15]
Richard Eisenberg. The units package. Feb. 2015. url: https://github.
com/goldfirere/units.
[GHC15a]
GHC-Team. GHC.TypeLits. 2015. url: https://hackage.haskell.org/
package/base-4.8.0.0/docs/GHC-TypeLits.html#t:Nat.
[GHC15b]
GHC-Team. Typechecker plugins. 2015. url: https://downloads.haskell.
org/~ghc/7.10.1/docs/html/users_guide/compiler-plugins.html#
typechecker-plugins.
[GHC15c]
GHC-Team. Undecidable instances. 2015. url: https://downloads.haskell.
org/~ghc/latest/docs/html/users_guide/type- class- extensions.
html#undecidable-instances.
[GHC15d]
GHC-Team. Undecidable instances. 2015. url: https://downloads.haskell.
org/~ghc/latest/docs/html/users_guide/data- type- extensions.
html#existential-quantification.
[Ole15a]
Keean Schupke Oleg Kiselyov Ralf Laemmel. The HList package. 2015. url:
https://hackage.haskell.org/package/HList-0.4.0.0/.
19
[Ole15b]
Keean Schupke Oleg Kiselyov Ralf Laemmel. The HList package. Mai 2015.
url: https://hackage.haskell.org/package/HList.
[Wik15a]
WikiBooks. Haskell/Existentially quantified types. Apr. 2015. url: http://
en.wikibooks.org/wiki/Haskell/Existentially_quantified_types.
[Wik15b]
WikiBooks. Haskell/GADT. Apr. 2015. url: http://en.wikibooks.org/
wiki/Haskell/GADT.
[Wik15c]
Wikipedia. Lambda cube. Juni 2015. url: https://en.wikipedia.org/
wiki/Lambda_cube.
[Wik15d]
Wikipedia. Relation (Mathematik)/Eigenschaften zweistelliger Relationen.
Juni 2015. url: https://de.wikipedia.org/wiki/Relation_(Mathematik)
#Eigenschaften_zweistelliger_Relationen.
[Wik15e]
Wikipedia. System F. Juni 2015. url: https://en.wikipedia.org/wiki/
System_F.
[Wei]
Stephanie Weirich. Depending on Types. url: https://www.cis.upenn.
edu/~sweirich/talks/icfp14.pdf.
20
Herunterladen