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