Funktionale Programmierung mit Haskell Prof Dr Hans J Schneider Lehrstuhl für Programmiersprachen und Programmiermethodik Friedrich-Alexander-Universität Erlangen-Nürnberg Sommersemester 2005 I Die Sprache Haskell II Ergänzungen 7 Kontextfreie Grammatiken 8 Syntaxanalyse 9 Algebraische Spezifikation 10 Semantik von Programmiersprachen 11 Compilieren funktionaler Programme 12 Ausblick: Kategorien c Hans J Schneider 2005
P-Maschine Abstraktes Modell einer Rechenanlage Von N Wirth entwickelt, um die Implementierung von Pascal portabel zu machen Aufbau: Programmspeicher CD mit einem Programmzähler PC Stapelspeicher ST mit dem Zeiger SP Haldenspeicher mit Zeiger NP Befehlsvorrat: bedingte und unbedingte Sprungbefehle Verknüpfungsbefehle, die nur auf dem Stapel arbeiten Transportbefehle von und zum Stapel, wobei sich hinter dem normalen Speicher tiefer liegende Teile des Stapels verbergen (siehe Blockstruktur) compilertechnische Makrobefehle Funktionale Programmierung mit Haskell 111
P-Maschine: Programmabarbeitung Befehlszyklus: PC := 0; do PC := PC + 1; Execute(CD[PC-1]) od Die Reihenfolge innerhalb der Schleife ist Konsequenz der Sprungbefehle und der Unterprogrammaufrufe Auch die teilweise äußerst komplexen compilertechnischen Makrobefehle der P-Maschine werden hier als ein Befehl betrachtet Beispiele von P-Code-Befehlen: P -Code Bedeutung ADD ST [SP 1] := ST [SP 1] + ST [SP ] SP := SP 1 LDC q SP := SP + 1 ST [SP ] := q IND ST [SP ] := ST [ST [SP ]] Funktionale Programmierung mit Haskell 112
Haldenobjekte Speicher der P-Maschine: Stapel Halde SP EP NP Generierung von Haldenobjekten: code(new(x), ρ) = LDC LDC NEW ρ(x) size(type(x)) Konkretisierung des NEW-Befehles: P -Code Bedeutung NEW if NP ST [SP ] EP then error else NP := NP ST [SP ] ST [ST [SP 1]] := NP SP := SP 2 EP = extreme stack pointer Funktionale Programmierung mit Haskell 113
Speicheraufteilung bei Prozeduren Jede Inkarnation einer Prozedur schafft sich eine eigene Umgebung, dh einen eigenen Abschnitt auf dem Stapel: Stapel Halde neue Prozinkarn SP = MP new SP new NP Organisatorische Angaben (Activation record): Rücksprungadresse, ggf Funktionswert (oder dessen Adresse auf der Halde) bisheriger Stapelzeiger bisheriger Wert von EP (muss nach Rückkehr wieder bekannt sein) Beginn des Stapelbereichs der aufrufenden Prozedur (dynamischer Vorgänger) Beginn des Stapelbereichs der syntaktisch umgebenden Prozedur (statischer Vorgänger) Funktionale Programmierung mit Haskell 114
Inkarnationsbeschreibung einer Prozedur Eine mögliche Reihenfolge der Daten: SP + 1 : MP function value SP + 2 : static link (SL) SP + 3 : dynamic link (DL) SP + 4 : extreme pointer (EP ) SP + 5 : return address (RA) SP + 6 : parameters local variables, array descriptors arrays (parameters, local) SP new local stack EP Funktionale Programmierung mit Haskell 115
Aufbau der Inkarnationsbeschreibung Vom aufrufenden Programm zu bearbeiten: Bestimme SL für das gerufene Programm M P des rufenden Programms wird DL des gerufenen Rette EP des rufenden Programms Berechne und speichere aktuelle Parameter Setze M P für das gerufene Programm (ursprüngliches SP + 1) Speichere Rücksprungadresse Springe Prozedur an Vom gerufenen Programm zu bearbeiten: Erhöhe SP um den statischen Speicherbedarf Bearbeite den dynamischen Speicherbedarf Setze neues EP Führe den Prozedurrumpf aus Funktionale Programmierung mit Haskell 116
Übersetzungskontexte bei funktionalen Sprachen Beispiel: letrec add == λx y x + y; dbl == λx x 2; inc == add 1; from == λn cons n (from (inc n)); h == λx if x = 0 then inc else dbl; g == λy h 1 y B: Das Ergebnis ist eine Basiskonstante auf dem Stapel V: Beliebiges Ergebnisobjekt auf der Halde Ein Zeiger auf das Objekt befindet sich auf dem Stapel Beispiele: Vektor von Parametern C: Das Ergebnis ist eine suspendierte Berechnung auf der Halde (closure) Ein Zeiger darauf befindet sich auf dem Stapel Beispiele: die aktuellen Parameter (lazy evaluation) und alle rechten Seiten von Definitionen P: Spezialfall von V für ein gesamtes Programm Funktionale Programmierung mit Haskell 117
Currying Currying konstruiert aus bereits definierten Funktionen neue, indem einige Parameterpositionen frei bleiben Beispiel: letrec add == λx y x + y; inc == add 1; in inc 5; In der Definition von inc wird der erste Parameter von add festgelegt, der zweite bleibt offen Bei der Verwendung von inc wird (compilertechnisch) der zweite Parameter von add festgelegt Der Compiler hat dafür Sorge zu tragen, dass der auszuführende Prozedurrumpf genau die Parameter bekommt, die er benötigt Auf der Halde muss ein Objekt geschaffen werden, das den Zusammenhang zwischen der neu definierten Funktion, dem ursprünglichen Prozedurrumpf und den bereits vorhandenen Parametern herstellt Funktionale Programmierung mit Haskell 118
Funktionen als Ergebnis von Funktionen Eine Funktion kann scheinbar zuviele Parameter haben: Beispiel: letrec in h == λx if x = 0 then inc else dbl; g == λy h 1 y h benötigt einen Parameter, hat aber scheinbar zwei h 1 liefert als Ergebnis eine Funktion, die ihrerseits einen Parameter, hier y, benötigt Das Beispiel legt nahe, die Parameter beim Prozeduraufruf in umgekehrter Reihenfolge auf den Stapel zu legen Funktionale Programmierung mit Haskell 119
Stapelaufbau Der Stapel ist die zentrale Umschlagstation Auf dem Stapel befinden sich Basiskonstanten, Halden- und Stapeladressen, Programmspeicheradressen, zb Adressen von Prozedurrümpfen Stapelabschnitt für eine Funktionsanwendung: SP + 1 : continuation address SP + 2 : F P old (frame pointer) SP + 3 : F P GP old (global pointer) parameters local variables SP new frame pointer entspricht dem dynamic link Der global pointer zeigt auf einen auf der Halde liegenden Vektor, der aus Zeigern auf alle global definierten Objekte besteht Stapelabschnitt für eine Closure-Anwendung: analog, jedoch ohne Parameter Funktionale Programmierung mit Haskell 1110
Haldenobjekte CLOSU RE : (cp, gp): Repräsentiert eine ausstehende Berechnung cp = Zeiger auf den Programmcode (Fortsetzungsadresse) gp = Zeiger auf den Vektor der globalen Objekte F U N V AL : (cf, f ap, f gp): Ergebnis der Definition einer Funktion oder funktionales Ergebnis einer Funktionsanwendung cf = Zeiger auf den Programmcode (Rumpf) fap = Zeiger auf den Vektor der aktuellen Parameter (bei Curry-Funktionen nur teilweise gefüllt) fgp = Zeiger auf den Vektor der globalen Objekte BASIC: Basiskonstante Tritt als Ergebnis von Closure-Anwendungen auf Kann nach Typ weiter differenziert werden V ECT OR: Vektor von Zeigern auf andere (meist: Halden-) Objekte (zb aktuelle Parameter, globale Objekte) Funktionale Programmierung mit Haskell 1111
Beispiel (I) g == λx y x + y + a f == g 1 g F UN : cf fap fgp org( ) eval(par1) eval(par2) ADD eval(a) ADD org / a / ** org enthält ua den Test, ob genügend Argumente vorhanden sind, und erzeugt im andern Fall ein neues F U N V AL-Objekt mit teilweise ausgefülltem fap Funktionale Programmierung mit Haskell 1112
Beispiel (II) g == λx y x + y + a f == g 1 f CLOS : cp gp org eval(1) eval(g) apply org g / apply muss P C, F P, GP aus dem F UNV AL-Objekt g übernehmen und Zeiger auf die (bei Curry-Funktionen) bereits vorhandenen Parameter auf den Stapel kopieren Funktionale Programmierung mit Haskell 1113
Kreierung der Haldenobjekte F UNV AL: bei der Verarbeitung einer Funktionsdefinition CLOSURE: Funktionsrumpf: bei Verarbeitung der rechten Seite einer Definitionsgleichung aktuelle Parameter: bei Verarbeitung einer Funktionsanwendung V ECT OR, BASIC: bei Bedarf Die Kreierung erfolgt an Hand der obersten Stapelobjekte Beispiel: P -Code Bedeutung mkclosure ST [SP 1] := new ( CLOS : ST [SP ], ST [SP 1], ) SP := SP 1 Funktionale Programmierung mit Haskell 1114
Generierung des Zielcodes abhängig vom Kontext Vier Kontexttypen entsprechen vier verschiedenen Prozeduren: P code : B code : V code : C code : Ergebnis = auszuführendes Programm P code(e) = V code(e, [ ], 0) Basiswert auf Stapel Basiswert oder Vektor auf Halde und Zeiger auf Stapel Closure auf Halde und Zeiger auf Stapel Gemeinsame Parameter dieser Prozeduren: e β sl expression to be compiled environment stack level sl ist die Differenz zwischen SP und einem sp 0, das den Anfang der formalen Parameter und lokalen Variablen kennzeichnet β liefert (LOC, reladdr) bzw (GLOB, reladdr) Funktionale Programmierung mit Haskell 1115
Adressierung der Parameter Beispiel letrec f == λv 1 v n letrec a 1 == e 1 ; a q == e q in e 0 ; g == f e 1 e m (m < n) in g e m+1 e k (k > n) Denkbare Lösung (Beispiel beim Aufruf von f mit m = n): P C old F P old F P GP old e n Argumente e 1 sp 0 a 1 lokale Größen a 2 SP a q Funktionale Programmierung mit Haskell 1116
B-Code: Basiskonstante auf Stapel Basiskonstante: B code(b, β, sl) = SP := SP + 1 ST [SP ] := b Ausdruck: B code(e 1 op e 2, β, sl) = B code(e 1, β, sl) B code(e 2, β, sl + 1) op Bedingter Ausdruck: B code(if e 1 then e 2 else e 3, β, sl) = B code(e 1, β, sl) F JP L 1 B code(e 2, β, sl) UJP L 2 L 1 : B code(e 3, β, sl) L 2 : Funktionale Programmierung mit Haskell 1117
V-Code: Basiskonstante oder Vektor auf Halde Basiskonstante: V code(b, β, sl) = B code(b, β, sl) ST [SP ] := new(basic : ST [SP ]) Ausdruck: V code(e 1 op e 2, β, sl) = B code(e 1 op e 2, β, sl) ST [SP ] := new(basic : ST [SP ]) Bedingter Ausdruck: V code(if e 1 then e 2 else e 3, β, sl) = B code(e 1, β, sl) F JP L 1 V code(e 2, β, sl) UJP L 2 L 1 : V code(e 3, β, sl) L 2 : Funktionale Programmierung mit Haskell 1118
Auswertung der Parameter im Kontext C code: C code(v, β, sl) = getvar(v, β, sl) da sowohl die Definitionen der lokalen Größen als auch die aktuellen Parameter zunächst Closure-Objekte sind im Kontext V code: V code(v, β, sl) = getvar(v, β, sl) eval im Kontext B code: B code(v, β, sl) = V code(v, β, sl) if HP [ST [SP ]]tag BASIC then error else ST [SP ] := HP [ST [SP ]] Funktionale Programmierung mit Haskell 1119
Getvar und Eval getvar liefert den Zeiger auf das Objekt: getvar(v, β, sl) = let (p, i) = β(v) in SP := SP + 1 if p = LOC then ST [SP ] := ST [sp 0 + i] else ST [SP ] := HP [GP ]v[i] eval muss nur etwas tun, wenn eine Closure vorliegt, was aber (abgesehen von möglichen Optimierungssituationen) der Normalfall ist: if HP [ST [SP ]]tag = CLOS then ST [SP + 1] := P C ST [SP + 2] := F P ST [SP + 3] := GP GP := HP [ST [SP ]]gp P C := HP [ST [SP ]]cp SP := SP + 3 F P := SP Beachte: Das ist ein Befehl! Funktionale Programmierung mit Haskell 1120
Funktionsdefinition (Lambda-Ausdruck) F UN : cf fap fgp address / globals C code(λv 1 v 2 v n e, β, sl) = getvar(v 1, β, sl) get globals getvar(v g, β, sl + g 1) mkvec g mkvec 0 push L 1 mkf unval UJP L 2 L 1 : targ n test arguments V code(e, [v i (LOC, i), v j (GLOB, j)], 0) return n update stack, repeat application L 2 : Funktionale Programmierung mit Haskell 1121
Funktionsanwendung Voraussetzung: e e e V code(e e 1 e n, β, sl) = ST [SP + 1] := L - - Fortsetzungsadresse ST [SP + 2] := F P ST [SP + 3] := GP SP := SP + 3 F P := SP C code(e n, β, sl + 3) C code(e 1, β, sl + n + 2) V code(e, β, sl + n + 3) apply L : apply muss P C, F P, GP aus dem F UNV AL-Objekt e übernehmen und Zeiger auf die (bei Curry-Funktionen) bereits vorhandenen Parameter auf den Stapel kopieren Funktionale Programmierung mit Haskell 1122
Apply apply muss P C, F P, GP aus dem F UNV AL-Objekt e übernehmen und Zeiger auf die (bei Curry-Funktionen) bereits vorhandenen Parameter auf den Stapel kopieren Stapelstruktur: vor apply nach apply P C old P C old F P old F P old F P GP old F P GP old e n e n e 1 e 1 SP e a p SP a 1 Funktionale Programmierung mit Haskell 1123
Anfang und Ende des Funktionsrumpfes Wiederholung: C code(λv 1 v 2 v n e, β, sl) = L 1 : targ n V code(e, [v i (LOC, i), v j (GLOB, j)], 0) return n L 2 : targ n testet die Anzahl der vorhandenen Argumente Genug Argumente: Mit dem erzeugten V code weitermachen Zuwenig Argumente: Neues F UNV AL-Objekt erzeugen return n: Wenn ursprünglich mehr Argumente vorhanden waren, als zur Anwendung der Funktion erforderlich sind, muss das Ergebnis der Funktionsanwendung wieder ein Funktionsobjekt sein, das unmittelbar anschließend ausgeführt wird Funktionale Programmierung mit Haskell 1124
Argumenttest Stapelstruktur bei Konstruktion eines FUNVAL-Objektes: vor targ nach targ Halde P C old SP f f F UN : P C F P old v F P GP old GP a m v V ECT OR : a m SP a 1 a 1 Expansion von targ n: if SP F P < n then h := ST [F P 2] ST [F P 2] := new(f UN : P C 1, new(v ECT OR : ST [F P + 1],, ST [SP ]), GP ); GP := ST [F P ] SP := F P 2 F P := ST [F P 1] P C := h Funktionale Programmierung mit Haskell 1125
Rücksprung Wenn ursprünglich zuviele Argumente vorhanden waren, muss das Ergebnis wieder ein Funktionsobjekt sein: if SP = F P + 1 + n then / Delete stack frame and return / else if HP [ST [SP ]]tag F UNV AL then error / Repeat function application / Stapelstruktur: vor return nach Fall 1 nach Fall 2 P C old SP result P C old F P old F P old F P GP old F P GP old a m a m = a m a 1 a n+1 = a q+1 SP result a q SP a 1 Funktionale Programmierung mit Haskell 1126