int gcd (int x, int y) {
while (y>0) { int z = x%y; x = y; y = z; }
return x; }gcc -S -O2 gcd.c erzeugt gcd.s:
.L3: movl %edx, %r8d ; cltd ; idivl %r8d
movl %r8d, %eax ; testl %edx, %edx
jg .L3
Ü: was bedeutet cltd, warum ist es notwendig?
Ü: welche Variable ist in welchem Register?
identischer (!) Assembler-Code für
int gcd_func (int x, int y) {
return y > 0 ? gcd_func (y,x % y) : x;
}vollständige Quelltexte: siehe Repo
Bsp Java-Kompilation: https://www.imn.htwk-leipzig.de/~waldmann/etc/safe-speed/
Bsp Haskell-Kompilation:
MicroHS (Lennart Augustsson, Haskell Symposium 2024, https://github.com/augustss/MicroHs/blob/master/doc/hs2024.pdf )
Konzepte von Programmiersprachen
Semantik von einfachen (arithmetischen) Ausdrücken
lokale Namen, \(\bullet\) Unterprogramme (Lambda-Kalkül)
Zustandsänderungen (imperative Prog.)
Continuations zur Ablaufsteuerung
realisieren durch
Interpretation, \(\bullet\) Kompilation
Hilfsmittel:
Theorie: Inferenzsysteme (f. Auswertung, Typisierung)
Praxis: Haskell, Monaden (f. Auswertung, Parser)
mit Interpreter:
Quellprogramm, Eingaben \(\stackrel{\text{Interpreter}}\longrightarrow\) Ausgaben
mit Compiler:
Quellprogramm \(\stackrel{\text{Compiler}}\longrightarrow\) Zielprogramm
Eingaben \(\stackrel{\text{Zielprogramm}}\longrightarrow\) Ausgaben
Mischform:
Quellprogramm \(\stackrel{\text{Compiler}}\longrightarrow\) Zwischenprogramm
Zwischenprogramm, Eingaben \(\stackrel{\text{virtuelle Maschine}}\longrightarrow\) Ausgaben
reale Maschine (CPU) ist Interpreter für Maschinen- sprache (Interpretation in Hardware, in Microcode)
gemeinsam ist: syntaxgesteuerte Semantik (Ausführung oder Übersetzung)
Franklyn Turbak, David Gifford, Mark Sheldon: Design Concepts in Programming Languages, MIT Press, 2008. http://cs.wellesley.edu/~fturbak/
Guy Steele, Gerald Sussman: Lambda: The Ultimate Imperative, MIT AI Lab Memo AIM-353, 1976
(the original ’lambda papers’, https://web.archive.org/web/20030603185429/http://library.readscheme.org/page1.html)
Alfred V. Aho, Monica S. Lam, Ravi Sethi and Jeffrey D. Ullman: Compilers: Principles, Techniques, and Tools (2nd edition) Addison-Wesley, 2007, http://dragonbook.stanford.edu/
J. Waldmann: Das M-Wort in der Compilerbauvorlesung, Workshop der GI-Fachgruppe Prog. Spr. und Rechnerkonzepte, 2013 http://www.imn.htwk-leipzig.de/~waldmann/talk/13/fg214/
Implementierung höherer Programmiersprachen
architekturspezifische Optimierungen (Parallelisierung, Speicherhierarchien)
Entwurf neuer Architekturen (RISC, spezielle Hardware)
Programm-Übersetzungen (Binär-Übersetzer, Hardwaresynthese, Datenbankanfragesprachen)
Software-Werkzeuge (z.B. Refaktorisierer)
domainspezifische Sprachen
pro Woche eine Vorlesung, eine Übung.
in Vorlesung, Übung und Hausaufgaben:
Theorie,
Praxis: Quelltexte (weiter-)schreiben
Prüfungszulassung: regelmäßiges und erfolgreiches Bearbeiten von Übungsaufgaben
Prüfung: Klausur (120 min, keine Hilfsmittel)
Bei Interesse und nach voriger Absprache: Ersatz eines Teiles der Klausur durch vorherige Hausarbeit
z.B. Reparaturen an autotool-Aufgaben oder anderem open-source-Projekt (Ihrer Wahl), bei denen Techniken des Compilerbaus angewendet werden
data Exp = Const Integer
| Plus Exp Exp | Times Exp Exp
deriving ( Show )
ex1 :: Exp
ex1 =
Times ( Plus ( Const 1 ) ( Const 2 ) ) ( Const 3 )
value :: Exp -> Integer
value x = case x of
Const i -> i
Plus x y -> value x + value y
Times x y -> value x * value y
das ist syntax-gesteuerte Semantik:
Wert des Terms wird aus Werten der Teilterme kombiniert
data Exp = ... | Let String Exp Exp | Ref String
ex2 :: Exp
ex2 = Let "x" ( Const 3 )
( Times ( Ref "x" ) (Ref "x" ) )
type Env = ( String -> Integer )
extend n w e = \ m -> if m == n then w else e m
value :: Env -> Exp -> Integer
value env x = case x of
Ref n -> env n
Let n x b -> value (extend n (value env x) env) b
Const i -> i
Plus x y -> value env x + value env y
Times x y -> value env x * value env y
test2 = value (\ _ -> 42) ex2
... | Let String Exp Exp — wirklich?
es gilt type String = [Char], also
einfach verkettete Liste von Zeichen
mit Bedarfsauswertung (lazy Konstruktoren)
das ist
ineffizient (in Platz und Zeit)
egal (für unseren einfachen Anwendungsfall)
gefährlich (wenn man es für andere Anwendungen übernimmt)
deswegen jetzt schon Diskussion …
von alternativen Implementierungen
und wie man diese versteckt
type String = [Char]: einfach verkettet, lazy: ist
in den allermeisten Fällen unzweckmäßig
data Vector a: Array (d.h., zusammenhängender
Speicherbereich, deswegen effiziente Indizierung) mit kostenlosem
slicing (Abschnitt-Bildung)
data Bytestring: \(\approx\) Vektor von Bytes (d.h., für rein
binären Datenaustausch)
data Text (aus Modul Data.Text)
efficient packed, immutable Unicode text type, (d.h., Zeichen
\(=\) Bytefolge)
Modul Data.Text.Lazy: lazy Liste von (strikten)
Text-Abschnitten, für Stream-Verarbeitung
Implementierung direkt sichtbar:
data Exp = ... | Let Text Exp Exp
Verschieben der Implementierungs-Entscheidung:
type Id = Text; data Exp = ... | Let Id Exp Exp
bleibt aber sichtbar (type-Deklarationen werden bei
Kompilation immer expandiert)
Verstecken der Entscheidung:
modul Id (Id) where data Id = Id Text exportiert wird
Typ-Name, aber nicht der Konstruktor
der Anwender (Importeur) von Id sieht Text
nicht
data-Deklaration mit genaue einem Konstruktor:
erstetzen durch newtype Id = Id Text
dieser kostet gar nichts (keine Zeit, keinen Platz)
alle benötigten Funktionen (einschl. Konstruktoren) für
Id implementieren und exportieren (es sind nicht viele)
eqId::Id->Id->Bool; eqId (Id s) (Id t) = s == t
diese spezifischen Namen will sich keiner merken \(\Rightarrow\) verwende standardisierte Typklassen, Bsp.
instance Eq Id where (Id s) == (Id t) = s == t
der Importeur von Id sieht den Namen (==)
bereits, weil er in Prelude definiert ist
wenn die Implementierung einer standardisierten Klasse eine einfache Delegation ist, kann sie vom Compiler erzeugt werden
newtype Id = Id Text deriving Eq
-- Implementierung des Konstruktors
import qualified Data.Text as T
fromString::String->Id;fromString s = Id (T.pack s)
-- Anwendung:
foo :: Id ; foo = fromString "bar"
der Schreibaufwand wird verringert durch
-- bei Implementierung:
import Data.String;
instance IsString Id where fromString = T.pack
-- bei Anwendung:
{-# language OverloadedStrings #-}
foo :: Id ; foo = "bar"
String-Literale sind dann überladen \(\Rightarrow\) Compiler setzt
fromString vor jedes ("bar"\(\Rightarrow\)fromString "bar")
Wiederholung Haskell
Interpreter/Compiler: ghci http://haskell.org/
Funktionsaufruf nicht f(a,b,c+d), sondern
f a b (c+d)
Konstruktor beginnt mit Großbuchstabe und ist auch eine Funktion
Wiederholung funktionale Programmierung/Entwurfsmuster
rekursiver algebraischer Datentyp (ein Typ, mehrere Konstruktoren)
(OO: Kompositum, ein Interface, mehrere Klassen)
rekursive Funktion
Wiederholung Pattern Matching:
beginnt mit case ... of, dann Zweige
jeder Zweig besteht aus Muster und Folge-Ausdruck
falls das Muster paßt, werden die Mustervariablen gebunden und der Folge-Ausdruck auswertet
Benutzung:
Beispiel für die Verdeckung von Namen bei geschachtelten Let
Beispiel dafür, daß der definierte Name während seiner Definition nicht sichtbar ist
Erweiterung:
Verzweigungen mit C-ähnlicher Semantik:
Bedingung ist arithmetischer Ausdruck, verwende 0 als Falsch und alles andere als Wahr.
data Exp = ...
| If Exp Exp Expwelche Operationen auf Id werden benötigt?
Konstruktion (fromString)
Gleichheit
Ausgabe (nur für Fehlermeldungen!)
für newtype Id = Id Text deriving Eq:
wie teuer ist Vergleich? wie könnte man das verbessern?
für type Env und extend wie angegeben:
wie teuer ist das Aufsuchen des Wertes eines Namens in einer Umgebung,
die durch \(n\) geschachtelte
extend entsteht?
wie könnte man das verbessern?
Hinweis: mit Env als Funktion: gar nicht.
Welcher andere Typ könnte verwendet werden?
inferieren \(=\) ableiten
Inferenzsystem \(I\), Objekt \(O\),
Eigenschaft \(I\vdash O\) (in \(I\) gibt es eine Ableitung für \(O\))
damit ist \(I\) eine Spezifikation einer Menge von Objekten
man ignoriert die Implementierung (\(=\) das Finden von Ableitungen)
Anwendungen im Compilerbau:
Auswertung von Programmen, Typisierung von Programmen
ein Inferenz-System \(I\) besteht aus
Regeln (besteht aus Prämissen, Konklusion)
Schreibweise \(\frac{P_1, \ldots, P_n}{K}\)
Axiomen (\(=\) Regeln ohne Prämissen)
eine Ableitung für \(F\) bzgl. \(I\) ist ein Baum:
jeder Knoten ist mit einem Objekt beschriftet
jeder Knoten (mit Vorgängern) entspricht Regel von \(I\)
Wurzel (Ziel) ist mit \(F\) beschriftet
Def: \(I \vdash F\) \(:\iff\) \(\exists\) \(I\)-Ableitungsbaum mit Wurzel \(F\).
um unendliche Menge zu beschreiben, benötigt man unendliche Regelmengen
diese möchte man endlich notieren
ein Regel-Schema beschreibt eine (mglw. unendliche) Menge von Regeln, Bsp: \(\displaystyle\frac{(x,y)}{(x-y,y)}\)
Schema wird instantiiert durch Belegung der Schema-Variablen
Bsp: Belegung \(x\mapsto 13, y\mapsto 5\)
ergibt Regel \(\displaystyle\frac{(13,5)}{(8,5)}\)
Grundbereich \(=\) Zahlenpaare \(\mathbb{Z}\times\mathbb{Z}\)
Axiom: \(\displaystyle \frac{}{(13,5)}\)
Regel-Schemata: \(\displaystyle \frac{(x,y)}{(x-y,y)}\), \(\displaystyle \frac{(x,y)}{(x,y-x)}\)
gilt \(I\vdash (1,1)\) ?
Ü: Beziehung zu einem alten Algorithmus (früh im Studium, früh in der Geschichte der Menschheit)
Satz: \(p \in \mathbb{P}\iff\) \(\exists g:\) \(g\) ist primitive Wurzel mod \(p\):
\([g^0,g^1,g^2,\dots,g^{p-2}]\) ist Permutation von \([1, 2, \ldots,p-1]\)
let {p = 7; g = 3}
in map (`mod` p) $ take (p-1) $ iterate (*g) 1
[1,3,2,6,4,5]
Inferenzregel \(\displaystyle \frac{g_1:p_1,\ldots, g_k:p_k}{g:p}\),
falls \(p-1=q_1^{e_1}\cdots q_k^{e_k}\) und \(\forall i: g^{(p-1)/q_i}\neq 1 \mod p\)
Vaughan Pratt, Each Prime has a Succinct Certificate, SIAM J. Comp. 1975
es folgt \(\mathbb{P}\in \textsf{NP}\cap \textsf{co-NP}\), aber \(\mathbb{P}\) not known to be in \(\textsf{P}\)
Agrawal, Kayal, Saxena: Primes is in P, 2004
Grundbereich: disjunktive Klauseln
Inferenz-Regel: \(\displaystyle \frac{p_1\vee\dots\vee p_i\vee q, ~ \neg q\vee r_1\vee\dots\vee r_j} { p_1\vee\dots\vee p_i\vee r_1\vee\dots\vee r_j}\)
Beispiel: \(\{p\vee q,\neg q\vee r\}\vdash p\vee r\)
Def. Formel \(F\) folgt aus Formelmenge \(M\): \(M\models F :=\)
\(\forall b: ( \forall G\in M: \operatorname{\sfseries Wert}(G,b)=1) \Rightarrow \operatorname{\sfseries Wert}(F,b)=1\)
Beziehungen zw. Syntax (Resolution) und Semantik (Folgerung)
Resolution ist korrekt: \((M \vdash F) \Rightarrow (M\models F)\)
Resolution ist widerlegungsvollständig: \((M\models \emptyset)\Rightarrow (M\vdash \emptyset)\)
später implementieren wir das, als statische Analyse im Interpreter/Compiler,
jetzt geben wir nur die Regel an: \(\displaystyle \frac{f:T_1\to T_2, x:T_1}{f x:T_2}\)
Bsp. für Verwendung eines Inferenzsystems: Manuel Chakravarty, Gabriele Keller, Simon Peyton Jones, Simon Marlow, Associated Types with Class, POPL 2005
Absch. 4.2 (Fig. 2) Grundbereich: \(\Theta|\Gamma\vdash e:\sigma\)
means that in type environment \(\Gamma\) and instance environment \(\Theta\) the expression \(e\) has type \(\sigma\)
Bsp. für ein Regelschema: \(\displaystyle \frac{(v:\sigma)\in\Gamma}{\Theta|\Gamma\vdash v:\sigma} (\textsl{var})\)
Grundbereich: Aussagen \(\operatorname{\mathsf{wert}}(p,z)\) mit \(p \in \texttt{Exp}\) , \(z\in \mathbb{Z}\)
data Exp = Const Integer
| Plus Exp Exp | Times Exp ExpAxiome: \(\operatorname{\mathsf{wert}}(\texttt{Const} z,z)\)
Regeln:
\(\displaystyle \frac{\operatorname{\mathsf{wert}}(X,a),\operatorname{\mathsf{wert}}(Y,b)} {\operatorname{\mathsf{wert}}(\texttt{Plus} ~ X ~ Y,a+b)}, \quad \frac{\operatorname{\mathsf{wert}}(X,a),\operatorname{\mathsf{wert}}(Y,b)} {\operatorname{\mathsf{wert}}(\texttt{Times} ~ X ~ Y,a\cdot b)}, \dots\)
das ist syntaxgesteuerte Semantik:
für jeden Konstruktor von \(p\in
\texttt{Exp}\)
gibt es genau eine Regel mit Konklusion \(\operatorname{\mathsf{wert}}(p,\dots)\)
Grundbereich: Aussagen der Form \(\operatorname{\mathsf{wert}}(E,p,z)\)
(in Umgebung \(E\) hat Programm \(p\) den Wert \(z\))
Umgebungen konstruiert aus \(\emptyset\) und \(E[v:=b]\)
Regeln für Operatoren \(\displaystyle \frac{\operatorname{\mathsf{wert}}(E,X,a),\operatorname{\mathsf{wert}}(E,Y,b)} {\operatorname{\mathsf{wert}}(E,\texttt{Plus} X Y,a+b)},\dots\)
Regeln für Umgebungen \(\displaystyle \frac{}{\operatorname{\mathsf{wert}}(E[v:=b],\mathtt{Ref}~v,b)},\)
\(\displaystyle \frac{\operatorname{\mathsf{wert}}(E,\mathtt{Ref}~v',b')}{\operatorname{\mathsf{wert}}(E[v:=b],\mathtt{Ref}~v',b')}\) für \(v \neq v'\)
Regeln für Bindung: \(\displaystyle \frac{\operatorname{\mathsf{wert}}(E,X,b), \operatorname{\mathsf{wert}}(E[v:=b],Y,c)}{\operatorname{\mathsf{wert}}(E,\mathtt{let~} v=X \mathtt{~in~} Y,c)}\)
Umgebung ist (partielle) Funktion von Name nach Wert
Realisierungen: type Env = String -> Integer
Operationen:
empty :: Env leere Umgebung
lookup :: Env -> String -> Integer
Notation: \(e(x)\)
extend :: String -> Integer -> Env -> Env
Notation: \(e[v := z]\)
Beispiel
lookup (extend "y" 4 (extend "x" 3 empty)) "x"
entspricht \((\emptyset[x:=3][y:=4]) x\)
Primalitäts-Zertifikate
welche von \(2,4,8\) sind primitive Wurzel mod 101?
vollst. Primfaktorzerlegung von 100 angeben
ein vollst. Prim-Zertifikat für 101 angeben.
bestimmen Sie \(2^{(101-1)/5} \mod 101\) von Hand
Hinweise: 1. das sind nicht 20 Multiplikationen,
2. es wird nicht mit riesengroßen Zahlen gerechnet.
Geben Sie den vollständigen Ableitungsbaum an für die Auswertung von
let {x = 5} in let {y = 7} in x
warum ist aussagenlog. Resolution nicht vollständig? (es gilt nicht: \((M\models F)\Rightarrow(M\vdash F)\).) Ein einfaches Gegenbeispiel reicht.
ein Paper aus POPL heraussuchen, das Inferenzsysteme verwendet zur Beschreibung von statischer oder dynamische Semantik einer Programmiersprache
bisher: Wert eines arithmetischen Ausdrucks ist Zahl.
jetzt erweitern (Motivation: if-then-else mit richtigem Typ):
data Val = ValInt Int
| ValBool Booltypische Verarbeitung:
value env x = case x of
Plus l r ->
case value env l of
ValInt l ->
case value env r of
ValInt r ->
ValInt ( i + j )
Programmablauf-Abstraktion durch Continuations:
with_int :: Val -> (Int -> Val) -> Val
with_int v k = case v of
ValInt i -> k i
_ -> error "expected ValInt"
k ist die continuation (die Fortsetzung im
Erfolgsfall)
eben geschriebenen Code refaktorisieren zu:
value env x = case x of
Plus l r ->
with_int ( value env l ) $ \ i ->
with_int ( value env r ) $ \ j ->
ValInt ( i + j )
Bool im Interpreter
Boolesche Literale
relationale Operatoren (==, <,
o.ä.),
Inferenz-Regel(n) für Auswertung des If
Implementierung der Auswertung von if/then/else mit
with_bool,
Striktheit der Auswertung
einen Ausdruck e :: Exp angeben, für den
value undefined e eine Exception ist (zwei mögliche Gründe:
nicht gebundene Variable, Laufzeit-Typfehler)
mit diesem Ausdruck: diskutiere Auswertung von
let {x = e} in 42
bessere Organisation der Quelltexte
Cabalisierung (Quelltexte in src/,
Projektbeschreibungsdatei cb.cabal), Anwendung:
cabal repl usw.
separate Module für Exp, Env,
Value,
in verschiedenen Prog.-Sprachen gibt es verschiedene Formen von Unterprogrammen:
Prozedur, sog. Funktion, Methode, Operator, Delegate, anonymes Unterprogramm
allgemeinstes Modell:
Kalkül der anonymen Funktionen (Lambda-Kalkül),
abstrakte Syntax:
data Exp = ...
| Abs { par :: Name , body :: Exp }
| App { fun :: Exp , arg :: Exp }konkrete Syntax:
let { f = \ x -> x * x } in f (f 3)konkrete Syntax (Alternative):
let { f x = x * x } in f (f 3)erweitere den Bereich der Werte:
data Val = ... | ValFun ( Val -> Val )erweitere Interpreter:
value :: Env -> Exp -> Val
value env x = case x of
... | Abs n b -> _ | App f a -> _mit Hilfsfunktion with_fun :: Val -> ...
Testfall (in konkreter Syntax)
let { x = 4 } in let { f = \ y -> x * y }
in let { x = 5 } in f xlet { x = A } in Q
kann übersetzt werden in
(\ x -> Q) A
let { x = a , y = b } in Q
wird übersetzt in …
beachte: das ist nicht das let aus Haskell
…simulieren durch einstellige:
mehrstellige Abstraktion:
\ x y z -> B := \x -> (\y -> (\z -> B ))mehrstellige Applikation:
f P Q R := ((f P) Q) R
(die Applikation ist links-assoziativ)
der Typ einer mehrstelligen Funktion:
T1 -> T2 -> T3 -> T4 :=
T1 -> (T2 -> (T3 -> T4))
(der Typ-Pfeil ist rechts-assoziativ)
bisher: ValFun ist Funktion als Datum der
Gastsprache
value env x = case x of ...
Abs n b -> ValFun $ \ v ->
value (extend n v env) b
App f a ->
with_fun ( value env f ) $ \ g ->
with_val ( value env a ) $ \ v -> g valternativ: Closure: enthält Umgebung env und Code
b
value env x = case x of ...
Abs n b -> ValClos env n b
App f a -> ...Closure konstruieren (Axiom-Schema):
\(\displaystyle \frac{ }{\operatorname{\mathsf{wert}}(E,\lambda n.b,\operatorname{\mathsf{Clos}}(E,n,b)) }\)
Closure benutzen (Regel-Schema, 3 Prämissen)
\(\displaystyle \frac{ \begin{array}{c} \operatorname{\mathsf{wert}}(E_1,f,\operatorname{\mathsf{Clos}}(E_2,n,b)), \\ \operatorname{\mathsf{wert}}(E_1,a,w), \operatorname{\mathsf{wert}}(E_2[n:=w],b,r) \end{array} } {\operatorname{\mathsf{wert}}(E_1,f a,r)}\)
Ü: Inferenz-Baum für Auswertung des vorigen Testfalls (geschachtelte Let) zeichnen
…oder Interpreter so erweitern, daß dieser Baum ausgegeben wird
Das geht nicht, und soll auch nicht gehen:
let { x = 1 + x } in xaber das hätten wir doch gern:
let { f = \ x -> if x > 0
then x * f (x -1) else 1
} in f 5
(nächste Woche)
aber auch mit nicht rekursiven Funktionen kann man interessante Programme schreiben:
let { t f x = f (f x) }
in let { s x = x + 1 }
in t t t t s 0
auf dem Papier den Wert bestimmen
mit selbstgebautem Interpreter ausrechnen
mit Haskell ausrechnen
in JS (node) ausrechnen
Fehler explizit im semantischen Bereich des Interpreters repräsentieren (anstatt als Exception der Gastsprache)
data Val = ... | ValErr Text
strikte Semantik: ValErr niemals in
Umgebung (bei Let-Bindung oder UP-Aufruf)
Ü: realisieren durch Aufruf (an geeigneten Stellen) von
with_val :: Val -> (Val -> Val) -> Val
with_val v k = case v of
ValErr _ -> v
_ -> k v
eingebaute primitive Rekursion (Induktion über Peano-Zahlen):
implementieren Sie die Funktion
fold :: r -> (r -> r) -> N -> r
Testfall: fold 1 (\x -> 2*x) 5 == 32
durch data Exp = .. | Fold .. und neuen Zweig in
value
Wie kann man damit die Fakultät implementieren?
alternative Implementierung von Umgebungen
bisher type Env = Id -> Val
jetzt type Env = Data.Map.Map Id Val oder
Data.HashMap
Messung der Auswirkungen: 1. Laufzeit eines Testfalls, 2. Laufzeiten einzelner UP-Aufrufe (profiling)
1. Modellierung von Funktionen:
intensional: Fkt. ist Berechnungsvorschrift, Programm
(extensional: Fkt. ist Menge v. geordneten Paaren)
2. Notation mit gebundenen (lokalen) Variablen, wie in
Analysis: \(\int x^2 \operatorname{d}x, \sum_{k=0}^n k^2\)
Logik: \(\forall x \in A: \forall y\in B: P(x,y)\)
Programmierung:
static int foo (int x) { ... }
ist der Kalkül für Funktionen mit benannten Variablen
Alonzo Church, 1936 …Henk Barendregt, 1984 …
die wesentliche Operation ist das Anwenden einer Funktion:
\[(\lambda x . B) A \to_\beta B[x:=A]\]
Beispiel: \((\lambda x.x*x)(3+2) \to_\beta(3+2)*(3+2)\)
Im reinen Lambda-Kalkül gibt es nur Funktionen
(keine Zahlen, Wahrheitswerte usw.)
Menge \(\Lambda\) der Lambda-Terme
(mit Variablen aus einer Menge \(V\)):
(Variable) wenn \(x \in V\), dann \(x\in \Lambda\)
(Applikation) wenn \(F\in \Lambda, A\in \Lambda\), dann \((F A)\in\Lambda\)
(Abstraktion) wenn \(x\in V,B\in\Lambda\), dann \((\lambda x.B)\in\Lambda\)
Beispiele: \(x, (\lambda x.x), ((x z) (y z)), (\lambda x.(\lambda y.(\lambda z.((x z) (y z)))))\)
verkürzte Notation (Klammern weglassen)
\((\dots ((F A_1) A_2) \dots A_n) \sim F A_1 A_2 \dots A_n\)
\(\lambda x_1.(\lambda x_2. \dots (\lambda x_n.B)\dots) \sim \lambda x_1 x_2 \dots x_n . B\)
mit diesen Abkürzungen simuliert \((\lambda x_1 \dots x_n.B)A_1 \dots A_n\) eine mehrstellige Funktion und -Anwendung
\(\to_\beta\) auf \(\Lambda\) ist nicht terminierend (es gibt Terme mit unendlichen Ableitungen)
\(W=\lambda x.xx, \Omega = WW\).
es gibt Terme mit Normalform und unendlichen Ableitungen, \(KI\Omega\) mit \(K=\lambda xy.x, I=\lambda x.x\)
\(\to_\beta\) auf \(\Lambda\) ist konfluent
\(\forall A,B,C\in\Lambda: A\to_\beta^*B \wedge A\to_\beta^* C \Rightarrow \exists D\in\Lambda: B\to_\beta^*D \wedge C\to_\beta^* D\)
Folgerung: jeder Term hat höchstens eine Normalform
\(\lambda\)-Kalkül: Rel. \(\to_\beta\) substituiert Variablen im Term
schwache Reduktion: wie \(\to_\beta\), aber niemals unter \(\lambda\)
unser Interpreter: realisiert schwache Reduktion, Regeln für \(\operatorname{\mathsf{wert}}(E,X,w)\) speichern Substitutionen in Umgebung
ein Zusammenhang wird hergestellt durch Kalküle für explizite Substitutionen,
Pierre-Louis Curien: An Abstract Framework for Environment Machines, TCS 82 (1991),
Abadi, Cardelli, Curien, Levy: Explicit Substitutions, JFP 1991,
Simulation von Daten (Tupel)
durch Funktionen (Lambda-Ausdrücke):
Konstruktor: \(\langle D_1,\ldots,D_k\rangle := \lambda s. s D_1 \ldots D_k\)
Selektoren: \(s_i^k := \lambda t . t (\lambda d_1 \ldots d_k.d_i)\)
es gilt \(s_i^k \langle D_1, \ldots, D_k\rangle \to_\beta^* D_i\)
Ü: überprüfen für \(k=2\)
Anwendungen:
Modellierung von Listen, Zahlen
Auflösung simultaner Rekursion
Wahrheitswerte:
\(\operatorname{\textsf{True}}:= \lambda x y.x, \operatorname{\textsf{False}}:= \lambda x y .y\)
Verzweigung: \(\verb|if| ~b~ \verb|then|~ x~ \verb|else| ~ y := b x y\)
natürliche Zahlen als iterierte Paare (Ansatz)
\((0) := \langle\operatorname{\textsf{True}},\lambda x.x\rangle; ~ (n+1) := \langle\operatorname{\textsf{False}},n\rangle\)
\(s^2_2\) ist partielle Vorgänger-Funktion: \(s^2_2(n+1) = n\)
Verzweigung: \(\verb|if| ~a=0~ \verb|then|~ x~ \verb|else| ~ y := s^2_1 a x y\)
Ü: nachrechnen. Ü: das geht sogar mit \((0)=\lambda x.x\)
Rekursion?
Beispiel: die Fakultät
f = \ x -> if x=0 then 1 else x*f(x-1)
erhalten wir als Fixpunkt einer Fkt. 2. Ordnung
g = \ h x -> if x=0 then 1 else x * h(x-1)
f = fix g -- d.h., f = g f
Ü: \(g (\lambda z.z) 7\), Ü: \(\texttt{fix}~g~7\)
Implementierung von fix mit Rekursion:
fix g = g (fix g)es geht aber auch ohne Rekursion. Ansatz: \(\texttt{fix}= A A\),
dann \(\texttt{fix}~g = A A g =g (A A g) = g(\texttt{fix}~g)\)
eine Lösung ist \(A = \lambda x y . \dots\)
Definition (der Fixpunkt-Kombinator von Turing)
\(\Theta =(\lambda x y. (y (x x y))) (\lambda x y. (y (x x y)))\)
Satz: \(\Theta f \to_\beta^* f(\Theta f)\), d. h. \(\Theta f\) ist Fixpunkt von \(f\)
Folgerung: im Lambda-Kalkül kann man simulieren:
Daten: Zahlen, Tupel von Zahlen
Programmablaufsteuerung durch:
Nacheinanderausführung:
Verkettung von Funktionen
Verzweigung,
Wiederholung: durch Rekursion (mit Fixpunktkomb.)
Satz: (Church, Turing)
Menge der Turing-berechenbaren Funktionen
(Zahlen als Wörter auf Band)
Alan Turing: On Computable Numbers, with an Application to the Entscheidungsproblem, Proc. LMS, 2 (1937) 42 (1) 230–265 https://dx.doi.org/10.1112/plms/s2-42.1.230
Menge der Lambda-berechenbaren Funktionen
(Zahlen als Lambda-Ausdrücke)
Alonzo Church: A Note on the Entscheidungsproblem, J. Symbolic Logic 1 (1936) 1, 40–41
Menge der while-berechenbaren Funktionen
(Zahlen als Registerinhalte)
\(c:\mathbb{N}\to\Lambda: n \mapsto \lambda f x.f^n(x)\)
mit \(f^0(x) := x, f^{n+1}(x):= f(f^n(x))\)
in Haskell: c n f x = iterate f x !! n
Decodierung: d e = e (\x -> x+1) 0
Nachfolger: \(s(c_n)=c_{n+1}\) für \(s =\lambda n f x. f (n f x)\)
1. auf Papier beweisen, 2. mit leancheck prüfen
benutze check $ \ (Natural x) -> ...
Addition: \(\texttt{plus} ~ c_a~ c_b=c_{a+b}\) für \(\texttt{plus} = \lambda a b f x . a f (b f x)\)
implementiere die Multiplikation, beweise, prüfe
Potenz: \(\texttt{pow} ~c_a ~c_b=c_{a^b}\) für \(\texttt{pow}=\lambda a b.b a\)
Claude Heiland-Allen: GULCII, my Graphical Untyped Lambda Calculus Interactive Interpreter,
https://mathr.co.uk/blog/2017-10-26_gulcii_in_edinburgh.html
vorgeführt auf FARM 2017 (Workshop on Functional Art, Music, Modeling and Design), Oxford https://functional-art.org/2017/
Inhalt:
Kodierung von Zahlen nach Church
…Scott (einfacher?, aber mit Rekursion/Fixpunkt)
Equivalenz dieser Kodierungen
Erweiterung der abstrakten Syntax:
data Exp = ... | Rec Name ExpBeispiel
App
(Rec g (Abs v (if v==0 then 0 else 2 + g(v-1))))
5
Bedeutung: Rec x B ist Fixpunkt von \((\lambda x.B)\)
Semantik: \(\displaystyle \frac{\operatorname{\mathsf{wert}}(E,(\lambda x.B)(\operatorname{\textsf{Rec}}~x~B),v)} {\operatorname{\mathsf{wert}}(E,\operatorname{\textsf{Rec}}~x~B,v)}\)
Ü: verwende Let statt
App (Abs ..) ..
Ü: das Beispiel mit dieser Regel auswerten
bisher: Simulation von Tupeln (Konstruktor, Selektor) durch Funktionen
jetzt: Realisierung im Interpreter
abstrakte Syntax:
data Exp = ..
| Tuple [Exp]
| Select Integer Exp
Index für Select ist Literal (nicht Exp), damit wir später statisch typisieren können
Werte: data Val = .. | ValTuple [Val]
Anwendung: Tupel realisiert Umgebung (nur Werte, ohne Namen) zur Laufzeit der Zielsprache der Kompilation
Beispiel (aus: D. Hofstadter, Gödel Escher Bach, 1979)
letrec { f = \ x -> if x == 0 then 1
else x - g(f(x-1))
, g = \ x -> if x == 0 then 0
else x - f(g(x-1))
} in f 15
Bastelaufgabe: für welche \(x\) gilt \(f(x)\neq g(x)\)?
weitere Beispiele:
letrec { y = x * x, x = 3 + 4 } in x - y
letrec { f = \ x -> .. f (x-1) } in f 3Teilausdrücke (für jedes \(i\))
let { n1 = select1 t, .. nk = selectk t
} in xi
äquivalent vereinfachen zu
t (\ n1 .. nk -> xi)
LetRec [(n1,x1), .. (nk,xk)] y
=> ( rec t
( tuple ( t ( \ n1 .. nk -> x1 ))
...
( t ( \ n1 .. nk -> xk )) ))
( \ n1 .. nk -> y )Ü: implementiere
letrec {f = _, g = _} in f 15
die Fakultät (z.B. von 7) …
in Haskell (ohne Rekursion, aber mit
Data.Function.fix)
in unserem Interpreter (ohne Rekursion, mit Turing-Fixpunktkombinator \(\Theta\))
in Javascript (ohne Rekursion, mit \(\Theta\))
Kodierung von Wahrheitswerten und Zahlen (nach Church)
implementiere Test auf 0: \(\texttt{iszero}~c_n=\) if \(n=0\) then \(\operatorname{\textsf{True}}\) else \(\operatorname{\textsf{False}}\)
implementiere Addition, Multiplikation, Fakultät
ohne If, Eq, Const,
Plus, Times
für nützliche Ausgaben: das Resultat nach ValInt
dekodieren (dabei muß Plus und Const benutzt
werden)
folgende Aufgaben aus H. Barendregt: Lambda Calculus
(Abschn. 6.1.5) gesucht wird \(F\) mit \(F x y = F y x F\).
Musterlösung: es gilt \(F = \lambda x y . F y x F = (\lambda f x y .f y x f)F\),
also \(F=\Theta (\lambda f x y .f y x f)\)
(Aufg. 6.8.2) Konstruiere \(K^\infty \in\Lambda^0\) (ohne freie Variablen) mit \(K^\infty x = K^\infty\) (hier und in im folgenden hat \(=\) die Bedeutung \((\to_\beta\cup \to_\beta^-)^*\)
Konstruiere \(A\in\Lambda^0\) mit \(A x = x A\)
beweise den Doppelfixpunktsatz (Kap. 6.5)
\(\forall F,G : \exists A,B: A = FAB \wedge B = GAB\)
(Aufg. 6.8.17, B. Friedman) Konstruiere Null, Nulltest, partielle Vorgängerfunktion für Zahlensystem mit Nachfolgerfunktion \(s=\lambda x.\langle x\rangle\) (das 1-Tupel)
(Aufg. 6.8.14, J. W. Klop) \[\begin{aligned} &&X = \lambda abcdefghijklmnopqstuvvxyzr. \\ && \qquad r(thisisafixedpointcombinator) \\ &&Y = X^{27}=\underbrace{X\dots X}_{27} \end{aligned}\] Zeige, daß \(Y\) ein Fixpunktkombinator ist.
bisherige Programme sind nebenwirkungsfrei, das ist nicht immer erwünscht:
direktes Rechnen auf von-Neumann-Maschine: Änderungen im Hauptspeicher
direkte Modellierung von Prozessen mit Zustandsänderungen ((endl.) Automaten)
Dazu muß semantischer Bereich geändert werden.
bisher: Val, jetzt:
State -> (State, Val)
(dabei ist (A,B) die Notation für \(A\times B\))
Semantik von (Teil-)Programmen ist Zustandsänderung.
Implementierung benutzt größenbalancierte Suchbäume
Notation mit qualifizierten Namen:
import qualified Data.Map as M
newtype Addr = Addr Int
type Store = M.Map Addr Valnewtype: wie data mit genau einem
Konstruktor,
Konstruktor wird zur Laufzeit nicht repräsentiert
Aktion: liefert neue Speichernbelegung und Resultat
newtype Action a =
Action ( Store -> ( Store, a ))newtype Action a = Action ( Store -> ( Store, a ))
spezifische Aktionen:
new :: Val -> Action Addr
get :: Addr -> Action Val
put :: Addr -> Val -> Action ()Aktion ausführen, Resultat liefern (Zielzustand ignorieren)
run :: Store -> Action a -> aAktionen nacheinander ausführen
bind :: Action a -> ... Action b -> Action b
Aktion ohne Zustandsänderung
result :: a -> Action a
Ausdrücke (mit Nebenwirkungen):
date Exp = ...
| New Exp | Get Exp | Put Exp ExpResultattyp des Interpreters ändern:
value :: Env -> Exp -> Val
:: Env -> Exp -> Action Valsemantischen Bereich erweitern:
data Val = ... | ValAddr AddrAufruf des Interpreters:
run Store.empty $ value env0 $ ...bisher:
with_int :: Val -> ( Int -> Val ) -> Val
with_int v k = case v of
ValInt i -> k ijetzt:
with_int :: Action Val
-> ( Int -> Action Val ) -> Action Val
with_int a k = bind a $ \ v -> case v of
ValInt i -> k iHauptprogramm muß kaum geändert werden (!)
in unserem Modell haben wir:
veränderliche Speicherstellen,
aber immer noch unveränderliche Variablen (lokale Namen)
\(\Rightarrow\) der Wert eines Namens kann eine Speicherstelle sein, aber dann immer dieselbe.
es fehlen noch wesentliche Operatoren:
Nacheinanderausführung (Sequenz)
Wiederholung (Schleife)
diese kann man:
simulieren (durch let)
als neue AST-Knoten realisieren (Übung)
mehrere Möglichkeiten zur Realisierung
im Lambda-Kalkül (in der interpretierten Sprache)
mit Fixpunkt-Kombinator
durch Rekursion in der Gastsprache des Interpreters
simulieren (in der interpretierten Sprache)
durch Benutzung des Speichers
Idee: eine Speicherstelle anlegen und als Vorwärtsreferenz auf das Resultat der Rekursion benutzen
Rec n (Abs x b) ==>
a := new 42
put a ( \ x -> let { n = get a } in b )
get aFakultät imperativ:
let { fak = \ n ->
{ a := new 1 ;
while ( n > 0 )
{ a := a * n ; n := n - 1; }
return a;
}
} in fak 5
Schleife durch Rekursion ersetzen und Sequenz durch
let:
fak = let { a = new 1 }
in Rec f ( \ n -> ... )Syntaxbaumtyp erweitern um Knoten für Sequenz und Schleife
unsere Motivation: semantischer Bereich,
result :: a -> m a als wirkungslose Aktion,
Operator bind :: m a -> (a -> m b) -> m b zum
Verknüpfen von Aktionen
auch nützlich: do-Notation (anstatt Ketten von
>>=)
die Wahrheit: a monad in X is just a monoid
in the category of endofunctors of X
die ganze Wahrheit:
Functor m => Applicative m => Monad m
weitere Anwendungen: IO, Parser-Kombinatoren,
weitere semant. Bereiche (Continuations, Typisierung)
Definition (in Standardbibliothek)
class Monad m where
return :: a -> m a
( >>= ) :: m a -> (a -> m b) -> m bInstanz (für benutzerdefinierten Typ)
instance Monad Action where
return = result ; (>>=) = bindBenutzung der Methoden:
value e l >>= \ a ->
value e r >>= \ b ->
return ( a + b )value e l >>= \ a ->
value e r >>= \ b ->
return ( a + b )
do-Notation (explizit geklammert):
do { a <- value e l
; b <- value e r
; return ( a + b )
}
do-Notation (implizit geklammert):
do a <- value e l
b <- value e r
return ( a + b )
Haskell: implizite Klammerung nach let, do,
case, where
Aktionen mit Speicheränderung (vorige Woche)
Action (Store -> (Store, a))
Aktionen mit Welt-Änderung: IO a
Transaktionen (Software Transactional Memory)
STM a
Vorhandensein oder Fehlen eines Wertes
data Maybe a = Nothing | Just a
…mit Fehlermeldung
data Either e a = Left e | Right a
Nichtdeterminismus (eine Liste von Resultaten):
[a]
Parser-Monade (nächste Woche)
data IO a -- abstract
instance Monad IO -- eingebaut
readFile :: FilePath -> IO String
putStrLn :: String -> IO ()
Alle Funktionen, deren Resultat von der Außenwelt (Systemzustand)
abhängt, haben Resultattyp IO ..., sie sind tatsächlich
Aktionen.
Am Typ einer Funktion erkennt man ihre möglichen Wirkungen bzw. deren garantierte Abwesenheit.
main :: IO ()
main = do
cs <- readFile "foo.bar" ; putStrLn cs
Kategorie \(C\) hat Objekte \(\operatorname{\text{Obj}}_C\) und Morphismen \(\operatorname{\text{Mor}}_C\),
jeder Morphismus \(m\) hat als Start (\(S\)) und Ziel (\(T\)) ein Objekt, Schreibweise: \(m:S\to T\) oder \(m\in\operatorname{\text{Mor}}_C(S,T)\)
für jedes \(O\in\operatorname{\text{Obj}}_C\) gibt es \(\operatorname{\text{id}}_O:O\to O\)
für \(f:S\to M\) und \(g:M\to T\) gibt es \(f\circ g:S\to T\).
es gilt immer \(f\circ \operatorname{\text{id}}=f\), \(\operatorname{\text{id}}\circ g=g\), \(f\circ(g\circ h)=(f\circ g)\circ h\)
Beispiele:
Set: \(\operatorname{\text{Obj}}_{\operatorname{\text{Set}}}\) = Mengen, \(\operatorname{\text{Mor}}_{\operatorname{\text{Set}}}\) = totale Funktionen
Grp: \(\operatorname{\text{Obj}}_{\operatorname{\text{Grp}}}\) = Gruppen, \(\operatorname{\text{Mor}}_{\operatorname{\text{Grp}}}\) = Homomorphismen
für jede Halbordnung \((M,\le)\): \(\operatorname{\text{Obj}}=M,\operatorname{\text{Mor}}=(\le)\)
Hask: \(\operatorname{\text{Obj}}_{\operatorname{\text{Hask}}}\) = Typen, \(\operatorname{\text{Mor}}_{\operatorname{\text{Hask}}}\) = Funktionen
Beispiel: Isomorphie
eigentlich: Abbildung, die die Struktur (der abgebildeten Objekte) erhält
Struktur von \(O\in \operatorname{\text{Obj}}(C)\) ist aber unsichtbar
Eigenschaften von Objekten werden beschrieben
durch Eigenschaften ihrer Morphismen
(vgl. abstrakter Datentyp, API)
Bsp: \(f:A\to B\) ist
Isomorphie (kurz: ist iso),
falls es ein \(g:B\to A\) gibt mit
\({f\circ g=\operatorname{\text{id}}_A }\wedge
{g\circ f=\operatorname{\text{id}}_B}\)
weiteres Beispiel
Def: \(m:a\to b\) monomorph: \(\forall f,g: f\circ m=g\circ m\Rightarrow f=g\)
Satz: \(f\) mono \(\wedge\) \(g\) mono \(\Rightarrow\) \(f\circ g\) mono.
Def: \(P\) ist Produkt
von \(A_1\) und \(A_2\)
mit Projektionen \(\operatorname{\text{proj}}_1:P\to A_1,
\operatorname{\text{proj}}_2:P\to A_2\),
wenn für jedes \(B\) und Morphismen \(f_1:B\to A_1, f_2:B\to A_2\)
es existiert genau ein \(g:B\to
P\)
mit \(g\circ\operatorname{\text{proj}}_1=f_1\)
und \(g\circ\operatorname{\text{proj}}_2=f_2\)
für Set ist das wirklich das Kreuzprodukt
für die Kategorie einer Halbordnung?
für Gruppen? (Beispiel?)
Wenn ein Begriff kategorisch definiert ist,
erhält man den dazu dualen Begriff
durch Spiegeln aller Pfeile
Bsp: dualer Begriff zu Produkt (heißt Summe)
Definition hinschreiben, Beispiele angeben
Ü: dualer Begriff zu: mono (1. allgemein, 2. Mengen)
entsprechend: die duale Aussage
diese gilt gdw. die originale (primale) Aussage gilt
Ü: duale Aussage für Komposition von mono.
SS 24 : zur VL KW 22: Aufgaben 1, 2, 3 (b, c)
der identische Morphismus jedes Objektes ist eindeutig (das bedeutet: falls es \(\operatorname{\text{id}}_1, \operatorname{\text{id}}_2\) gibt mit \(\forall f: \operatorname{\text{id}}_k\circ f = f = f \circ \operatorname{\text{id}}_k\), dann \(\operatorname{\text{id}}_1 = \operatorname{\text{id}}_2\))
Definieren Sie den duale Begriff zu Monomorphismus. Geben Sie Beispiele und Gegenbeispiele für Monom. und duale Monom. in der Kategorie der Mengen an
Beweise: für das (kategorisch definierte) Produkt \(P\) von endlichen Mengen \(A_1,A_2\) gilt: \(|P|=|A_1\times A_2|\).
(Musterlösung) \(|P|\ge |A_1\times A_2|\): indirekter Beweis.
Falls \(|P|< |A_1\times A_2|\), wähle \(B=A_1\times A_2\), \(f_i(x_1,x_2)=x_i\).
Dann ist \(g\) nicht injektiv: es gibt \(x=(x_1,x_2)\neq(y_1,y_2)=y\), mit \(g(x)=g(y)\).
Für diese \(x,y\) gilt \(x_i\neq y_i\) für wenigstens ein \(i\in\{1,2\}\).
Aus \(g\circ\operatorname{\text{proj}}_i=f_i\) folgt \(x_i=f_i(x)=(g\circ \operatorname{\text{proj}}_i)(x)=\operatorname{\text{proj}}_i(g(x)) = \operatorname{\text{proj}}_i(g(y)) = (g\circ \operatorname{\text{proj}}_i)(y) = f_i(y)=y_i\), Widerspruch.
(Aufgabe) \(|P|\le |A_1\times A_2|\)
Geben Sie eine entsprechende Aussage für das duale Produkt endlicher Mengen an und beweisen Sie diese.
Kategorie mit gerichteten Graphen als Objekte,
\(f:\operatorname{\textsf{V}}(G)\to \operatorname{\textsf{V}}(H)\) ist Morphismus, falls \(\forall x,y: (x,y)\in \operatorname{\textsf{E}}(G) \iff (f(x),f(y))\in \operatorname{\textsf{E}}(H)\).
Was bedeuten dort mono, epi?
Produkt, Summe? Welche Graph-Operationen (aus VL Modellierung) haben die passenden Eigenschaften?
Def: Funktor \(F\) von Kategorie \(C\) nach Kategorie \(D\):
Wirkung auf Objekte: \({F_{\operatorname{\text{Obj}}}}:\operatorname{\text{Obj}}(C)\to\operatorname{\text{Obj}}(D)\)
Wirkung auf Pfeile: \({F_{\operatorname{\text{Mor}}}}: (g:s\to t)\mapsto (g':{F_{\operatorname{\text{Obj}}}}(S)\to{F_{\operatorname{\text{Obj}}}}(T))\)
mit den Eigenschaften:
\({F_{\operatorname{\text{Mor}}}}(\operatorname{\text{id}}_o)=\operatorname{\text{id}}_{{F_{\operatorname{\text{Obj}}}}(o)}\)
\({F_{\operatorname{\text{Mor}}}}(g\circ_C h)={F_{\operatorname{\text{Mor}}}}(g)\circ_D{F_{\operatorname{\text{Mor}}}}(h)\)
Bsp: Funktoren zw. Kategorien von Halbordnungnen?
class Functor f where
fmap :: (a -> b) -> (f a -> f b)
Beispiele: List, Maybe, Action
Plan: für Kategorie \(C\), Endo-Funktor \(F :C\to C\)
definiere sinnvolle Struktur auf Pfeilen \(s \to F t\)
Durchführung: die Kleisli-Kategorie \(K\) von \(F\):
\(\operatorname{\text{Obj}}(K)=\operatorname{\text{Obj}}(C)\), Pfeile: \(s\to F t\)
…\(K\) ist tatsächlich eine Kategorie, wenn:
identische Morphismen (return), Komposition
(>=>)
mit passenden Eigenschaften
\((F,\verb|return|,\verb|(>=>)|)\) heißt dann Monade
Diese Komposition ist
f >=> g = \ x -> (f x >>= g)
Aus o.g. Forderung (\(K\) ist
Kategorie) ergibt sich Spezifikation für return und
>>=
https://wiki.haskell.org/Functor-Applicative-Monad_Proposal
class Functor f where
fmap :: (a -> b) -> (f a -> f b)
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> (f a -> f b)
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
eine Motivation: effizienterer Code für >>=,
wenn das rechte Argument eine konstante Funktion ist
(d.h. die Folge-Aktion hängt nicht vom Resultat der ersten Aktion ab: dann ist Monad nicht nötig, es reicht Applicative)
data Maybe a = Nothing | Just a
instance Monad Maybe where ...
Beispiel-Anwendung:
case ( evaluate e l ) of
Nothing -> Nothing
Just a -> case ( evaluate e r ) of
Nothing -> Nothing
Just b -> Just ( a + b )
mittels der Monad-Instanz von Maybe:
evaluate e l >>= \ a ->
evaluate e r >>= \ b ->
return ( a + b)
Ü: dasselbe mit do-Notation
instance Monad [] where
return = \ x - > [x]
m >>= f = case m of
[] -> []
x : xs -> f x ++ ( xs >>= f )
Beispiel:
do a <- [ 1 .. 4 ]
b <- [ 2 .. 3 ]
return ( a * b )
Anwendung: Ablaufsteuerung für Suchverfahren
verwendet zur Definition semantischer Bereiche,
Monade \(=\) Monoid über Endofunktoren in Hask,
(Axiome für return, >=> bzw.
>>=)
Notation do { x <- foo ; bar ; .. }
(>>= ist das benutzer-definierte
Semikolon)
Grundlagen: Kategorien-Theorie (ca. 1960),
in Funktl. Prog. seit ca. 1990 http://homepages.inf.ed.ac.uk/wadler/topics/monads.html
in anderen Sprachen: F#: Workflows, C#: LINQ-Syntax
GHC ab 7.10: Control.Applicative: pure
und <*>
(\(=\) return und
eingeschränktes >>=)
Motivation: Kombination verschiedener Semantiken:
Zustands-Änderung (Action)
Fehlschlagen der Rechnung (Maybe)
Schritt-Zählen
Logging
in einer Monade
diese nicht selbst schreiben,
sondern Monaden-Transformatoren verwenden, z.B.
type Sem = ExceptT Err (StateT Store Identity)
Standard-Bibliotheken dafür: transformers, mtl
newtype StateT s m a
= StateT { runStateT :: s -> m (a,s) }die elementaren Operationen:
get :: Monad m => StateT s m s
put :: Monad m => s -> StateT s m ()instance Monad m => Monad (StateT s m)Anwendung auf
newtype Identity a = Identity { runIdentity :: a}
ergibt type State s = StateT s Identity
newtype MaybeT m a =
MaybeT { runMaybeT :: m (Maybe a) }Anwendung im Interpreter:
type Sem = StateT Store (MaybeT Identity)
with_int :: Sem Val->(Val -> Sem r)->Sem r
with_int a k = a >>= \ case
ValInt i -> k i ; _ -> lift Nothingbenutzt Einbettung der inneren Monade:
class MonadTrans t where
lift :: Monad m => m a -> t m a
instance MonadTrans (StateT s)wie MaybeT, aber mit Fehlermeldungen (Exceptions)
newtype ExceptT e m a = ExceptT (m (Either e a))
instance Monad m => Monad (ExceptT e m)
instance MonadTrans (ExceptT e)
Operationen:
runExceptT :: ExceptT e m a -> m (Either e a)
throwE :: Monad m => e -> ExceptT e m a
catchE :: Monad m => ExceptT e m a -> (e -> ..) -> ..
Ü: verwende Sem = StateT (ExceptT Err Id.) mit
sinnvollem data Err = ..
Ü: Unterschied zu ExceptT Err (StateT Id.)
gemeinsame Grundlage: Mark P Jones: Functional Programming with Overloading and Higher-Order Polymorphism, AFP 1995, http://web.cecs.pdx.edu/~mpj/pubs/springschool.html (S. 36 ff.)
https://hackage.haskell.org/package/transformers: wie hier beschrieben
https://hackage.haskell.org/package/mtl: zusätzliche Typklassen und Instanzen
class Monad m => MonadState s m | m -> s where
get :: ... ; put :: ...
instance MonadState s m => MonadState s (MaybeT m)
dadurch braucht man fast keine lift zu
schreiben
SS 24: zu KW 22: Aufgaben 1, 3, 4
(Functor, Monad für Action)
implementieren Sie instance Functor Action
direkt.
überprüfen Sie die Funktor-Gesetze an Beispielen.
eine indirekte Implementierung ist
instance Functor m where fmap f a = a >>= \ x -> return (f x)
Beweisen Sie: für jede Monade m erfüllt dieses
fmap die Funktor-Gesetze.
die Konstruktorklasse Applicative hat unter anderem diese Methoden
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
im folgenden geht es nur um statische Semantik (Typkorrektheit), (noch) nicht um dynamische Semantik (Axiome)
Implementieren Sie (<*>) durch
liftA2
Implementieren Sie liftA2 durch
(<*>)
geben Sie eine Implementierung für liftA2 mit
Monaden-Operationen an. (Beachte: andersherum geht das nicht, denn nicht
jedes Applicative ist auch Monade)
was ist the monad of no return?
Funktor- und Monadengesetze (mit leancheck in ghci)
cabal install --lib leancheck
import Test.LeanCheck
import Test.LeanCheck.Function
-- Kleisli-Komposition:
import Control.Monad ((>=>))
:set -XPatternSignatures
-- return ist rechts neutral
check $ \ (f:: Bool -> Maybe Bool) x -> (f x == (return >=> f) x)
-- ein Test, der zeigt, daß eine falsche Aussage erkannt wird:
check $ \ (f:: Bool -> Maybe Bool) x -> (f x == (f >=> f) x)
falsche Functor-/Monad-Instanzen für Maybe, List, Tree (d.h. typkorrekt, aber semantisch falsch)
Standard-Datentyp mit newtype verpacken
newtype List a = List [a]
instance Functor List where
-- korrekte Instanz für List
fmap f (List xs) =
-- benutzt korrekte Instanz für []
List (fmap f xs)
oder strukturgleichen Typ selbst definieren
import Prelude hiding (Maybe)
data Maybe a = Nothing | Just a deriving (Eq, Show)
instance Monad Maybe where ...
StateT Int zum Zählen der Auswertungsschritte
(Aufrufe von value) einbauen
import qualified Control.Monad.Trans.State as S
import qualified Control.Monad.Trans.Class as T
import Numeric.Natural
type Domain = S.StateT Natural Action Val
value :: Env Name Val -> Exp -> Domain
...
Put e f -> with_addr (value env e) $ \ a ->
with_value (value env f) $ \ v ->
T.lift (put a v) >>= \ _ ->
return ValUnit
test3 = print $ run state0 $ flip S.runStateT 0 $ value env0
$ Let "a" (New $ LitInt 0) ...
WriterT [String] zur Protokollierung (Argumente und
Resultat von value)
Lesen der Umgebung mit ReaderT Env. Änderung der
Umgebung (in Let, App) durch welche Funktion?
data Parser c a =
Parser ( [c] -> [ (a, [c]) ] )
über Eingabestrom von Zeichen (Token) \(c\),
mit Resultattyp \(a\),
nichtdeterministisch (List).
Beispiel-Parser, aufrufen mit:
parse :: Parser c a -> [c] -> [(a,[c])]
parse (Parser f) w = f walternative Definition:
type Parser c a = StateT [c] [] a-- | das nächste Token
next :: Parser c c
next = Parser $ \ toks -> case toks of
[] -> []
( t : ts ) -> [ ( t, ts ) ]
-- | das Ende des Tokenstroms
eof :: Parser c ()
eof = Parser $ \ toks -> case toks of
[] -> [ ( (), [] ) ]
_ -> []
-- | niemals erfolgreich
reject :: Parser c a
reject = Parser $ \ toks -> []
Definition:
instance Monad ( Parser c ) where
return x = Parser $ \ s -> return ( x, s )
p >>= g = Parser $ \ s -> do
( a, t ) <- parse p s
parse (g a) t
beachte: das return/do gehört zur List-Monade
Anwendungsbeispiel:
p :: Parser c (c,c)
p = do x <- next ; y <- next ; return (x,y)
mit Operatoren aus Control.Applicative:
p = (,) <$> next <*> next
satisfy :: ( c -> Bool ) -> Parser c c
satisfy p = do
x <- next
if p x then return x else reject
expect :: Eq c => c -> Parser c c
expect c = satisfy ( == c )
ziffer :: Parser Char Integer
ziffer = ( \ c -> fromIntegral
$ fromEnum c - fromEnum '0' )
<$> satisfy Data.Char.isDigit
Folge (and then) (ist >>= aus der
Monade)
Auswahl (oder) (ist Methode aus
class Alternative)
( <|> ) :: Parser c a -> Parser c a -> Parser c a
Parser f <|> Parser g = Parser $ \ s -> f s ++ g s
Wiederholung (beliebig viele, wenigstens einer)
many, some :: Parser c a -> Parser c [a]
many p = some p <|> return []
some p = (:) <$> p <*> many pzahl :: Parser Char Integer =
foldl (\ a z -> 10*a+z) 0 <$> some ziffer
(aus Control.Applicative)
der zweite Parser hängt nicht vom ersten ab:
(<*>) :: Parser c (a -> b)
-> Parser c a -> Parser c beines der Resultate wird exportiert, anderes ignoriert
(<*) :: Parser a -> Parser b -> Parser a
(*>) :: Parser a -> Parser b -> Parser b
Eselsbrücke: Ziel des Pfeiles wird benutzt
der erste Parser ändert den Zustand nicht (fmap)
(<$>) :: (a -> b) -> Parser a -> Parser bCFG-Grammatik mit Regeln \(S \to a S b S, S \to \epsilon\) entspricht
s :: Parser Char ()
s = (expect 'a' *> s *> expect 'b' *> s )
<|> return ()
Anwendung: parse (s <* eof) "abab"
CFG: Variable \(=\) Parser, nur
<*> und <|> benutzen
höherer Ausdrucksstärke (Chomsky-Stufe 1, 0) durch
Argumente (\(\approx\) unendlich viele Variablen)
Monad (bind) statt Applicative (abhängige Fortsetzung)
chainl1 :: Parser c a -> Parser c (a -> a -> a)
-> Parser c a
chainl1 p op = (foldl ...)
<$> p <*> many ((,) <$> op <*> p)expression :: [[Parser c (a -> a -> a)]]
-> Parser c a -> Parser c a
expression opss atom =
foldl ( \ p ops -> chainl1 ... ) atom opssexp = expression
[ [ string "*" *> return Times ]
, [ string "+" *> return Plus ] ]
( Exp.Const <$> zahl )Designfragen:
Nichtdeterminismus einschränken
Backtracking einschränken
Fehlermeldungen (Quelltextposition)
klassisches Beispiel: Parsec (Autor: Daan Leijen) http://hackage.haskell.org/package/parsec
Ideen verwendet in vielen anderen Bibliotheken, z.B. http://hackage.haskell.org/package/attoparsec (benutzt z.B. in http://hackage.haskell.org/package/aeson)
gemeinsam:
(<|>) :: Parser c a -> Parser c a
-> Parser c a
Parser p <|> Parser q = Parser $ \ s -> ...
symmetrisch: p s ++ q s
asymmetrisch: if null p s then q s else p s
Anwendung: many liefert nur maximal mögliche
Wiederholung (nicht auch alle kürzeren)
Nichtdeterminismus \(=\) Berechnungsbaum \(=\) Backtracking
asymmetrisches p <|> q : probiere erst
p, dann q
häufiger Fall: p lehnt sofort ab
Festlegung (in Parsec): wenn p wenigstens ein Zeichen
verbraucht, dann wird q nicht benutzt (d. h. p
muß erfolgreich sein)
Backtracking dann nur durch try p <|> q
Fehler \(=\) Position im Eingabestrom, bei der es nicht weitergeht
und auch durch Backtracking keine Fortsetzung gefunden wird
Fehlermeldung enthält:
Position
Inhalt (Zeichen) der Position
Menge der Zeichen mit Fortsetzung
Roman Cheplyaka, 2015 https://hackage.haskell.org/package/regex-applicative
data RE s a = ...
instance Applicative (RE s) where ...
-- | Attempt to match a string of symbols
-- against the regular expression:
match :: RE s a -> [s] -> Maybe a
match re = let obj = compile re in \str ->
listToMaybe $ results $ foldl (flip step) obj str
Erläuterung (geraten): regulärer Ausdruck wird in endlichen
nicht-det. Automaten übersetzt (compile re),
unabhängig von der Eingabe (str) — deswegen
keine Monad-Instanz,
Rechnung des dazu äquivalenten det. Automaten wird simuliert (für diese eine Eingabe, ohne den gesamten Potenzmengenautomaten auszurechnen), vgl. https://www.imn.htwk-leipzig.de/~waldmann/edu/ss23/afs/folien/#(61)
Paolo Capriotti, Huw Campbell, 2012– https://hackage.haskell.org/package/optparse-applicative
Anwendungs-Beispiel: https://git.imn.htwk-leipzig.de/waldmann/pure-matchbox/-/blob/master/src/Matchbox/Config.hs#L118
Bezeichner parsen (alphabetisches Zeichen, dann Folge von alphanumerischen Zeichen),
Hinweis:
fmap fromString $ (:) <$> _ <*> many _
Whitespace ignorieren (verwende
Data.Char.isSpace)
Hinweis: jeder Token-Parser t (Bsp: Bezeichner,
Zahl-Literal) wird abgeschlossen durch t <* space (warum
nicht space *> t?)
konkrete Haskell-ähnliche Syntax realisieren
if .. then .. else ..
let { .. = .. } in ..
Abstraktion (\ x -> ..) einstellig
Applikation (f a b c) mehrstellig
Hinweis:
exp = foldl App <$> atom <*> many atom
atom = Const <$> number <|> _ <|> parens expAbstraktion (\ x y z -> ..) mehrstellig (im
Parser übersetzen in geschachtelte einstellige)
Implementierung chainl1, expression
ergänzen, Operator-Parser vervollständigen für Ausdrücke mit
Multiplikation, Addition, Vergleichen
Hinweis: Operatoren sollen schwächer binden als Applikation (Bsp:
f a + b bedeutet (f a) + b), realisieren
durch
exp = expression _ app
app = foldl App <$> atom ... -- wie oben
atom = ... <|> parens exp -- wie obenden Typ des Parsers ändern
type Parser c = StateT [c] [] -- vorher
type Parser c = StateT [c] Maybe -- nachher
und Auswirkung diskutieren.
Fehlermeldungen mit
type Parser c = StateT [c] (Except String)
char c = satisfy (== c)
<|> throwError ("expected: " <> [c])den Eigenbau-Parser Parser Char durch
Text.Parsec.String.Parser ersetzen https://hackage.haskell.org/package/parsec/
Operator-Ausdrücke dann mit Text.Parsec.Expr
Node "foo" (Node "baz" Leaf Leaf) (Node "foobaz" Leaf Leaf)
much easier to read if it is presented as
Node "foo" (Node "baz" Leaf Leaf)
(Node "foobaz" Leaf Leaf)
A pretty-printer’s job is to lay out structured data appropriately (John Hughes 1995)
inappropriate wäre deriving Show,
denn Ausgabe ist dort immer einzeilig
Anwendung: Code-Ausgabe in Kompilation,
(Mensch soll es lesen, hat es aber nicht geschrieben)
data Doc abstrakter Dokumententyp für
Textblöcke
Konstruktor: text :: String -> Doc
Kombinatoren:
vcat :: [ Doc ] -> Doc -- vertikal
hcat, hsep :: [ Doc ] -> Doc -- horizontalAusgabe: render :: Doc -> String (oder
Text)
der wesentliche Punkt ist
cat :: [ Doc ] -> Doc
vertikal oder horizontal je nach verfügbarem Platz
nachvollziehbare Spezifikation, effiziente Implement.
John Hughes: The Design of a Pretty-printing Library in Advanced Functional Programming, 1995 Johan Jeuring and Erik Meijer (eds), LNCS 925
verwendet in GHC (Glasgow Haskell Compiler) von Anfang an
Übung: wirklich? https://downloads.haskell.org/~ghc/0.29/
und GHC heute?
alleinstehende Implementierung
Phil Wadler: A Prettier Printer, 1997
Implementierung (Daan Leijen 2000)
modern (David Luposchainksi)
dort auch Vergleich verschiedener Prettyprinter
vgl. https://xkcd.com/927/ Standards
class Pretty a where pretty :: a -> Doc(z.B. in autotool) automatische Code-Erzeugung für
instance Pretty T für algebraische Datentypen
Klammern weglassen (in Operator-Ausdrücken) durch Berücksichtigung von Präzedenzen
class Pretty a where pretty_prec :: Int -> a -> DocMotivation: parse und (pretty-)print aus einem gemeinsamen Quelltext
Tillmann Rendel and Klaus Ostermann: Invertible Syntax Descriptions, Haskell Symposium 2010
http://lambda-the-ultimate.org/node/4191
Datentyp
data PP a = PP
{ parse :: String -> [(a,String)]
, print :: a -> Maybe String
}
Spezifikation, elementare Objekte, Kombinatoren?
(alles nach: Turbak/Gifford Ch. 17.9)
CPS-Transformation (continuation passing style):
original: Funktion gibt Wert v zurück
f = \ x y -> let { ... } in vcps: Funktion erhält zusätzliches Argument, das ist eine Fortsetzung (continuation), die den Wert verarbeitet:
f_cps = \ x y k -> let { ... } in k vaus g (f 3 2)
wird \ k -> f_cps 3 2 $ \ v -> g_cps v k
Funktionsaufrufe in CPS-Programm kehren nie zurück, können also als Sprünge implementiert werden!
CPS als einheitlicher Mechanismus für
Linearisierung (sequentielle Anordnung von primitiven Operationen)
Ablaufsteuerung (Schleifen, nicht-lokale Sprünge)
Unterprogramme (Übergabe von Argumenten und Resultat)
Unterprogramme mit mehreren Resultaten
(a + b) * (c + d) wird übersetzt (linearisiert) in
( \ top ->
plus a b $ \ x ->
plus c d $ \ y ->
mal x y top
) ( \ z -> z )
plus x y k = k (x + y)
mal x y k = k (x * y)
später tatsächlich als Programmtransformation (Kompilation)
wie modelliert man Funktion mit mehreren Rückgabewerten?
benutze Datentyp Tupel (Paar):
f : A -> (B, C)benutze Continuation:
f/cps : A -> (B -> C -> D) -> Derweiterter Euklidischer Algorithmus:
prop_egcd x y =
let (p,q) = egcd x y
in (p*x + q*y) == gcd x y
egcd :: Integer -> Integer
-> ( Integer, Integer )
egcd x y = if y == 0 then ???
else let (d,m) = divMod x y
(p,q) = egcd y m
in ???
vervollständige, übersetze in CPS
Wdhlg: CPS-Transformation von 1+(2*(3-(4+5))) ist
\ top -> plus 4 5 $ \ a ->
minus 3 a $ \ b ->
mal 2 b $ \ c ->
plus 1 c top
Neu: label und jump
1 + label foo (2 * (3 - jump foo (4 + 5)))
Semantik: durch label wird die aktuelle Continuation
benannt: foo = \ c -> plus 1 c top
und durch jump benutzt:
\ top -> plus 4 5 $ \ a -> foo a
Vergleiche: label: Exception-Handler deklarieren,
jump: Exception auslösen
Semantik von Ausdruck x in Umgebung \(E\)
ist Funktion von Continuation nach Wert (Action)
value(E, label L B) = \ k ->
value (E[L:=k], B) k
value (E, jump L B) = \ k ->
value (E, L) $ \ k' ->
value (E, B) k'
Beispiel 1:
value (E, label x x)
= \ k -> value (E[x:=k], x) k
= \ k -> k k
Beispiel 2
value (E, jump (label x x) (label y y))
= \ k ->
value (E, label x x) $ \ k' ->
value (E, label y y) k'
= \ k ->
( \ k0 -> k0 k0 ) $ \ k' ->
value (E, label y y) k'
= \ k ->
( \ k0 -> k0 k0 ) $ \ k' ->
( \ k1 -> k1 k1 ) k'
semantischer Bereich:
type Continuation a = a -> Action Val
newtype CPS a
= CPS ( Continuation a -> Action Val )
value :: Env -> Exp -> CPS Val
Plan:
abstrakte Syntax: Label, Jump, konkrete S.
(Parser)
Semantik:
Verkettung durch >>= aus
instance Monad CPS
Einbetten von Action Val durch
lift
value für bestehende Sprache
value für label und
jump
ein CPS-transformiertes Programm ausführen
feed :: CPS a -> ( a -> Action Val )
-> Action Val
feed ( CPS s ) c = s cSpezifikation der Monaden-Operationen
feed ( return x ) c = c x
feed ( s >>= f ) c =
feed s ( \ x -> feed ( f x ) c )Einbettung von Aktionen (Bsp:
put,get,new)
lift :: Action a -> CPS a(Ross Paterson, 2001)
newtype ContT r m a =
ContT { runContT :: (a -> m r) -> m r }Beziehung zu voriger Folie:
CPS = ContT Val Action, feed = runContT,
lift = Control.Monad.Trans.Class.lift
Ü: vergleiche unser label/jump mit callCC
Ü: delimited Continuations
(reset,shift)
Kenichi Asai and Oleg Kiselyov, CW 2011
Rekursion (bzw. Schleifen) mittels Label/Jump
(und ohne Rec oder Fixpunkt-Kombinator)
folgende Beispiele sind aus Turbak/Gifford, DCPL, 9.4.2
Beschreibe die Auswertung (Datei ex4.hs)
let { d = \ f -> \ x -> f (f x) }
in let { f = label l ( \ x -> jump l x ) }
in f d ( \ x -> x + 1 ) 0jump (label x x) (label y y)
Ersetze undefined, so daß f x = x!
(Datei ex5.hs)
let { triple x y z = \ s -> s x y z
; fst t = t ( \ x y z -> x )
; snd t = t ( \ x y z -> y )
; thd t = t ( \ x y z -> z )
; f x = let { p = label start undefined
; loop = fst p ; n = snd p ; a = thd p
} in if 0 == n then a
else loop (triple loop (n - 1) (n * a))
} in f 5Typ \(=\) statische Semantik
(Information über mögliches Programm-Verhalten, erhalten ohne Programm-Ausführung)
formale Beschreibung:
\(P\): Menge der Ausdrücke (Programme)
\(T\): Menge der Typen
Aussagen \(p :: t\) (für \(p\in P\), \(t\in T\))
prüfen oder
herleiten (inferieren)
Grundbereich: Aussagen der Form \(E \vdash X : T\)
(in Umgebung \(E\) hat Ausdruck \(X\) den Typ \(T\))
Menge der Typen:
primitiv: Int, Bool
zusammengesetzt:
Funktion \(T_1 \to T_2\)
Verweistyp \(\operatorname{\sffamily{Addr}}T\)
Tupel \((T_1, \ldots, T_n)\), einschl. \(n=0\)
Umgebung bildet Namen auf Typen ab
Axiome f. Literale: \(E \vdash \text{Zahl-Literal} : \text{Int}\), …
Regel für prim. Operationen: \(\displaystyle \frac{E\vdash X : \text{Int},E\vdash Y : \text{Int}} {E \vdash (X+Y) : \text{Int}}\), …
Abstraktion/Applikation: …
Binden/Benutzen von Bindungen: …
hierbei (vorläufige) Design-Entscheidungen:
Typ eines Ausdrucks wird inferiert
Typ eines Bezeichners wird …
in Abstraktion: deklariert
in Let: inferiert
(alles ganz analog zu Auswertung von Ausdrücken)
Regeln für Umgebungen
\(\displaystyle E[v:=t] \vdash v : t\)
\(\displaystyle \frac{E \vdash v' : t'}{E[v:=t] \vdash v' : t'}\) für \(v \neq v'\)
Regeln für Bindung: \[\frac{E\vdash X : s, \quad E [v:=s] \vdash Y : t} {E \vdash \mathtt{let~} v=X \mathtt{~in~} Y : t}\]
Applikation: \(\displaystyle \frac{E\vdash F : T_1 \to T_2, \quad E \vdash A : T_1} {E \vdash (F A) : T_2}\)
vergleiche mit modus ponens
Abstraktion (mit deklariertem Typ der Variablen)
\[\frac{E[v:=T_1] \vdash X : T_2} {E\vdash (\lambda (v :: T_1) X) : T_1 \to T_2}\]
dazu Erweiterung der abstrakten Syntax
basiert auf: Alonzo Church: Simple Theory of Types, J. Symb. Logic 1940
Beschreibung der dynamische Semantik als:
big-step: Relation zw. Programm (Term) und Wert (Normalform),
realisiert durch Inferenzbaum (äq.: durch Aufruf von
value)
small-step \(\stackrel{S}{\to}\): Schritt bei
Durchquerung des Inferenzbaums (äq.: bei Folge der Aufrufe von
value, repräsentiert Folge von
Beta-Reduktionsschritten)
Eigenschaften (Beziehung small-step zu Typisierung)
Sicherheit: \(e_0:T\) und \(e_0\stackrel{S}{\to} e_1\) \(\Rightarrow\) \(e_1:T\).
Fortschritt: \(e_0:T\) und \(e_0\) kein Wert \(\Rightarrow\) \(\exists e_1: e_0\stackrel{S}{\to} e_1\).
Termination: \(e_0:T\) \(\Rightarrow\) Folge \(e_0 \stackrel{S}{\to} e_1 \stackrel{S}{\to} \dots\) ist endlich
well-typed programs don’t go wrong
(Robin Milner 1978, über polymorphe Typisierung)
nicht jedes Programm hat einen Typ.
\((\lambda x.xx)(\lambda x.xx)\) hat keinen Typ
Ü: versuche die Bestimmung von \(T_1,T_2\) in \((\lambda (x:T_1).xx)(\lambda (x:T_2).xx)\)
…soll auch nicht, denn es terminiert nicht
nicht jedes terminierende Progamm hat einen Typ
\((\lambda x.xx)(\lambda y.y)0\)
es gibt ausdrucksstärkere Typsysteme,
aber wenn gilt \(\forall e:\) \(e\) hat Typ \(\iff\) \(e\) terminiert,
dann ist \(e\) hat Typ nicht entscheidbar,
weil das Halteproblem nicht entscheidbar ist
Typisierung für
Vergleichs-Operatoren für Zahlen
If-Then-Else,
New, Get, Put — Vorsicht: dann gibt es getypte, aber trotzdem nicht terminierende Programme.
für Tupel mit abstrakter Syntax
data Exp = ... | Tuple [Exp] | Nth Int Exp
dynamische Semantik,
statische Semantik (einschl. Prüfung des Index, deswegen nicht
Nth Exp Exp)
konkrete Syntax (Parser)
konkrete Syntax für AbsTyped
(dazu Parser für Typ-Ausdrücke, dabei soll (->)
rechts-assoziativ sein)
Bisher: Typ-Deklarationspflicht für Variablen in Lambda.
scheint sachlich nicht nötig. In vielen Beispielen kann man die Typen einfach rekonstruieren:
let { t = \ f x -> f (f x)
; s = \ x -> x + 1
} in t s 0Diesen Vorgang automatisieren!
Inferenz für Aussagen der Form \(E \vdash X : (T, C)\)
\(E\): Umgebung (Name \(\to\) Typ)
\(X\): Ausdruck (Exp)
\(T\): Typ
\(C\): Menge von Typ-Constraints
wobei
Menge der Typen \(T\) erweitert um Unbekannte \(U\)
Constraint: Paar von Typen \((T_1, T_2)\)
Lösung eines Constraints-System \(C\):
Substitution \(\sigma:U\to T\) mit
\(\forall (T_1,T_2)\in C: T_1 \sigma = T_2
\sigma\)
Bsp: \(U=\{u,v,w\}, C=\{(u,\textsf{Int}\to v),(w\to \textsf{Bool}, u)\0\}\),
\(\sigma=\{(u,\textsf{Int}\to\textsf{Bool}),(v,\textsf{Bool}),(w,\textsf{Int})\}\)
Plan:
Aussage \(E \vdash X : (T, C)\) ableiten,
dann \(C\) lösen (allgemeinsten Unifikator \(\sigma\) bestimmen)
dann ist \(T \sigma\) der Typ von \(X\) (in Umgebung \(E\))
Für (fast) jeden Teilausdruck eine eigene (frische) Typvariable ansetzen, Beziehungen zwischen Typen durch Constraints ausdrücken.
Inferenzregeln? Lösbarkeit? Implementierung? — Testfall:
\ f g x y ->
if (f x y) then (x+1) else (g (f x True))
primitive Operationen (Beispiel) \[\frac {E \vdash X_1:(T_1,C_1), \quad E \vdash X_2:(T_2,C_2)} {E \vdash X_1 + X_2 : (\textsf{Int}, \{ T_1=\textsf{Int},T_2=\textsf{Int}\}\cup C_1 \cup C_2) }\]
Applikation \[\frac {E \vdash F : (T_1, C_1), \quad E \vdash A : (T_2,C_2)} {E \vdash (F A): \ldots}\]
Abstraktion \[\frac{\ldots}{E\vdash \lambda x.B : \ldots}\]
(Ü) Konstanten, Variablen, if/then/else
durch Stapelung von Monaden-Transformatoren
type Domain a =
WriterT [Con] (StateT Int (Except T.Text)) a
StateT Int zum Erzeugen frischer
Unbekannter
WriterT [Constraint] zum Aufsammeln der
Constraints
Programm zur Typisierung bleibt (im wesentlichen), aber
if t1 == t2 then return foo else reject .. wird ersetzt
durch do tell (Con t1 t2); return foo
damit Except beim Erzeugen der Constraints unnötig
(aber beim Lösen der Constraints)
Signatur \(\Sigma=\Sigma_0\cup\ldots\Sigma_k\),
\(\operatorname{Term}(\Sigma,V)\) ist kleinste Menge \(T\) mit \(V\subseteq T\) und \(\forall 0\le i\le k, f\in\Sigma_i, t_1\in T,\ldots,t_i\in T: f(t_1,\ldots,t_i)\in T\).
(hier Anwendung für Terme, die Typen beschreiben)
Substitution: partielle Abbildung \(\sigma:V\to\operatorname{Term}(\Sigma,V)\),
Definitionsbereich: \(\operatorname{dom}\sigma\), Bildbereich: \(\operatorname{img}\sigma\).
Substitution \(\sigma\) auf Term \(t\) anwenden: \(t\sigma\)
\(\sigma\) heißt pur, wenn kein \(v\in\operatorname{dom}\sigma\) als Teilterm in \(\operatorname{img}\sigma\) vorkommt.
Beispiele (pur oder nicht?)
\(\{(X,f(Y)),(Y,a)\}, \{(X,f(a)),(Y,a)\}, \{(X,Y),(Y,f(X))\}\)
Produkt von Substitutionen: \(t (\sigma_1 \circ \sigma_2) = (t \sigma_1) \sigma_2\)
Beispiel 1:
\(\sigma_1=\{X\mapsto Y\}, \sigma_2 = \{Y\mapsto a\}, \sigma_1 \circ \sigma_2 = \{ X\mapsto a, Y\mapsto a\}\).
Beispiel 2 (nachrechnen!):
\(\sigma_1 = \{X\mapsto Y\}, \sigma_2=\{Y \mapsto X\}, \sigma_1\circ\sigma_2 = \sigma_2\)
Eigenschaften:
\(\sigma\) pur \(\Rightarrow\) \(\sigma\) idempotent: \(\sigma \circ \sigma= \sigma\)
\(\sigma_1\) pur \(\wedge\) \(\sigma_2\) pur impliziert nicht \(\sigma_1\circ \sigma_2\) pur
Implementierung:
import Data.Map
type Substitution = Map Identifier Term
times :: Substitution -> Substitution -> Substition
Substitution \(\sigma_1\) ist allgemeiner als Substitution \(\sigma_2\):
\(\sigma_1 \raisebox{-8pt}[0pt][0pt]{$\stackrel{\displaystyle <}{\sim}$}\sigma_2 \iff \exists \tau : \sigma_1 \circ \tau = \sigma_2\)
Beispiele:
\(\{X\mapsto Y\} \raisebox{-8pt}[0pt][0pt]{$\stackrel{\displaystyle <}{\sim}$}\{X\mapsto a,Y\mapsto a\}\),
\(\{X\mapsto Y\}\raisebox{-8pt}[0pt][0pt]{$\stackrel{\displaystyle <}{\sim}$}\{Y\mapsto X\}\),
\(\{Y\mapsto X\}\raisebox{-8pt}[0pt][0pt]{$\stackrel{\displaystyle <}{\sim}$}\{X\mapsto Y\}.\)
Eigenschaften
Relation \(\raisebox{-8pt}[0pt][0pt]{$\stackrel{\displaystyle <}{\sim}$}\) ist Prä-Ordnung (…, …, aber nicht …)
Die durch \(\raisebox{-8pt}[0pt][0pt]{$\stackrel{\displaystyle <}{\sim}$}\) erzeugte Äquivalenzrelation ist die …
Unifikationsproblem
Eingabe: Terme \(t_1,t_2\in\operatorname{Term}(\Sigma,V)\)
Ausgabe: ein allgemeinster Unifikator (mgu): Substitution \(\sigma\) mit \(t_1\sigma = t_2\sigma\).
(allgemeinst: infimum bzgl. \(\raisebox{-8pt}[0pt][0pt]{$\stackrel{\displaystyle <}{\sim}$}\))
Satz: jedes Unifikationsproblem ist
entweder gar nicht
oder bis auf Umbenennung eindeutig
lösbar.
Typ
mgu :: Term -> Term -> Maybe Substitution
-- oder ... -> Except Text Substitution
Implementierung: \(\operatorname{mgu}(s, t)\) nach Fallunterscheidung
\(s\) ist Variable: …
\(t\) ist Variable: symmetrisch
\(s=(s_1 \to s_2)\) und \(t=(t_1 \to t_2)\): …
für andere zusammengesetzte Typen (Tupel, Verweis) entsprechend
Bemerkungen:
gegebene Implementierung ist korrekt, übersichtlich, aber nicht effizient,
(Ü) es gibt Unif.-Probl. mit exponentiell großer Lösung,
eine komprimierte Darstellung davon kann man aber in Polynomialzeit ausrechnen.
Bsp: Signatur \(\{f/2, a/0\}\),
unifiziere \(f(X_1,f(X_2,f(X_3,f(X_4,a))))\) mit \(f(f(X_2,X_2),f(f(X_3,X_3),f(f(X_4,X_4),f(a,a))))\)
Typrekonstrution: Quelltexte zur Berechnung von Typ und Constraints ergänzen. (Quelltexte zur Lösung der Constraints werden noch gegeben.)
Insbesondere: die alte Implementierung
with_int
:: Domain Val -> ( () -> Domain Val) -> Domain Val
with_int a f = a >>= \ v -> case v of
Type.Int -> f ()
_ -> reject "Integer expected"
ist jetzt falsch, man darf hier nicht ablehnen, sondern muß ein Constraint ausgeben.
diskutiere Typisierung (evtl. Typrekonstruktion) für Continuations.
Vergleiche mit Typ von callCC aus
transformers (dieser Type ist aber polymorph, wir sind hier
monomorph)
(noch von früher) Typisierung für Tupel
konkrete Syntax (Parser, Prettyprinter)
ungetypt:
let { t = \ f x -> f (f x)
; s = \ x -> x + 1
} in (t t s) 0
einfach getypt nur so möglich:
let { t2 = \ (f :: (Int -> Int) -> (Int -> Int))
(x :: Int -> Int) -> f (f x)
; t1 = \ (f :: Int -> Int) (x :: Int) -> f (f x)
; s = \ (x :: Int) -> x + 1
} in (t2 t1 s) 0
wie besser?
Typ-Abstraktion, Typ-Applikation:
let { t = \ (t :: *)
-> \ ( f :: t -> t ) ->
\ ( x :: t ) ->
f ( f x )
; s = \ ( x :: int ) -> x + 1 }
in (((t @(int -> int)) (t @int)) s) 0
konkrete Syntax wie in Haskell
(-XTypeApplications)
zur Laufzeit werden die Typ-Abstraktionen und Typ-Applikationen ignoriert
…besser: nach statischer Analyse entfernt
neuer Konstruktor Pi Name Type in
data Type:
\(t\in\text{Var} \wedge T\in\text{Typ} \Rightarrow \Pi t.T\in\text{Typ}\),
dabei kann \(t\) in \(T\) vorkommen (Konstruktor
Ref Name)
neue Konstruktoren in data Exp, mit
Inferenz-Regeln:
Typ-Abstraktion (TypeAbs Name Exp)
erzeugt parametrischen Typ \(\displaystyle \frac{E \vdash X:T_1 } {E \vdash \lambda (t::*)\to X : \Pi t.T_1}\)
Typ-Applikation (TypeApp Exp Type)
instantiiert param. Typ \(\displaystyle \frac{E \vdash F : \Pi t. T_1}{E \vdash F ~ @ T_2 : T_1[t:=T_2]}\),
dabei \([t:=\dots]\) Ersetzen aller freien Vorkommen von \(t\)
Typen von Ausdrücken sind (sollen sein) geschlossen:
keine freien Typvariablen, vgl. \(\lambda(a::*)(x::b).x\)
…ist im Allgemeinen nicht möglich:
Joe Wells: Typability and Type Checking in System F Are Equivalent and Undecidable, Annals of Pure and Applied Logic 98 (1998) 111–156,
übliche Einschränkung (ML, Haskell): Typ-Abstraktionen nur für let-gebundene Bezeichner:
Luis Damas, Roger Milner: Principal Type Schemes for Functional Programs 1982,
let { t = \ f x -> f(f x) ; s = \ x -> x+1 }
in t t s 0
folgendes ist dann nicht typisierbar (t ist
monomorph):
( \ t s -> t t s 0 )
( \ f x -> f (f x) ) (\ x -> x+1)
wir haben Daten-Variablen (DV) und Typ-Variablen (TV)
werden unterschiedlich behandelt in Typisierung:
Typ einer DV: entsteht durch Deklaration in Abstraktion oder durch Inferenz in Let, wird realisiert durch Umgebung,
Belegung einer TV: entsteht durch Typ-Applikation, wird realisiert durch Substitution.
das ist: 1. nicht uniform, 2. nicht allgemein genug: wir können damit nur über Typen abstrahieren, nicht über Typkonstruktoren.
höhere Kinds, Bsp:
import Data.Kind (Type)
data List (a :: Type) = Nil | Cons a (List a)
type List :: * -> *
Kind-Inferenz,
data T f a = Node a (f (T f a))
type T :: (* -> *) -> * -> *
Kind-Polymorphie
data T f g = C (f (g Bool))
type T :: forall {k}. (k -> *) -> (* -> k) -> *
in Haskell strikte Trennung von Daten, Typen, Kinds
in Agda werden Daten, Typen, Kinds, … uniform behandelt.
Damit sind auch Funktionen von Daten nach Typen möglich: dependent types.
für zukünftiges dependent Haskell ist ein Plan: Datum \(\stackrel{?}{=}\) Type \(=\) Kind \(=\)…
Richard Eisenberg: design for dependent types
(Beispiel aus Vorlesung) Der Lambda-Ausdruck \((\lambda x . x x) (\lambda y . y) 0\) terminiert, aber ist nicht monomorph typisierbar, denn bereits der Teilausdruck \(x x\) ist es nicht.
Eine Typisierung ist möglich, wenn \(x\) polymorph ist und sein Typ-Argument \(a\) unterschiedlich instantiiert wird:
(( \x -> x @(b->b) (x @b)) -- <-- hier wird a belegt
:: forall b . (forall a . a -> a) -> ( b -> b)
)
@Int -- <-- hier wird b belegt
((\y -> y) :: forall c . c -> c)
0
Übertragen Sie auf unsere Sprache, falls möglich.
Die lokale Quantifikation erfordert System F, in ghc
anschalten durch -XRank2Types. Im System Hindley-Milner
sind Quantoren global, aber der Typ
forall b a . (a -> a) -> (b -> b)
trifft hier nicht zu.
Ein Rank-2-Typ aus der Praxis ist
newtype LogicT m a =
LogicT { unLogicT :: forall r. (a -> m r -> m r) -> m r -> m r }
daraus folgt dieser Typ von unLogicT
:: forall {k} (m :: k -> *) a
. LogicT m a
-> forall (r :: k). (a -> m r -> m r) -- <-- lokale Quant.
-> m r -> m r