Typsysteme Typisierung Polymorphie Typinferenz Einführung in die Funktionale Programmierung mit Haskell Typsysteme & Typinferenz Steffen Jost LFE Theoretische Informatik, Institut für Informatik, Ludwig-Maximilians Universität, München 12. Juni 2013 Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Planung Klausur: 19.7. 10h Vorläufige Planung: Mi Vorlesung Do Vorlesung 17. Apr 13 18. Apr 13 24. Apr 13 25. Apr 13 1. Mai 13 2. Mai 13 8. Mai 13 9. Mai 13 15. Mai 13 16. Mai 13 22. Mai 13 23. Mai 13 29. Mai 13 30. Mai 13 5. Jun 13 6. Jun 13 12. Jun 13 13. Jun 13 19. Jun 13 20. Jun 13 26. Jun 13 27. Jun 13 3. Jul 13 4. Jul 13 10. Jul 13 11. Jul 13 17. Jul 13 18. Jul 13 Fr Übung 19. Apr 13 26. Apr 13 3. Mai 13 10. Mai 13 17. Mai 13 24. Mai 13 31. Mai 13 7. Jun 13 14. Jun 13 21. Jun 13 28. Jun 13 5. Jul 13 12. Jul 13 19. Jul 13 Steffen Jost Sollumfang: Raumbuchung: 3+2 4+2 42 Termine, davon 3 Feiertage 4 Übung nachholen 6 Entfällt 1 Klausurtermin = 20V + 12Ü Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Typprüfung Wir haben bisher behauptet: Das statische Typsystem von Haskell prüft während dem Kompilieren alle Typen: Keine Typfehler und keine Typprüfung zur Laufzeit Kompiler findet bereits viele Programmierfehler Kompiler kann besser optimieren “Well-typed programs can’t go wrong!” R. Milner 1934-2010 Heute klären wir: Was genau sind Typfehler? Was ist der Unterschied zwischen getypten und ungetypten Sprachen? Wie bestimmt man den Typ eines Programmes? Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Typfehler Offensichtlich machen folgende Ausdrücke keinen Sinn: "Hello" * True 0.23 + ’a’ False && (\x -> "error") (*) 1 (+) 2 3 4 (\f -> f 42) 69 if 3 * x then 42 else "nothing" Ist dies wirklich offensichtlich? Wie formalisieren wir die Intuition, welche Ausdrücke korrekt sind? Wie ist das in anderen Sprachen? Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Typsysteme in der Praxis In der Praxis gibt es durchaus unterschiedliche Ansätze zum Einsatz von Typsystemen in der Programmierung. Was passiert bei der Auswertung von (\f -> f 42) 69? Keine Typprüfung: 69 wird als Sprungaddresse interpretiert. Systemabsturz oder schwere Betriebssystem-Ausnahme möglich. Dynamische Typprüfung: Werte tragen zur Laufzeit Typ-Markierung Ganzzahl, Funktion, . . . Da 69 keine Funktion ist, wird eine Ausnahme im Laufzeitsystem geworfen. Statische Typprüfung: Der Übersetzer weigert sich, ein Programm mit Typfehlern zu übersetzen. Es kommt nie zu derartigen Laufzeitfehlern. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Typsysteme in der Praxis Unterschiedliche Ansätze zum Einsatz von Typsystemen: Keine Typprüfung: Assembler ⊕ Schnelle Ausführung; Viel Freiheit für Programmierer Keinerlei Sicherheit; Schwere Fehler möglich Dynamische Typprüfung: LISP, Scheme, BASIC, Skriptsprachen: JavaScript, Python ⊕ Viel Freiheit für Programmierer Ständige Typrüfung zur Laufzeit verlangsamt Ausführung Statische Typprüfung: Haskell, SML, Ocaml, Scala, Eingeschränkt: C, Java ⊕ Kompiler schliesst viele Fehler von vorn herein aus Eingeschränkte Freiheit beim Programmieren; Langwierige Fehlermeldung nerven beim Kompilieren Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Programmausdrücke Zur Vereinfachung betrachten wir nur ein Haskell-Fragment. Der λ-Kalkül (1936, Alonzo Church(1902-95)) beschreibt den funktionalen Kern von Haskell. Ein Programmausdruck e im Lambda-Kalkül ist: e ::= x Variable foo | c Konstante 2.718 | e1 e2 Anwendung sqrt 5.0 | λx.e Funktionsabstraktion (\x -> e) (e wie expression, engl. für “Ausdruck”) Der Einfachheit verwenden wir die Haskell-Syntax Weitere Haskell-Ausdrücke werden nach Bedarf hinzugenommen Infix-Operatoren schreiben wir hier meist in präfix Notation, also (+) 2 7 anstatt 2 + 7 Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Typausdrücke Ein Typausdruck A is definiert durch: A, B ::= α | β | . . . | Int | Double | String | . . . | A→B Typvariable a, b Basistyp Funktionstyp Int -> Bool Wir beschränken uns auf Grund- und Funktionstypen; also keine Listen, keine Typklassen, etc. Funktionstypen sind weiterhin implizit rechts-geklammert: A → B → C = A → (B → C ) 6= (A → B) → C Eventuell verwenden wir Abkürzungen für Basistypen I, D, S, . . . anstatt Int, Double, String, . . . . Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Typzuweisung Eine Typzuweisung e::A ist ein Paar, bestehend aus einem Programmausdruck e und einen Typausdruck A, interpretiert als Aussage “e hat Typ A” 4 :: Int wahr true :: String falsch 3.3 :: a falsch \x -> x - 1 :: Int-> Int wahr \x y -> x :: a -> b -> a wahr \x y -> y :: a -> b -> a falsch z (x + 0) :: Bool ??? Typzuweisungs-Aussagen können wahr oder falsch sein In der Literatur wird oft e::A anstatt e::A geschrieben Der Typausdruck z (x + 0) enthält unbekannte, freie Variablen x und z, deren Typ wir nicht kennen Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Typkontext Ein Typ(isierungs)kontext Γ ist eine endliche Menge von Typzuweisungen Γ = {x1 ::A1 , . . . xn ::An } (n ≥ 0) wobei die Typvariablen xi paarweise verschieden sein müssen. (engl. typing context/environment) {x::Bool, y::Int → Int} {z::Int → Bool, x::Int} {h::Int → Bool, h::Int} gültig gültig ungültig Γ ist endliche Abbildung von Variablen x auf Typen Γ(x) Die Reihenfolge der Typzuweisungen in Γ ist unbedeutend Wir schreiben Γ, x::A um x::A in Γ einzufügen; falls x ∈ dom(Γ) dann ist Γ, x::A undefiniert Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Typisierung Ein Typurteil ist eine Aussage der Form: “Unter der Annahme, dass Variablen die in Γ angegebenen Typen haben, hat Ausdruck e den Typ A” (engl. typing judgement) Wir schreiben kurz Γ ` e::A für “Ausdruck e den Typ A in Kontext Γ”. Formal ist ` : eine dreistellige Relation zwischen Typkontexten, Ausdrücken, und Typen. Beispiele: {x::Int} ` \y → (+) x y : Int → Int gültig {x::Bool} ` \y → (+) x y : Int → Int ungültig {y : α} : β→α gültig ` \x → y Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Typregeln Ein Typurteil ist nur dann gültig, wenn es durch Typregeln hergeleitet werden kann. Typregeln haben immer die Form: P1 . . . Pn K (Name der Typregel) Dabei sind P1 . . . Pn die Prämissen der Typregel und K die Konklusion. Sind alle Prämissen gültig, dann auch die Konklusion. Pi und K sind in der Regel Typurteile Typregeln ohne Prämissen heißen Axiome Z.B. für jede Konstante c gibt es ein Typaxiom. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Typregel Var Der Typ einer Variablen ist durch den Kontext gegeben: (x::A) ∈ Γ Γ ` x::A (Var) Da Γ als endliche Abbildung aufgefasst werden kann, können wir diese Typregel auch äquivalent kurz schreiben durch: Γ ` x::Γ(x) (Var) Beispiele: {x::Double} {x::Double, y ::Int, z::Bool} {x::Double, y ::Int, z::Bool} {} Steffen Jost ` ` ` ` x::Double y ::Int y ::Double x::Double gültig gültig falscher Typ ungebundene Variable Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Typregel Const Für jede Konstante eines jeden Basistypen definieren wir ein Axiom (Regel ohne Prämisse), welches die Konstante erkennt: (True) (False) Γ ` True::Bool Γ ` False::Bool Dabei ist der Kontext Γ hier beliebig, d.h. wir definieren ein Axiomenschema für jedes Γ. Mehrere Konstanten fassen wir mit folgenden Schemen zusammen: c ist eine Integer Konstante c ist eine Double Konstante Γ ` c::Int Γ ` c::Double (Int) (Double) Es ist sowohl {} ` 5::Int als auch {} ` 5::Double herleitbar. Dies ist unproblematisch so lange wir Typklassen ignorieren. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Typregel Funktionsanwendung Anwendung einer Funktion e1 mit Typ A → B auf Argument e2 von Typ A liefert ein Ergebnis des Typs B: Γ ` e1 ::A → B Γ ` e2 ::A Γ ` e1 e2 ::B (App) Diese Regel ist nicht anwendbar, falls: e1 nicht von einem Funktionstypen ist e2 nicht von Typ A ist, d.h. nicht im Definitionsbereich liegt. Beispiel: {x::Bool → Bool, y ::Bool} ` x::Bool → Bool {x::Bool → Bool, y ::Bool} ` y ::Bool {x::Bool → Bool, y ::Bool} ` xy ::Bool (App) Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Beispiel Funktionsanwendung Γ ` e1 :: A → B Γ ` e2 :: A Γ ` e1 e2 :: B (App) Zur Anwendung der allgemeinen Typregel werden Metavariablen gemäß Kontext, Ausdruck und Typ instanziiert: Γ ` \z → (+) z x::Int → Int Γ ` (∗) 2 y ::Int (App) Γ ` \z → (+) z x (∗) 2 y :: Int . . . ist z.B. herleitbar für Γ = {x::Int, y :: Int}. Alternative Schreibweise in Infix-Notation: Γ ` \z → z + x :: Int → Int Γ ` 2 ∗ y :: Int (App) Γ ` \z → z + x 2 ∗ y :: Int Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Beispiel Funktionsanwendung Γ ` e1 :: A → B Γ ` e2 :: A Γ ` e1 e2 :: B (App) Zur Anwendung der allgemeinen Typregel werden Metavariablen gemäß Kontext, Ausdruck und Typ instanziiert: Γ ` \z → (+) z x::Int → Int Γ ` (∗) 2 y ::Int (App) Γ ` \z → (+) z x (∗) 2 y :: Int . . . ist z.B. herleitbar für Γ = {x::Int, y :: Int}. Alternative Schreibweise in Infix-Notation: Γ ` \z → z + x :: Int → Int Γ ` 2 ∗ y :: Int (App) Γ ` \z → z + x 2 ∗ y :: Int Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Typregel Funktionsabstraktion Die Typregel für anonyme Funktionsabstraktion lautet: Γ, x::A ` e::B Γ ` \x → e :: A → B (Abs) “Hat der Programmausdruck e den Typ B unter der Annahme x : A, so hat der Programmausdruck \x → e den Typ A → B” Man nennt e den Funktionsrumpf der Funktion \x → e, im Funktionsrumpf kommt die abstrahierte Variable x frei vor, in dem Programmausdruck \x → e gebunden. Man sagt auch “Lambda \ bindet die darauf folgende Variable” Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Typherleitung Eine Typherleitung / Typbeweis ist ein endlicher Baum, wobei alle die Blätter Typaxiome sind; alle Knoten derart Instanzen von Typregeln sind, dass die Beschriftung des Knotens die Konklusion ist, und die Kinder des Knotens die Prämissen sind. Die Beschriftung des Wurzelknotens ist die hergeleitete Aussage. Eine Typaussage ist herleitbar, wenn sie eine Herleitung hat. Eine Typherleitung ist letztendlich nur eine verkette Anwendung von Typregeln. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Beispiel Herleitungsbaum Eine Herleitung als Baum dargestellt: {x::α, f ::α → β} ` f :: α → β Var {x::α, f ::α → β} ` x :: α App {x::α, f ::α → β} ` f x :: β {x::α} ` \f → f x :: (α → β) → β Var Abs {} ` \x → (\f → f x) :: α → (α → β) → β Abs Den Namen der angewendeten Regel schreiben wir an den rechten Rand, damit man besser nachvollziehen kann, was gemacht wurde. In Haskell könnten wir anstatt \x → (\f → f x) auch einfach \x f → f x schreiben, doch unsere Typregel Abs haben wir der Einfachheit halber nur für ein Argument definiert – was ja keinen Unterschied macht! Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Beispiel Herleitungen Eine Herleitung in linearer Schreibweise: 1 2 3 4 5 {x::α, f ::α → β} ` f ::α → β {x::α, f ::α → β} ` x::α {x::α, f ::α → β} ` f x::β {x::α} ` \f → f x::(α → β) → β {} ` \x → (\f → f x)::α → (α → β) → β Var Var App(1, 2) Abs(3) Abs(4) Bei dieser Schreibweise müssen wir alle Zeilen nummerieren. Bei jeder Regelanwendung listen wir dann die verwendeten Prämissen der Reihe nach auf. Erstellt werden Herleitungen normalerweise von unten nach oben, gelesen werden sie aber meist von oben nach unten. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Kontexterweiterung Eine Typaussage bleibt gültig, wenn wir den Typisierungkontext erweitern. Bsp: {x::α} ` \f → f x :: (α → β) → β {x::α, y ::γ} ` \f → f x :: (α → β) → β {f ::Int → Bool, x::α, y ::γ} ` \f → f x :: (α → β) → β Lemma (Kontexterweiterung/Abschwächung) Wenn Γ ⊆ Γ0 und Γ ` e : A herleitbar ist, dann auch Γ0 ` e : A. Beweisbar durch Induktion über die Länge der Herleitung. Daraus leiten wir folgende strukturelle Typregel ab: Γ0 ⊇ Γ Γ0 Γ`e:A `e:A (Weak) Die Regel ist jederzeit anwendbar, da sie unabhängig von e ist. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Einführung in die Funktionale Programmierung mit Haskell Typsysteme & Typinferenz Steffen Jost LFE Theoretische Informatik, Institut für Informatik, Ludwig-Maximilians Universität, München 19. Juni 2013 Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Typregeln Wdh. Ein Typurteil ist nur dann gültig, wenn es durch Typregeln hergeleitet werden kann. Typregeln haben immer die Form: P1 . . . Pn K (Name der Typregel) Dabei sind P1 . . . Pn die Prämissen der Typregel und K die Konklusion. Sind alle Prämissen gültig, dann auch die Konklusion. Pi und K sind in der Regel Typurteile Typregeln ohne Prämissen heißen Axiome Z.B. für jede Konstante c gibt es ein Typaxiom. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Bisher behandelte Typregeln Wdh Folgende Typregeln wurden bisher in der Vorlesung behandelt: Γ ` x :: Γ(x) c ist eine Integer Konstante Γ ` c :: Int Γ ` e1 :: A → B Γ ` e2 :: A Γ ` e1 e2 :: B Γ, x::A ` e :: B Γ ` \x → e :: A → B Steffen Jost (Var) (Int) (App) (Abs) Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Typherleitung Beispiel An der Tafel konstruieren wir eine Typherleitung für das Typurteil: (+)::Int → Int → Int, z::α ` (+) 4 (\x -> 1) z :: Int Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Weakening Wdh Wir hatten gesehen, dass folgende Typregel zulässig ist, da Kontexterweiterungen harmlos sind: Γ0 ⊇ Γ Γ0 Γ`e:A `e:A (Weak) Intuitiv: “Es schadet nie, mehr zu wissen als nötig” Eine Typregel ist zulässig (engl. admissible), wenn deren Hinzunahme keine neuen Herleitungen ermöglicht. D.h. die Regel kann Herleitungen lediglich vereinfachen. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Exchange Da wir Typkontexte als Mengen von Typzuweisungen definiert haben, ist die Reihenfolge der Typannahmen bedeutungslos. Dies können wir ebenfalls durch eine zulässige Regel ausdrücken: Γ, x::A, y ::B, ∆ ` e :: C Γ, y ::B, x::A, ∆ ` e :: C Steffen Jost (Exchange) Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Kontraktion Mehrfachverwendung von Variablen ist harmlos: Γ, x::A, y ::A ` e :: C Γ, z::A ` e[z/x, z/y ] :: C (Contraction) Termsubstitution: e[z/x, z/y ] bezeichnet den Term e, bei den alle freie Vorkommen von x und y ersetzt werden. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Termsubstitution: Wir definieren Substitution für Terme wir folgt: x[e0 /x] := e0 y [e0 /x] := y (e1 e2 )[e0 /x] := e1 [e0 /x] e2 [e0 /x] (\y -> e1 )[e0 /x] := \x -> e1 [e0 /x] falls y ∈ / FV(e0 ) (\x -> e1 )[e0 /x]:= \x -> e0 (\y -> e1 )[e0 /x]:= \y 0 -> (e1 [y 0 /y ])[e0 /x] für y 0 eine frische Variable Bei Substitutionen muss man aufpassen, dass freie Variablen nicht versehentlich eingefangen werden: Wir schreiben kurz e0 [e1 /x, e2 /y ] für e0 [e1 /x] [e2 /y ], d.h. mehrere Substitution arbeiten wir von links nach rechts ab. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Strukturelle Typregeln Üblicherweise gelten diese drei strukturellen Typregeln implizit, wenn nichts anderes angegeben wurde. Zusätzliche Annahmen sind harmlos: Γ0 ⊇ Γ Γ0 Γ`e:A (Weak) `e:A Reihenfolge der Annahmen ist bedeutungslos: Γ, x::A, y ::B, ∆ ` e :: C Γ, y ::B, x::A, ∆ ` e :: C (Exchange) Eine Variable kann mehrfach verwendet werden: Γ, x::A, y ::A ` e :: C z∈ / FV(e) Γ, z::A ` e[z/x, z/y ] :: C Steffen Jost (Contraction) Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Bisher behandelte Typregeln II Übung 7 behandelte Typregeln für Bildung von Paaren und deren Elimination durch Pattern-Matching so wie if-then-else Ausdrücke: Γ ` e1 :: A Γ ` e2 :: B Γ ` (e1 , e2 ) :: (A, B) Γ ` e1 :: (B, C ) Γ, x::B, y ::C ` e2 :: A Γ ` let (x, y ) = e1 in e2 :: A Γ ` e1 : Bool (Pair-Intro) Γ ` e2 : A (Pair-Elim) Γ ` e3 : A Γ ` if e1 then e2 else e3 : A (Cond) Dabei wurden neue Programm- und Typ-ausdrücke eingeführt. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Typregeln Paare Neue Programmausdrücke: (e1 , e2 ) let (x, y ) = e1 in e2 Paar-Introduktion Paar-Elimination Neuer Typausdruck: (A, B) Neue Typregeln: Γ ` e1 :: A Γ ` e2 :: B Γ ` (e1 , e2 ) :: (A, B) Γ ` e1 :: (B, C ) (Pair-Intro) Γ, x::B, y ::C ` e2 :: A Γ ` let (x, y ) = e1 in e2 :: A (Pair-Elim) Bedeutung: Typ eines Paares ist das Produkt der Typen der Einzelteile Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Curry-Howard-Isomorphismus Beobachtung Regeln treten of in Paaren auf: zur Einführung eines Typs und zu Elimination eines Typs Abs/App, Pair-Intro/Pair-Elim Dies ist sehr ähnlich zur mathematischen Beweisführung im Hilbert-Stil, welche ebenfalls Herleitungsbäume verwendet. Curry-Howard-Isomorphismus Ein Typ kann als logische Formel aufgefasst werden. Gibt es einen Programmausdruck zu einem Typ, so ist die entsprechende logische Formel intuitionistisch beweisbar. Typ A → B entspricht der logischen Implikation, Typ (A, B) entspricht dem logischen “und”, usw. Beobachtet durch Curry & Feys (1958) und Howard (1969) Anwendung Übertragung von Erkenntnissen zwischen Fachgebieten, Konstruktion von Beweisassistenten, Automatische Programmextraktion aus intuitionistischen Beweisen Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Typregel Konditional Neuer Programmausdruck: if e1 then e2 else e3 Neue Typregel: Γ ` e1 :: Bool Γ ` e2 :: A Γ ` e3 :: A Γ ` if e1 then e2 else e3 :: A (Cond) Bedeutung: Bedingung muss Typ Bool haben Zweige müssen vom gleichen Typ wie Gesamtausdruck sein Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Safety = Progress + Preservation Eine Typherleitung ist ein formaler Beweis, dass ein Typausdruck einen bestimmten Typ hat. Was nutzt das? Wir behaupteten: “Well-typed programs can’t go wrong” Dies beweist man üblicherweise in 2 Schritten: Progress Jeder wohl-typisierte Programmausdruck ist entweder ein Wert oder er kann weiter ausgewertet werden. Preservation Auswertung verändert den Typ eines Programmausdrucks nicht. Für diese Beweise müssten wir Substitutionsmodell durch Auswerteregeln formalisieren, wie z.B. Operationale Semantik e1 [e2 /x] ; v (;-App) (\x -> e1 ) e2 ; v Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Grundlagen Typregeln Typherleitung Strukturell Ausblick Beweisidee: Solche Beweise werden üblicherweise mit Induktion über die Länge der Typherleitung geführt: Um zu zeigen, dass if e1 then e2 else e3 ausgewertet werden kann, darf man annehmen dann, dass e1 zu einem Wert ausgewertet werden kann, da die Typherleitung für Γ ` e1 : Bool ja ein Schritt kleiner sein muss; usw. e1 ; True e2 ; v if e1 then e2 else e3 ; v Γ ` e1 :: Bool Γ ` e2 :: A Γ ` e3 :: A Γ ` if e1 then e2 else e3 :: A (;-If-T) (Cond) Auswerteregeln und Typregeln müssen übereinstimmen. Auswerteregeln und Implementation müssen übereinstimmen. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Typsubstitutionen Prinzipale Typisierung Inferenzprobleme Polymorphie Wie wir wissen, können Ausdrücke mehr als einen Typ haben, z.B. für den Programmausdruck f x sind herleitbar: {x::Int, f ::Int → Int} {x::Bool, f ::Bool → Int} {x::α, f ::α → α} {x::α, f ::α → β} ` ` ` ` f f f f x x x x :: :: :: :: Int Int α β Letzte hier die allgemeinste Typisierung (engl. principal typing): jede andere Typisierung von f x erhält man daraus durch Einsetzen von Typen für die Typvariablen α und β (Instanziierung). {x::α, f ::α → β}[Bool/α, Int/β] ` f x :: β[Bool/α, Int/β] Man kann beweisen: Allgemeinste Typisierung eines Ausdrucks ist bis auf Umbenennung von Typvariablen eindeutig. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Typsubstitutionen Prinzipale Typisierung Inferenzprobleme Typsubstitutionen Eine Typsubstitution σ ist eine endliche Abbildung von Typvariablen α auf Typausdrücke A. σ1 = = = σ2 = σ3 = σ4 = [Bool/α, Int/β] [Bool/α, Int/β, γ/γ] [Bool/α, Int/β, γ/γ, δ/δ] . . . [β/α, (β → Bool)/β, Int/γ] [γ/α, δ/β] [β/α, α/β] Für die Anwendung Aσ einer Substitution σ auf Typen A gilt z.B.: Int σ = Int (A → B)σ = Aσ → Bσ α[Bool/α, Int/β] = Bool Da bei uns alle Typvariablen frei sind, gibt es keine vergleichbaren Probleme wir bei der Termsubstitution. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Typsubstitutionen Prinzipale Typisierung Inferenzprobleme Instanziierung Wir definieren die Anwendung einer einer Substitution σ auf einen Typkontext Γ punktweise: (Γσ)(x) := (Γ(x))σ Beispiel {x::Int, y ::α → β, z::α}[γ/α, Int/β, Bool/γ] = {x::Int, y ::Bool → Int, z::Bool} Es ist beweisbar, dass Typurteile unter Substitution gültig bleiben: Lemma (Typerhaltung unter Substitution) Wenn Γ ` e :: A, dann Γσ ` e :: Aσ. Wir leiten daraus erneut eine zulässige Typregel ab: Γ ` e :: A Γσ ` e :: Aσ Steffen Jost (subst) Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Typsubstitutionen Prinzipale Typisierung Inferenzprobleme Substitutionskomposition Die Komposition σσ 0 zweier Substitutionen definieren wir wieder von links nach rechts: A(σ1 σ2 ) := (Aσ1 )σ2 Eine Komposition können wir oft vereinfachen: α[(α → β)/α][Int/α, Bool/β] = (α → β)[Int/α, Bool/β] = Int → Bool β[(α → β)/α][Int/α, Bool/β] = β[Int/α, Bool/β] = Bool [(α → β)/α][Int/α, Bool/β] = [(Int → Bool)/α, Bool/β] Komposition ist assoziativ, σ1 (σ2 σ3 ) = (σ1 σ2 )σ3 , aber nicht kommutativ σ1 σ2 6= σ2 σ1 Als neutrales Element der Komposition haben wir die leere Identitätssubstitution id = []. Es gilt id σ = σ und σ id = σ. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Typsubstitutionen Prinzipale Typisierung Inferenzprobleme Prinzipale Typisierung Eine Typisierung Γ ` e :: A eines Ausdrucks e ist die allgemeinste oder auch prinzipal Typisierung, falls es für jede Typisierung Γ0 ` e :: A0 von e eine Substitution σ gibt, so dass A0 = Aσ und Γ0 ⊇ Γσ. {x::α, f ::α → β} {x::Int, f ::Int → Int} {x::α, f ::α → β, y ::γ} {x::γ, f ::γ → β} ` ` ` ` f f f f x x x x :: :: :: :: β prinzipal Int σ = [Int/α, Int/β] β σ = id β σ = [γ/α] (auch prinzipal) Mann kann beweisen, dass jeder Programmausdruck unserer eingeschränkten Sprache eine prinzipale Typisierung hat! Für volles Haskell gilt dies aber nicht mehr. Die Berechnung einer prinzipalen Typisierung für einen Programmausdruck ist oft unentscheidbar. unentscheidbar: kein Verfahren mit endlicher Laufzeit bekannt Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Typsubstitutionen Prinzipale Typisierung Inferenzprobleme Monomorphismus Einschränkung In Haskell ist die Suche nach allgemeinen Typen grundsätzlich eingeschränkt: > :t show show :: Show a => a -> String > let f1 = show f1 :: () -> String > let f2 x = show x f2 :: Show a => a -> String > let f3 = \x -> show x f3 :: () -> String Option NoMonomorphismRestriction erweitert die Suche: > :set -XNoMonomorphismRestriction > let f4 = show f4 :: Show a => a -> String Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Typsubstitutionen Prinzipale Typisierung Inferenzprobleme Monomorphismus Einschränkung Diese Option hat aber auch weitere Nachteile: lenTwo = (len,len) where len = Data.List.genericLength xs Für den Typ lenTwo :: Num t => [b] -> (t, t) muss die Länge nur einmal berechnet werden, aber für den Typ lenTwo :: (Num a, Num b) => [c] -> (a,b) muss die Länge zwei Mal berechnet werden, da die grundlegenden Operationen (z.B. Addition) ja anders implementiert sein könnten. Wenn man Typsignaturen explizit angibt, dann braucht man die Option ohnehin nicht, da Haskell dann einfach nur den gegeben Typ prüft. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Typsubstitutionen Prinzipale Typisierung Inferenzprobleme Polymorphe Rekursion Polymorphe Rekursion bezeichnet rekursive Aufrufe mit anderem Typ; genauer: polymorphe Typparameter werden anders instantiiert. Polymorphe Rekursion ist sehr problematisch für Typinferenz. Es wurde bewiesen, dass Typinferenz für polymorphe Rekursion unentscheidbar ist. Beispiel data PowerList a = Zero a | Succ (PowerList (a,a)) pl1 = Succ $ Succ $ Succ $ Zero (((1,2),(3,4)),((5,6),(7,8))) plLength :: PowerList a -> Int -- GHC cannot infer this type! plLength (Zero _ ) = 1 plLength (Succ pl) = 2 * plLength pl Im Rumpf der Definition für plLength :: PowerList a -> Int findet ein rekursiver Aufruf mit Typ PowerList (a,a) -> Int statt. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Typsubstitutionen Prinzipale Typisierung Inferenzprobleme Rank N-Types Die Spracherweiterungen um GADTs und Rank N-Types bereiten der Typinferenz ebenfalls Probleme Beide Themen gehen jedoch über den Rahmen der Vorlesung heraus. Rank N-Types sind Typen, bei denen Quantoren für Typvariablen mitten im Typ auftauchen, und nicht nur wie bisher ganz vorne. Pathologisches Beispiel -- foo :: (forall a . a) -> b foo :: (forall a . a -> a) -> (b -> b) foo x = x x > (foo (foo id)) 42 42 Keine der beiden Typsignaturen kann von GHC inferiert werden. foo hat keinen prinzipalen Typ. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Unifikation Zusammenfassung Typinferenz Typinferenz berechnet zu jedem Ausdruck den allgemeinsten Typ (bzw. Fehlermeldung, falls der Ausdruck keinen Typ hat). Beispiel Der prinzipale Typ einer Funktionsanwendung e1 e2 muss aus den prinzipalen Typen von e1 und e2 berechnet werden: {} ` e1 :: (Int → β) → (γ → β) {x::α} ` e2 :: α → Double {x::Int} ` e1 e2 :: γ → Double Beide Prämissen müssen mit [Int/α, Double/β] instanziiert werden, welche die allgemeinste Lösung der Gleichung Int → β = α → Double ist. Dazu müssen wir also Typgleichung lösen können. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Unifikation Zusammenfassung Unifikation Eine Substitution σ ist allgemeiner als σ 0 , falls es eine Substitution τ gibt mit σ 0 = στ . Die speziellere Substitution σ 0 ist also eine Instanz von σ. Zum Lösen von Typgleichungen verwenden wir Unifikation: Ein Unifikator zweier Typen A und A0 ist eine Substitution σ, so dass Aσ = A0 σ. Der allgemeinste Unifikator von A und A0 ist die allgemeinste Substitution σ, so dass Aσ = A0 σ. Beispiel: (α → β) = (Int → β) für diese Typgleichung ist [Int/α, Int/β] ein Unifikator. Die Substitution [Int/α] ist der allgemeinste Unifikator. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Unifikation Zusammenfassung Robinson’s Unifikationsalgorithmus Der Unifikationsalgorithmus unify arbeitet auf einer Menge E = {A1 = B1 , . . . , An = Bn } von Typgleichungen und liefert allgemeinste Substitution σ, so dass Ai σ = Bi σ für alle i gilt oder einen Typfehler. Algorithmus unify: 1 unify({}) = id 2 unify({A = A} ] E ) = unify(E ) A0 - -Fertig =B→ B 0 }]E ) - -redundante Gleichung = unify({A = B, A0 = B 0 }]E ) 3 unify({A → 4 unify({α = B} ] E ) = unify({B = α} ] E ) = falls α in B erwähnt wird: Fehlermeldung “Zirkulär”, sonst berechne Komposition σ unify(E σ) für σ = [B/α] 5 In allen andern Fällen: Fehlermeldung “Typfehler” z.B. wenn (Int = Bool) oder (Int = α → β) E sind. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Unifikation Zusammenfassung Beispiele Unifikation Nur Variablen: unify{α = β, β = γ} = [β/α]unify{β = γ} = [β/α][γ/β]unify{} = [β/α][γ/β]id = [β/α][γ/β] = [γ/α, γ/β] Typkonstruktorkonflikt: unify{Int → α = α → Double} = unify{Int = α, α = Double} = [Int/α]unify{Int = Double} = ”Error: Typfehler” Zirkulärer Typ: unify{β = β, α = α → Double} = unify{α = α → Double} = ”Error: Zirkulär” Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Unifikation Zusammenfassung Typinferenzalgorithmus I inferΓ (e) = (A, σ) nimmt einen Ausdruck e in Typkontext Γ und liefert den allgemeinsten Typ A von e samt der allgemeinsten Instanziierung σ von Γ, so dass Γσ ` e :: A. 1 inferΓ (x) = wenn x::A ∈ Γ dann (A, id) sonst Fehlermeldung “Ungebundene Variable” 2 inferΓ (c) = (A, id) wobei A der Typ der Konstanten c ist. 3 inferΓ (\x -> e) = Für eine frische Typvariable α berechne zuerst inferΓ,x::α (e) = (B, σ) Damit gilt (Γσ, x::ασ) ` e :: B und wir liefern (ασ → B, σ) Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Unifikation Zusammenfassung Typinferenzalgorithmus II Applikation inferΓ (e) = (A, σ) nimmt einen Ausdruck e in Typkontext Γ und liefert den allgemeinsten Typ A von e samt der allgemeinsten Instanziierung σ von Γ, so dass Γσ ` e :: A. 4 inferΓ (e1 e2 ) = Wir berechnen zuerst inferΓ (e1 ) = (C , σ1 ) und inferΓ (e2 ) = (A, σ2 ). Für eine frische Typvariable β berechne σ3 = unify{C σ 0 = A → β}. Damit gilt Γσ1 σ2 σ3 ` e1 :: Aσ3 → βσ3 und wir liefern (βσ3 , σ1 σ2 σ3 ) zurück. Dieser Algorithmus geht auf mehrere Personen zurück: Ursprung bei Curry & Feys (1958) Erweitert und Allgemeinheit bewiesen bei Hindley (1969) Milner zeigte 1979 unabhängig das Geliche wie Hindley Erweitert unf Vollstänsigkeit bewiesen von Damas (1982) Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Typsysteme Typisierung Polymorphie Typinferenz Unifikation Zusammenfassung Zusammenfassung Well-typed programs can’t go wrong Robin Milner Typisierung in Form von Typurteilen: Γ ` e::A. Kontrolle eines Typs durch Typherleitung. Eine Typherleitung hat eine Baum-Struktur. Wenn alle Blätter Axiome sind, dann ist die Herleitung ein Beweis. Typinferenz beruht auf Unifikation. Prinzipale Typen in vielen Sprache berechenbar. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell