2.3 Einblick in die Implementierung funktionaler Sprachen Vorgehen: • Sprachmittel funktionaler Programmiersprachen • Ein Interpreter für eine einfache funktionale Sprache • Übersetzung einer einfachen funktionalen Sprache 2.3.1 Sprachmittel funktionaler Programmiersprachen Funktionale Programmierung im Überblick: • funktionales Programm: - partielle Funktion von Eingabe- auf Ausgabedaten, - besteht aus Deklarationen von Datentypen und Funktionen, insbesondere Funktionen höherer Ordnung (s.u.) - Rekursion ist eines der zentralen Sprachkonzepte - kein Zustandskonzept, keine veränderlichen Variablen, keine Schleifen, keine Zeiger • Ausführung eines funktionalen Programms: Anwendung der Funktion auf Eingabedaten 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 107 Definition: (partielle Funktion) Ein Funktion heißt partiell, wenn sie nur auf einer Untermenge ihres Argumentbereichs definiert ist. Andernfalls heißt sie total. Bemerkungen: • Da Terminierung nicht entscheidbar ist, definiert man in der Informatik häufig nur partielle Funktionen. • Durch Einführen eines Wertes für „undefiniert“ kann man jede partielle Funktion total machen. Üblicherweise bezeichnet man den Wert für „undefiniert“ mit ⊥ (engl. „bottom“). Definition: (strikte Funktion) Ein Funktion f heißt strikt, wenn f(⊥) = ⊥ . Klassifikation funktionaler Programmiersprachen: strikt/eager nicht-strikt/lazy typisiert ML Haskell untypisiert Lisp ( OLisp ) Wir betrachten im Folgenden nur ML. 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 108 Es gibt drei Arten von Werten bzw. Typen, mit denen gerechnet werden kann: • Basisdatentypen (int, bool, string, ...) • rekursive Datentypen • Funktionstypen In funktionalen Programmiersprachen werden Funktionen also auch als Werte betrachtet. Werte werden mit Ausdrücken beschrieben. „Variablen“ werden benutzt, um Werte zu bezeichnen. Die funktionale Programmiersprache ML Vernachlässigt man die Modularisierungskonstrukte, besteht ein ML-Programm aus • der Einführung von Bezeichnern für Werte: - val x = 7; • der Definitionen von Typen: - type t = ... ; - datatype dt = ... ; ML bietet ein interaktives Laufzeitsystem, das Eingaben obiger Form akzeptiert. Selbstverständlich kann man Eingaben auch aus einer Datei einlesen. Darüber hinaus gibt es Übersetzer für ML. 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 109 Beispiel: (Werte von Basisdatentypen) - val x = 7; - val y = 9; - val z = x + y ; Bei Datentypen antwortet das Laufzeitsystem immer mit dem berechneten Wert und dem Typ: val z = 16 : int Funktionen - Anwendung und Beschreibung: Funktionen sind in ML immer einstellig. Die Anwendung (application) einer Funktion f auf . ein Argument e wird notiert als Funktionen werden mittels λ-Abstraktion beschrieben, d.h.: - val f = fn x => A ; Hier wird f als Bezeichner für eine Funktion eingeführt; d.h. fn x => A beschreibt eine Funktion. Welche Funktion beschreibt fn x => A ? Auswertungssemantik: Den Wert von (fn x=>A) e erhält man, indem man • das Argument auswertet; Ergebnis sei der Wert • x durch z in A ersetzt und • den resultierenden Ausdruck auswertet. 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 110 Beispiele: (Listen und Tupel in ML) - val l1 - val l2 - val l3 val l3 = = [ 2,3,4 ] ; = [7,9] ; = l1 @ l2 ; [2,3,4,7,9] : int list - val le = (1,"1.String",false); val le = (1,"1.String",false):int*string*bool - val re = ( 3 ); val re = 3 : int - val re = (3,7); val re = (3,7) : int * int - val tup = (le,re); val tup = ((1,"ersterString",false),(3,7)) : (int * string * bool) * (int * int) Mit der Tupelbildung lassen sich „baumstrukturierte“ Werte, sogenannte Tupelterme, aufbauen. So entspricht der Tupelterm: dem Baum: 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 111 Rekursive Datentypen: Rekursive Datentypdeklarationen deklarieren Mengen von baumstrukturierten, typisierten Werten (ähnlich unserer Notation für abstrakte Syntax). Folgende Datentypdeklaration deklariert den Typ ! " E mit der Konstanten B C D und # $ % den Konstruktorfunktionen und : % E ! " F D E G B C D $ G # $ % $ ! " & ! " H Typen der Konstruktorfunktionen: ! " # $ % ! " & ! " ! " Mit den Konstruktorfunktionen lassen sich typsicher „baumstrukturierte“ Werte, sogenannte Konstruktorterme, aufbauen: ' ( ) * + , - . / 0 1 2 3 1 ) 4 . / 0 1 ( ) * + , - . / 0 1 2 3 1 ) 4 5 6 2 . / 0 1 5 6. / 0 1 2 3 1 ) 4 2 . / 0 1 7 6 3 1 ) 4 < 6 3 1 ) 4 8 9 6 3 1 ) 4 < 9 9 = : 9 9 ; , > ? @ A 1 1 Listen werden in ML auch als Konstruktorterme I aufgefasst und zwar mit der Konstanten und dem Infixkonstruktor J J . 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 112 Beispiel: (nicht-rekursive Funktionen in ML) X Y Z X Y Z Y Z L L [ \ [ Q L b \ [ b \ U d ] c S \ ^ ] _ ` a a M Q O a Listen- und Tupeltypen: ML unterstützt Typkonstruktoren zur Definition von Listen- und Tupeltypen. Sind S und T Typen, dann bezeichnet: • K L M N O • S*T den Typ der Listen mit Elementen vom Typ den Typ der Tupel mit erstem Element vom Typ K und zweitem Element vom Typ P . Die leere Liste wird mit Q M L bezeichnet, das Anhängen vorne an eine Liste mit R X Y Z L M L \ ` S S Q M L Y Z L M L \ e f V U d V : a Eine Liste mit den Elementen [ T U V W W W V T Q ] geschrieben. X S SR g d h X V T U , ... , T Q wird als h i a Ein Tupel mit Elementen T U , e2 wird als ( T U V T f geschrieben. Tupel mit mehr als zwei Elementen sind auch zulässig. X Y Z L 14.12.2005 O k l \ m n V O o k T j j a © A. Poetzsch-Heffter, TU Kaiserslautern 113 K Pattern Matching: Bisher haben wir nur gezeigt, wie Terme aufgebaut werden, aber nicht, wie man auf ihre Teile zugreifen kann. ML arbeitet ohne Selektoren und nutzt zum Selektieren und für viele andere Aufgaben Pattern Matching auf Tupel- und Konstruktortermen. Idee: Terme mit Variablen (Pattern) werden so über variablenfreie Terme gelegt, dass die Wurzel der Terme zusammenfallen. „Passt“ das Muster/Pattern (match), werden die Variablen an die zugehörigen Teilterme gebunden. Beispiel: (Pattern Matching) Seien x, y Variablen; dann passt auf p p q r s t u v w r p p x y u z v { x r x | } ~ q s t u v p x y u z v { x r x | } ~ x r x s { { x w 14.12.2005 p p q r w r p r x x w w x r x s { { x w r x x w w x x © A. Poetzsch-Heffter, TU Kaiserslautern 114 Pattern Matching auf Konstruktortermen ist entsprechend definiert. Selbstverständlich lassen sich Tupel- und Konstruktorterme kombinieren. Definition von Funktionen - Weitere Aspekte: Durch die Verwendung von Tupeln und von Pattern Matching lassen sich in ML auch „mehrstellige“ Funktionen realisieren: ¢ ¡ Funktionen höherer Ordnung sind Funktionen, die Funktionen als Argumente oder Ergebnisse haben: £ £ ¤ ¥ § ¨ ¥ ¥ § ¨ ¥ ¥ £ £ ¤ ¥ ¦ ¦ ¦ ¦ © £ £ ¤ ¥ ª § ¨ ¥ ¥ (Beachte die Variablen im Typ von appltwice.) 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 115 Ein ähnliches Beispiel, bei dem Funktionen als Ergebnisse illustriert werden: « ¬ ­ ® ¬ ­ ® « ¯ ° ± ² ³ ¯ ° ± ² ³ ¬ ­ ® ¬ ­ ® « ´ ¬ ­ ® ³ Á  ³ Á  ´ µ ¶ µ ¶ ¾ ¿ ² ² À ¾ ¿ ² ² À ¬ ­ ® ´ ´ µ ¼ ´ · ¸ ½­ ¯ ° ± ² ³ ´ µ ¶ ´ ¾ ¿ ² ² À à Š¼ ¼ ¸ µ ¶ « · ¾ ¿ ² ² ± ¶ ¯ « · ¹ ´ · µ ¸ µ ½­ º « · ¸ ½­ ¹ º º » « · ½­ º » ± ¶ ¯ Ã Ä » ± ¶ ¯ Statt der in der obersten Zeile verwendeten Syntax unterstützt ML auch eine kürzere und flexiblere Syntax, für die Funktionsdefinition: « µ ¿ ¶ ¬ ­ ® ¯ ° ± ² ³ ¯ ° ± ² ³ ´ µ ¹ µ ¶ ´ ¼ µ ¸ µ ¸ ½­ ¹ º » « · ½­ º « · ¸ ½­ « · ½­ º Bei dieser Form der Funktionsdefinition kann Pattern Matching als Fallunterscheidung verwendet werden. Dieses scheinbar einfache Sprachmittel vereinfacht die Definition von Funktionen zum Teil erheblich. Wir erläutern den Mechanismus hier nur exemplarisch. Selektoren für Tupel: « µ ¿ ¶ ¬ ­ ® « µ ¾ ¯ ¬ ­ ® ¬ ­ ® µ ¾ ¯ ³ Á  ³ Á  14.12.2005 ´ ´ ¸ ¹ ÆÇ º µ ¶ ¼ ´ µ ¾ ¯ ´ ¹ ½­ È » ½É « · ½­ ¸ Ê ³ Á ¾ ¯ ³ ¾ Ë ® ³ Ì Ê Æ Ê À Ê ³ Á ¾ ¯ ³ ¾ Ë ® ³ Ì Ê ¼ Í Ë ® ³ Ì Ê º » ¾ ¯ Á ± ¶  © A. Poetzsch-Heffter, TU Kaiserslautern 116 Selektoren für rekursive Datentypen: Î Ï Ð Ñ Þ ã ß Ô Î Ò Ó Ô Ô Ó Ï Õ Ö × Ø Ù Ó Ò Ó Ô Ô Ó Ï Õ Õ Ò Ó Ô Ô Ó Ï Õ Ï Ð Ñ Þ Ý Ï Ñ ä Ò Ó Ô à Ñ Õ Ö æ Ó ß Ï Ò Ó Ô à Ñ Õ Õ Ö Ô Ú Û Ü Ü á à Ñ Õ Û Ó Ó ç Ü Ý Ô Ý Û ß à Ò Ó á à Ñ Õ Û Ó Ó Î å á à Ñ Õ Û Ó Ó Ý ç Ý Û ß à Ò Ó á à Ñ Õ Û Ó Ó â â Pattern Matching kann auch für Listenargumente verwendet werden. Als Beispiel betrachten wir die Funktionen map, die eine einstellige Funktion auf alle Elemente einer Liste anwendet: Î Ï Ð Ñ Þ ã ß Ô Î Ï ê ë è ß é Ï Ö ç è ß é ã ß Ô ã ß Ô è ß é Ý Ó Û í Ó Û í Ý Ï Ñ Ý ê ñ Ý ê ë ä äç Ò Ü ä Ö ìß è ß é Ý Î å Ò Ð î î ï Ú ò Úó ë ä Ö Ï ç Ü ä ä Ö è ß é ìá Ü ê ï à Ñ Õ Úð Î å ìß Ï ç Ò Ü Ô à Ò Õ Î å â ìá Ô à Ò Õ Úñ ë â Ô à Ò Õ Die Notation mit dem Schlüsselwort fun erlaubt auch die Definition von rekursiven Funktionen: Î Ï Ð Ñ ã ß Ô Î Ï ß î ã ß Ô ã ß Ô Ï ß î Ó Û í Ó Û í 14.12.2005 Ý Ý Ñ Ï Ñ Ý Ý à Ï ä Ï ß î Ñ Ý ô à Ñ Õ ö ï Õ õ Ó Ñ Î å à Ñ Õ ä à Ñ Õ ö Ó Ô Ò Ó Ñ ÷ Ï ß î Ö Ñ Î ö Ü â â ñ ø ù ô ô ö ó ô ô © A. Poetzsch-Heffter, TU Kaiserslautern 117 Alternativ lässt sich die Fallunterscheidung auch mit Pattern Matching realisieren: ú û ü ý û þ ÿ þ û þ ÿ û þ ÿ þ ú þ ý ý û ý û þ ÿ ý û þ ÿ ý ú ú ý ý Rekursive Datentypen, rekursive Funktionen und Pattern Matching erlauben eine sehr kompakte Programmierung auf Termen. Als erstes Beispiel betrachten wir eine Funktion, die in einem Bintree die Summe aller Blattbewertungen berechnet: ú û ü ý þ ú ú þ 14.12.2005 ü ü þ û ü ü ü ü ü ü þ þ ü ü ! û ý ! ý þ û þ û ü ü ! ú ü ü ý þ û " þ û ý © A. Poetzsch-Heffter, TU Kaiserslautern 118 Lokale Zwischenergebnisvereinbarungen: ML unterstützt verschiedene Möglichkeiten zur Vereinbarung von Bezeichnern für Zwischenergebnisse. Wir benötigen nur die Let-Vereinbarung: # $ % & $ % & ' ' ( & ) * ( 3 4 5 $ % & + ( , - ) . / + 0 + - + ) 0 1 2 + 0 * Deklaration von Typnamen: Oft verbessert es die Lesbarkeit von Programmen für „existierende“ Typen (d.h. für Typen die bereits definiert sind bzw. mit Typkonstruktoren aufgebaut sind), neue Namen einzuführen. ML bietet dafür die Typdeklaration: # * 6 7 ) $ % . ( 8 * . + 0 / 2 # * 6 7 ) + 0 * * 9 7 & ) ( + 0 * # * 6 7 ) ; < < & & + 8 * ( ; < < & : + 0 * 2 & + 8 * 2 Deklariert werden nur neue Namen, keine neuen Typen. Dies zeigt folgendes Beispiel: # 1 % * % * 6 7 ) 1 % * % * 6 7 ) 14.12.2005 ; < ' ; < ' ( ( = < ' + 0 / = < ' + 0 / < > < > + 0 * * 9 7 & ) + 0 * : 2 + 0 * © A. Poetzsch-Heffter, TU Kaiserslautern 119 Polymorphie: Parametrischer Polymorphismus ist bei typisierten funktionalen Programmiersprachen eines der interessantesten Sprachmittel. Die Typen charakterisieren Eigenschaften der Werte, so dass gilt: „Wohltypisierte Programme laufen nicht in die Irre.“ Parametrischer Polymorphismus erlaubt es: • Programme möglichst allgemein zu typisieren: ? @ A B C D E F G@ H I GJ K H I G@ L M N O H I GJ L M N O . • Explizite Typparameter einzuführen: H P @ O @ O Q A R T R @ C V W U P R G@ U C G@ U C G@ J M D O S R R J M D O S R R B X G@ J M D O S R R Y Typsystem ist im Detail recht komplex; Beispiel: let val id = fn x => x in ( id 7 , id true ) end ist wohltypisiert, nicht aber der operationell äquivalente Ausdruck ( fn id => ( id 7, id true ) ) ( fn x => x ) Bemerkungen: • Typanalyse basiert auf mächtiger Typinferenz. • Typen sind ein wertvolles Dokumentationsmittel. 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 120 Beispiel: (Ein einfacher Interpreter) Um das Zusammenwirken der Sprachmittel zu studieren, betrachten wir einen Interpreter für eine einfache Ausdruckssprache: datatype exp = Int of int | Add of exp * exp | Var of string | Let of string * exp * exp ; val mp = Let ("x",Add (Int 1, Int 2), Let ("y",Int 5, Let ("z",Int 8, Add (Add (Var "x",Var "y"), Var "z")))); type env = (string * int) list; fun lkup s [] = 0 | lkup s ((s1,i)::es) = if s=s1 then i else lkup s es ; fun eval (Int i) E = i | eval (Add (le,re)) E = (eval le E) + (eval re E) | eval (Var s) E = lkup s E | eval (Let (s,e1,e2)) E = eval e2 ((s,eval e1 E)::E) ; val erg = eval mp []; val erg = 16 : int 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 121 2.3.2 Ein Interpreter für eine einfache funktionale Sprache Ziele: • Nutzung funktionaler Programmierung zur Implementierung von Programmiersprachen • Einführung in die Interpretation funktionaler Programme • Vorbereitung des folgenden Übersetzungsabschnitts (Funktionsabschluss) Rekursive Datentypen und rekursive Funktionen bieten eine gute sprachliche Basis für die Implementierung von Programmiersprachen. Behandlung von Funktionen als Werte Demonstration anhand einer einfachen funktionalen Sprache, genannt TinyML. Dabei gehen wir von kontextkorrekten abstrakten Syntaxbäumen aus. TinyML besitzt alle zentralen Konstrukte funktionaler Sprachen außer rekursiven Funktionen. 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 122 Ein TinyML-Programm ist ein Ausdruck mit folgender abstrakten Syntax (Vorlesungsnotation): Expr = | Int ( Bool ( Con ( Var ( UnOp ( BinOp ( Cond ( Abs ( App ( Let ( ExprList Int | Bool | Con | BinOp | Cond | Abs | int ) bool ) ident, ExprList ) ident ) oprnd, Expr ) oprnd, Expr, Expr ) Expr, Expr, Expr ) ident, Expr ) Expr, Expr ) ident, Expr, Expr ) * Expr Var App | | UnOp Let wobei ident und oprnd durch String repräsentiert sind. Die Produktion mit Konstruktor Con beschreibt die Anwendung eines Konstruktors in TinyML: • ident repräsentiert den Konstruktorbezeichner. • ExprList repräsentiert die Argumente. Kern des Interpreters wird eine Funktion eval sein, die einen Ausdruck auswertet. Fragen: 1. Wie werden Variablen ausgewertet? 2. Wie werden Abstraktionen ausgewertet? 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 123 Ad 1: Wir benutzen eine Werteumgebung, die Bezeichnern Werte zuordnet. Ad 2: Zerfällt in zwei Probleme: • Was soll z.B. Z [ \ ] ^ _ ` a ^ b c b d e f g h i ^ j b k b d b c b d l m liefern, wobei E eine beliebige Umgebung ist? d n m • Wie soll man nicht-lokale Variablen behandeln? Beispiel: (Nicht-lokale Variablen in ML) v w x y z { w w x y o s z { w w x y x x q z { w } p } q s z { w p x | o p q r s t s ~ } q o u r } q o q q y s } q y In ML wird das Vorkommen von o in der Abstraktion p q r s t o u r statisch gebunden (vgl. 2.1.6, geschachtelte Prozeduren). Zur Auswertung des Aufrufes von p benötigen wir die Umgebung von der Definitionsstelle. 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 124 Beachte: Das Szenario kann viel komplexer sein. Die Abstraktion könnte z.B. auch nicht-lokale Funktionen benutzen: Ebenso können die Let-Ausdrücke an beliebiger Stelle im Rumpf einer Abstraktion vorkommen. Lösungsansatz: Das Ergebnis der Auswertung einer Abstraktion A in einer Umgebung E ist das Tupel (A,E). Dieses Tupel wir Abschluss der Funktion genannt (engl. closure). Der Abschluss enthält die statisch gebundenen Größen. An der Aufrufstelle der Abstraktion liefert der Abschluss dann die Umgebung, in der der Aufruf auszuwerten ist. 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 125 Damit ergibt sich folgende informelle Semantik, definiert über die Struktur der abstrakten Syntax: Die Auswertung eval in Umgebung E • eines Basiswerts w liefert w; • einer Variablen v liefert den Wert von v in E; • eines Konstruktorterms c [e1,...,en] liefert c [eval e1 E, ... , eval en E ] ; • einer Operation oprnd liefert das Ergebnis der Anwendung von oprnd auf die Ergebnisse der Teilausdrücke; entsprechend für den bedingten Ausdruck; • einer Abstraktion a liefert den Abschluss (a,E) • einer Applikation App(e,ep): - eval(e, E) ergibt Abschluss (Abs(x,e1),E1) - eval(ep,E) ergibt vp - damit ergibt sich: eval( e1, (x,vp)::E1 ) • eines Let-Ausdrucks: geeignete Erweiterung von E. Realisierung des Interpreters in ML Die Realisierung besteht aus drei Teilen: • Beschreibung der abstrakten Syntax und der Datentypen für die Werte • Beschreibung der Auswertungsfunktion • Beschreibung von Hilfsfunktionen 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 126 Umsetzen der abstrakten Syntax nach ML: type ident type oprnd = string = string datatype expr Int of | Bool of | Con of | Var of | UnOp of | BinOp of | Cond of | Abs of | App of | Let of ; = int bool ident ident oprnd oprnd expr ident expr ident * expr list * * * * * * expr expr * expr expr * expr expr expr expr * expr Beschreibung der Werte, d.h. der Ausdrücke in Normalform: datatype value = Intv of int | Boolv of bool | Conv of ident * value list | Clos of (ident * expr) * env withtype env = (ident*value) list ; Da jedem Wert auch ein Ausdruck entspricht, ist eine Trennung in Werte und Ausdrücke nicht nötig; sie bietet allerdings mehr Typsicherheit. 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 127 Beim Auslesen aus der Umgebung wird jeweils die zuletzt eingetragene Bindung mit dem passenden Bezeichner geliefert: fun lkup x ((y,e)::E) = if x=y then e else lkup x E ; Programmierung der Auswertungsfunktion: Umsetzung der informellen Auswertungssemantik in eine rekursive Funktion: fun eval (Int i) E = Intv i | eval (Bool b) E = Boolv b | eval (Con(c,l)) E = Conv(c,map (fn x=>eval x E) l) | eval (Var x) E = lkup x E | eval (UnOp (f,e)) E = apply1 f (eval e E) | eval (BinOp (f,el,er))E = apply2 f (eval el E,eval er E) | eval (Cond (e,e1,e2)) E = if (fn(Boolv b)=>b) (eval e E) then eval e1 E else eval e2 E | eval (Abs (x,e)) E = Clos ((x,e),E) | eval (App (e,ep)) E = let val Clos ((w,eb),E1) = eval e E in let val v = eval ep E in eval eb ((w,v)::E1) end end | eval (Let (x,e,eb)) E = eval eb ((x,eval e E)::E) ; 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 128 Es verbleibt die Anwendung der vordefinierten Operationen: ¡ ¢ £ ¤ ¥ ¦ ¤ § ¨ ¥ ¥ ¡ © ­ ¡ ¢ £ ¤ ® ¯ ¤ § ° ¥ © ­ ¡ ¢ £ ¤ ¦ ¡ ¤ ­ ¡ ¢ £ ² ¡ ¢ ¼ ¤ ½ ¤ § ¾ ¦ © ª ³ ¾ ¦ © ¢ « ¬ ¾ ¦ © § ª ½ ¢ « ­ ¡ ¢ ¼ ¤ ¿ ¤ § ¾ ¦ © ª ³ ¾ ¦ © ¢ « ¬ ¾ ¦ © § ª ¿ ¢ « ­ ¡ ¢ ¼ ¤ À ¤ § ¾ ¦ © ª ³ ¾ ¦ © ¢ « ¬ ¾ ¦ © § ª À ¢ « ­ ¡ ¢ ¼ ¤ Á ¤ § ¾ ¦ © ª ³ ¾ ¦ © ¢ « ¬ ¨ ¥ ¥ ¡ © § ª Á ¢ « ­ ¡ ¢ ¼ ¤  ¤ § ¾ ¦ © ª ³ ¾ ¦ © ¢ « ¬ ¨ ¥ ¥ ¡ © § ª  ¢ « ­ ¡ ¢ ¼ ¤ ¬ ¤ § ¾ ¦ © ª ³ ¾ ¦ © ¢ « ¬ ¨ ¥ ¥ ¡ © § ª ¬ ¢ « ­ ¡ ¢ ¼ ¤ ¬ ¤ § ¨ ¥ ¥ ¡ © ¬ ¨ ¥ ¥ ¡ © § ª ¬ ¢ « ­ ¡ ¢ ¼ ¤ ¬ ¤ § ° ¥ © ­ ¯ ­ ¡ ¢ ¼ ² ¶ ¬ ¨ ¥ ¥ ¡ © § ¤ ± ¥ ² ¤ ³ ´ ª ³¢ µ « « ¬ ª § ° ¥ © § ¤ ± ¥ ² ¤ ³ ´ ª ³¢ µ « « ¬ ¢ ¶ § · ¸ ¸ ¥ ¸ » ¡ ¢ £ « ¬ ª « ª ³¨ ¥ ¥ ¡ © § ± ³ ¡ « ³ ¬ ¨ ¥ ¥ ¡ © ¬ § · ¸ ¸ ¥ ¸ ¡ ¡ · Ä § ´ µ ³ ´ µ « ¬ ¡ ¡ · Ä § ª Å Å ¡ ³¢ Å Å ¡ à « ¬ § § ¨ ¥ ¥ ¡ © ² ¹ ¢ « ° ¥ © § ± ¬ ± à ² ¹ ¸ º ² · § ¥ ¦ ª « § ± à ³ ¡ à « « ¯ ¡ ² ¥ ¸ º ² · ¡ ¡ · Ä § ¡ ³ ¡ à « « » ¡ ¢ ¼ « ¦ ¸ · Æ « ¬ Á Æ « § ¡ ¢ ¼ ¤ ¬ ¤ § ª ³¢ « « ¯ ¡ ² ¥ ¡ ¡ · Ä § ¡ ³ ¡ à « ¹ wobei die Definitionen der verwendeten Ausnahmen wie folgt aussehen: · ª ± · ¦ º ¥ » ¡ ¢ £ ¹ · ª ± · ¦ º ¥ » ¡ ¢ ¼ ¹ ² · ¸ ¸ ¥ ¸ 14.12.2005 ¬ ¸ º ¦ § ¤Ç È ¦ ± ® É ª ± · ¦ º ¥ Å ¤ Ê ² Ê ¤Ç ¤ « ¹ © A. Poetzsch-Heffter, TU Kaiserslautern 129 Beispiele: (Anwendung der eval-Funktion) Sei a ein TinyML-Ausdruck, dann lässt er sich mit (eval a E) auswerten; hier folgen ein paar Beispiele für TinyML-Ausdrücke: val succ val plus = Abs ("x", BinOp ("+",Var "x",Int 1)); = Abs ("x", Abs ("y", BinOp ("+",Var "x",Var "y"))); val twice = Abs ("f", Abs ("x", App (Var "f", App (Var "f",Var "x")))); val comp = Abs ("f",Abs ("g", Abs ("x", App (Var "f", App (Var "g",Var "x"))))); val fourt = App (twice, twice); val let1 = Let ("x",Int 7, Let ("f",Abs ("y",BinOp ("+",Var "x",Var "y")), App (Var "f",Int 5) )); val l1 = Con ("cons",[Int 9,Con ("nil",[])]); val ll = Con ("cons",[Int 2, Con ("cons",[BinOp ("+",Int 5,Int 4), Con ("cons",[Int 4, Con ("nil",[])])])]); Auswertungsbeispiele: - val erg = eval (App (App (fourt, succ), Int 7)) []; val erg = Intv 11 : value - val c = eval (App (fourt, succ)) []; val c = Clos (("x", App (#,#)),[("f",Clos #)]): value 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 130 Beispiele: (Zur Diskussion des Abschlusses) Schließlich zwei Beispiele, die demonstrieren, warum man bei der Auswertung von Abstraktionen einen Abschluss bilden muss: 1. Einfacher let-Ausdruck: Ë Ì Í Î Ï Ë Ð Ñ Ò Ó Ô Õ Ô Ö Ñ × Ð Ø Ö Ì Ô Ù Als abstrakter Syntaxbaum: Ú Ì Í Û Ü Ð Ü Ý Þ Ô Í Ò Ý ß à á Û ÜÖ Ü Ýâ Ó Ô ã ä Û Ü Ø Ü Ýå Ï æ Ü Ð Ü Ýå Ï æ ÜÖ Ü ç ç ç Auswertung liefert: è Ë é á Û Û ÜÖ Ü Ý ê â Ó Ô ã ä Û Ü Ð Ü Ý Þ Ô Í Î Û Ü Ø Ü Ýå Ï æ Ò ç Ü Ð Ü Ýå Ï æ ÜÖ Ü ç ç Ý ë ç 2. Let-Ausdruck mit Funktionsabstraktionen: let val s = (fn y => y+1) in (fn f => fn x => f (f x)) s end; Auswertung liefert: è Ë é á Û Û Ü Ð Ü Ý ê ß ä ä Û Ü Õ Ü Ýè Ë é á Û å Ï æ ì ç Ý Ü Õ Ü Ý ì ç ç Ý Û Ü á Ü Ýè Ë é á ì ç ë ç ( Auswertung siehe nächste Seite ) 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 131 Auswertung des 2. Ausdrucks, hier in λ-Notation: let s = λ y.y+1 in λ f. λ x. f (f x) s end Auswertung mit leerer Umgebung: eval (let s = λ y.y+1 in λ f. λ x. f (f x) s end) [ ] = eval (λ f. λ x. f (f x) s) [ (s, eval λ y.y+1 [] ) ] = eval (λ f. λ x. f (f x) s) [ (s, Clos ( (y,y+1), [] ) ) ] = let Clos ( (w,eb), E1) = eval (λ f. λ x. f (f x) ) [ (s, Clos ( (y,y+1), [] ) ) ] in let v = eval s [ (s, Clos ( (y,y+1), [] ) ) ] in eval eb ( (w,v)::E1) end end = let Clos ( (w,eb), E1) = Clos ( (f, λ x. f (f x)) , [ (s, Clos ( (y,y+1), [] ) ) ] in let v = Clos ( (y,y+1), [] ) ) in eval eb ( (w,v)::E1) end end = eval λ x. f (f x) [ (f, Clos ( (y,y+1), [] ) ) , (s, Clos ( (y,y+1), [] ) ) ] = Clos ( ( x, f (f x) ), [ (f, Clos ( (y,y+1), [] ) ) , (s, Clos ( (y,y+1), [] ) ) ] 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 132 Bemerkungen: • Der Interpreter für TinyML demonstriert die Verwendung von Funktionsabschlüssen, einer zentralen Technik bei der Implementierung funktionaler Sprachen. • Nicht eingegangen wurde auf die Behandlung rekursiver Funktionen. Dafür werden „zyklische“ Umgebungen benötigt: Die Umgebung zum Abschluss einer rekursiven Funktion f muss eine Bindung für f enthalten. (Die Realisierung zyklischer Umgebungen in funktionalen Sprachen bedarf zusätzlicher Techniken.) 2.3.3 Übersetzung einer einfachen funktionalen Sprache Die Übersetzungstechnik ist bei funktionalen Sprachen je nach Klasse unterschiedlich: strikte Sprachen í SECD-artige Maschinen nicht-strikte Sprachen í Graphreduktion Wir betrachten hier den ersten Fall. 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 133 Vorgehen: • Erläuterung der SECD-Maschine • Realisierung eines Übersetzers: TinyML î SECD Motivation: • Kennen lernen einer abstrakten Maschine • Beispiel zur Übersetzung funktionaler Sprachen • Benutzung funktionaler Sprachen zur Übersetzung Die SECD-Maschine: Die SECD-Maschine ist eine typische abstrakte Maschine: • Sie abstrahiert von speziellen Eigenschaften realer Maschinen und bietet damit eine gemeinsame Plattform zum Erarbeiten vieler maschinenunabhängiger Übersetzungsvorgänge. • Sie bietet eine Zwischensprache, die auf die Übersetzung funktionaler Sprachen zugeschnitten ist. Damit ergeben sich zwei Übersetzungsschritte: funktionale Sprache 14.12.2005 1. Üb.schritt SECD -Code 2. Üb.schritt © A. Poetzsch-Heffter, TU Kaiserslautern Maschinen -Sprache 134 1. Übersetzungsschritt: • Sprachanalyse (kontextfrei, kontextabhängig) • Übersetzung von Hochsprachkonstrukten (Rekursion, Pattern Matching, komplexere Ausdruckskonstrukte, Modulkonstrukte, ...) • Namen werden durch Indizes ersetzt. • Zielsprachunabhängige Optimierungen 2. Übersetzungsschritt: • Übersetzung der zum Teil noch recht maschinenfernen Konstrukte der abstrakten Maschine • Zielsprachabhängige Optimierungen Wir betrachten hier nur den 1. Übersetzungsschritt. Während reale Maschinen meist nur mit elementaren Werten arbeiten, operiert die SECD-Maschine auf vier Kellern, den sogenannten Registern: • Stack: der Keller für Zwischenergebnisse • Environment: enthält die Variablenbindungen • Control: speichert den aktuell auszuführenden Code • Dump: kellert Maschinenkonfigurationen Zentrale Idee der SECD-Maschine: Verzichte auf Sprünge und verwalte auch das Ablaufverhalten kellerartig. 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 135 Syntax der SECD-Maschinensprache: Die SECD-Maschine besitzt die folgenden Befehle (abstrakte Syntax beschrieben als ML-Datentyp): datatype code = LD of value | LDV of int | LDC of code list | LDT of string * int | APP | RAP of int | DUM of int | COND of code list * code list | RET | ADD|SUB|MULT|NOT|EQ|LT|GT|HD|TL ; Dabei repräsentiert der Typ ï ð ñ ò ó die von der SECD-Maschine unterstützten Werte: datatype value = I of int | B of bool | T of string * value list | CL of code list * value list Neben den Basiswerten ô õ ö und ÷ ø ø ñ kann die SECDMaschine direkt mit Termen (T) und mit FunktionsAbschlüssen (CL) umgehen, wobei: • die 1. Komponente den Code für Abstraktion enthält, • die 2. Komponente die Umgebung repräsentiert. 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 136 Semantik der SECD-Maschinensprache: Die Arbeitsweise der SECD-Maschine (und damit die Semantik ihrer Befehle) beschreiben wir operationell, indem wir den Zustandsraum, Transitionsregeln sowie initiale und terminale Zustände beschreiben: • Ein Zustand ist ein Tupel der Form (S, E, C, D) mit S : value list, E : value list, C : code list, D : (value list * value list * code list) list • Transitionsregeln haben die Form: (S,E,C,D) ù (S‘,E‘,C‘,D‘) und beschreiben den Übergang von einem Zustand in den Folgezustand (siehe unten). • In initialen Zuständen sind S, E, D leer; C enthält den auszuführenden Code. • Ein korrekt terminierendes Programm endet in einem Zustand der Form (S, E, [ ], [ ] ). Transitionsregeln: Im Folgenden beschreiben wir die Transitionsregeln der SECD-Maschine mit Ausnahme derjenigen für RAP (rekursive Applikation) und DUM (Erzeugen von Dummy-Einträgen in der Umgebung). Diese Befehle dienen der hier nicht beschriebenen Behandlung rekursiver Funktionen. Keller werden mit der Listennotation beschrieben. 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 137 Ladebefehle LD, LDV, LDC, LDT: Alle Ladebefehle kellern ihr Argument. Int- und bool-Werte werden mit LD geladen: ( S, E, (LD x)::C, D ) ( x::S, E, C, D ) ú Der Wert einer Variablen wird mit LDV n geladen, wobei n der Index der Variablen in der Umgebung ist und die Funktion sel den n-ten Eintrag aus der Umgebung ausliest: ( S, E, (LDV n)::C, D ) ( (sel n E)::S, E, C, D ) ú Der LDC-Befehl lädt den Code zu einer Funktion in Form eines Funktionsabschlusses: ( S, E, (LDC C‘)::C, D ) ú (CL (C‘,E)::S, E, C, D ) Der Befehl LDT (c,n) wendet den n-stelligen Konstruktor auf die n obersten Kellerelemente an: ( xn::...::x1::S, E, (LDT (c,n))::C, D ) ú 14.12.2005 ( c(x1,...,xn)::S, E, C, D ) © A. Poetzsch-Heffter, TU Kaiserslautern 138 Die Befehle APP, COND und RET: Der APP-Befehl erwartet einen Abschluss und das Argument auf dem Stack. Er sichert die nachfolgende Maschinenkonfiguration, fügt das Argument der Umgebung zu und wertet den Code des Abschlusses aus: ( (CL (C‘,E‘))::x::S, E, APP::C, D ) û ( [ ], x::E‘, C‘, (S,E,C)::D ) APP entspricht dem Ansprung einer Prozedur. Ist der Control-Keller leer, erfolgt der „Rücksprung“, bei dem die im Dump gesicherte Maschinenkonfiguration wieder hergestellt wird; dabei bleibt das Ergebnis auf dem Keller: ( x::S, E, [ ], (S‘,E‘,C‘)::D ) û ( x::S‘, E‘, C‘, D ) Der COND-Befehl realisiert einen bedingten Ausdruck. Er erwartet einen booleschen Wert b auf dem Keller. In Abhängigkeit von b wird einer der Zweige ausgeführt, die beide mit dem RET-Befehl schließen. Die nachfolgende Codeliste wird gesichert: ( (B true)::S, E, (COND (CT,CF))::C, D ) û (S, E, CT, ( [], [], C)::D ) ( (B false)::S, E, (COND (CT,CF))::C, D ) û 14.12.2005 (S, E, CF, ( [], [], C)::D ) © A. Poetzsch-Heffter, TU Kaiserslautern 139 Der RET-Befehl bewirkt den „Rücksprung“ aus einem Zweig eines bedingten Ausdrucks (er markiert den Unterschied zu einem Rücksprung von einer Funktion): ( S, E, RET::[], ( [], [], C )::D ) ( S, E, C, D ) ü Befehle für die vordefinierten Operationen: Die Wirkung der vordefinierten Operationen wird hier anhand von Beispielen erläutert. Zu beachten ist, dass die Argumente in umgekehrter Reihenfolge erwartet werden: ( y::x::S, E, SUB::C, D ) ü ( (x-y)::S, E, C, D ) ( y::x::S, E, EQ::C, D ) ü ( (x=y)::S, E, C, D ) ( (cons (x,y))::S, E, HD::C, D ) ü ( x::S, E, C, D ) ( (cons (x,y))::S, E, TL::C, D ) ü ( y::S, E, C, D ) Befehlszyklus der SECD-Maschine: Ausgehend von einem initialen Zustand führe solange Transitionsregeln aus, bis • Control- und Dump-Keller leer sind oder • ansonsten keine Regel mehr anwendbar ist. Der zweite Fall kommt bei korrektem SECD-Code nicht vor. 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 140 Übersetzung von TinyML nach SECD: Die folgende Übersetzungsfunktion ý þ ÿ ÿ übersetzt korrekte abstrakte TinyML-Syntaxbäume in SECD-Code: comtiml: expr * ident list code list Eine der Übersetzungsaufgaben ist es, die Adressierung der Variablen bzgl. der Umgebung zu realisieren (vgl. Erklärung zu LDV). Die Zuordnung von Variablen zu Positionsindizes wird in einer Bezeichnerliste verwaltet, die als zweiter Parameter mitgeführt wird. Der Position einer Variablen v in dieser Liste entspricht die Position des Wertes von v in der Umgebung zur Laufzeit: position: ident ident list int fun position x (y::ys) = if x=y then 1 else 1 + position x ys 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 141 Realisierung von comtiml Die Übersetzungsfuntion ist rekursiv über den Aufbau der abstrakten Syntax von TinyML (vgl. Folie 127) definiert (d.h. wir haben 10 Fälle zu betrachten). Grundlegendes Übersetzungsschema: Für jeden TinyML-Ausdruck A wird der SECD-Code erzeugt, der A auswertet und den resultierenden Wert auf S kellert. Übersetzung von Konstanten: Direkt in den entsprechenden Ladebefehl comtiml (Int i) N = [ LD (I i) ] comtiml (Bool b) N = [ LD (B b) ] Übersetzung von Variablen: Die Position der Variablen v in der Bezeichnerliste wird zur Adressierung von v in der Umgebung N benutzt: comtiml (Var id) N = [ LDV (position id N) ] 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 142 Übersetzung von Konstruktortermen: 1. Erzeuge Code zum Auswerten aller Subterme. 2. Hänge die resultierenden Codelisten zusammen. 3. Hänge einen LDT-Befehl mit der Argumentanzahl an: comtiml (Con (id,expl)) N = concat(map (fn e=>comtiml e N) expl) @ [ LDT (id,length expl) ] Übersetzung von vordefinierten Operationen: Erzeuge Code für die Argumente und hänge dann einen Maschinenbefehl an, der die vordefinierte Operation realisiert. Der Zusammenhang zwischen den Bezeichnern vordefinierter Operationen und den zugehörigen Befehlen liefert folgende Funktion: fun cmd "+" = | cmd "hd" = | cmd "eq" = | ... ADD HD EQ Damit comtiml (UnOp (f,e)) N = comtiml e N @ [cmd f] comtiml (BinOp (f,e1,e2)) N = comtiml e1 N @ comtiml e2 N @ [cmd f] 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 143 Beachte: Bei den vordefinierten Operationen und den Konstruktortermen landen die Argumente in umgekehrter Reihenfolge auf dem Keller. Übersetzung der Fallunterscheidung: Erzeuge Code zum Auswerten der Bedingung und dann den COND-Befehl mit den durch RET-Befehle beendeten Zweigen: comtiml (Cond (e,e1,e2)) N = comtiml e N @ [COND ( comtiml e1 N @ [RET] , comtiml e2 N @ [RET] )] Übersetzung der Funktionsanwendung: Bei typkorrekten Programmen kann man davon ausgehen, dass das erste Argument zu einem Abschluss ausgewertet wird. Der APP-Befehl erwartet den Abschluss oben auf dem Laufzeitkeller, das Argument darunter: comtiml (App (eclos,earg)) N = comtiml earg N @ comtiml eclos N @[APP] 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 144 Übersetzung der Funktionsabstraktion: Der LDC-Befehl ist der Kern der Übersetzung einer Abstraktion Abs (x,e) und bekommt als Parameter den Code für den Rumpf e. Dabei wird der Rumpf mit der um x erweiterten Bezeichnerliste übersetzt: comtiml (Abs(x,e)) N = [LDC (comtiml e (x::N))] Übersetzung von Let-Ausdrücken: Operationell (nicht bzgl. der Typisierung) gilt folgende Äquivalenz: let x = e1 in e2 end == (λ x. e2) e1 Dementsprechend kann man den let-Ausdruck wie den Ausdruck auf der rechten Seite übersetzen: comtiml (Let(x,e1,e2)) N = comtiml e1 N @ [LDC (comtiml e2 (x::N)), APP] Damit sind alle Alternativen der abstrakten Syntax von TinyML behandelt und die Übersetzungsfunktion vollständig beschrieben. 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 145 Bemerkungen: • Die Übersetzungsfunktion demonstriert das grundlegende Konzept zur Übersetzung funktionaler Sprachen in abstrakten Maschinencode. • Die Übersetzung ließe sich natürlich auch mittels einer Attributierung spezifizieren. Wir betrachten hier exemplarisch den bedingten Ausdruck: comtiml (Cond (e,e1,e2)) N = comtiml e N @ [COND ( comtiml e1 N @ [RET] , comtiml e2 N @ [RET] )] Bezeichnerliste vom Typ ident list Codeliste vom Typ code list Cond CB @ [ COND (CT @ [ RET ], CE @ [ RET ] ) ] expr expr CB expr CT CE Beachte, dass die rekursiven Aufrufe in der Attributierungsspezifikation implizit sind. 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 146 • Anhand der Übersetzung von TinyML sollte illustriert werden, wie funktionale Sprachen zur Beschreibung und Implementierung von Übersetzern verwendet werden können. • Das Beispiel ist nicht geeignet, die Benutzung der SECD-Maschine zu motivieren; dafür müsste die Quellsprache umfangreicher sein. • Auf die Behandlung eines der zentralen Sprachmittel (rekursive Funktionen) wurde nicht eingegangen. Quellenhinweis: Abschnitte 2.3.2 und 2.3.3 folgen weitgehend: Martin Erwig: Grundlagen funktionaler Programmierung, Oldenbourg Verlag; Kapitel 6. 14.12.2005 © A. Poetzsch-Heffter, TU Kaiserslautern 147