Technische Universität München Institut für Informatik Prof. Tobias Nipkow, Ph.D. N. Schirmer, M. Blume WS 2002/2003 Teil 1 Übungsblatt 1 16.10.2002 Programmierpraktikum Funktionales Programmieren und Compilerbau Warm-Up in Haskell Abgabetermin: Mittwoch, 30. 10. 2002 Im ersten Semester wurde Gofer von Mark P. Jones als funktionale Programmiersprache eingeführt. Für dieses Praktikum wird Haskell verwendet. Da beide Sprachen aus dem gleichen Prozess hervorgegangen sind, ähneln sie sich in weiten Teilen. In diesem Übungsblatt geht es darum, grundlegende Sprachelemente von Haskell vorzustellen bzw. zu wiederholen. Ziel ist, einen Interpreter und einen Compiler für arithmetische Ausdrücke zu programmieren. Zum Schluss soll der entstandene Code auch noch decompiliert werden. Aufgabe 1: Modellierung arithmetischer Ausdrücke/Interpreter Wir definieren mit dem Schlüsselwort data einen eigenen Datentyp Expr, der einen arithmetischen Ausdruck repräsentiert. Er besitzt zwei Konstruktoren, Const und BinOp. Konstruktoren konstruieren einen Wert eines Typs. So wie z. B. 2 ein Wert des Typs Integer ist, ist Add ein Wert vom Typ Op oder sind Const 4.3 oder BinOp (Const 3) Sub (BinOp (Const 2) Add (Const 1)) Werte vom Typ Expr. testE enthält den Ausdruck (2 + 3) * ( 4 - 1). Mit deriving Show wird Haskell mitgeteilt, dass der durch data UnserTyp definierte Typ eine Instanz der Typklasse Show ist. So ist sichergestellt, dass z. B. bei der Eingabe von testE in Hugs der Ausdruck ausgegeben werden kann. module Arith where type Value = Float data Expr = Const Value | BinOp Expr Op Expr deriving Show data Op = Add | Sub | Mul | Div deriving Show testE = BinOp (BinOp (Const 2) Add (Const 3)) Mul (BinOp (Const 4) Sub (Const 1)) Es soll nun zunächst ein Interpreter zur Berechnung des arithmetischen Ausdrucks implementiert werden. Im Gegensatz zu einem Compiler, der einen Ausdruck in eine Befehlsfolge für eine Maschine übersetzt, berechnet ein Interpreter den Wert eines Ausdrucks direkt auf seiner abstrakten Darstellung (abstrakte Syntax). 1 Implementieren Sie die Funktion interprete :: Expr -> Value Als Hilfsfunktion dient apply :: Op -> Value -> Value -> Value Z. B. berechnet apply Add 3.2 0.8 den Wert 4. Aufgabe 2: Eine einfache Stackmaschine Nun soll die Funktion run entwickelt werden, die ein einfaches Programm abarbeitet und den Stack nach Ausführung des Programms zurückgibt. run simuliert in unserem Beispiel also eine einfache Stackmaschine, die nur einen Operanden-Stack hat. Als Instruktionen kommen daher nur Push und arithmetische Operationen in Frage, wobei für die arithmetische Operation die obersten zwei Stackelemente die Operanden sind. data Instr = Push Value | Apply Op deriving Show type Stack = [Value] Die Befehlsliste [Push 3, Push 1, Apply Sub] hätte auf den Stack folgende Auswirkung: Befehl Push 3 Push 1 Apply Sub Stack [3] [1,3] [2] Implementieren Sie die Funktion execute, die auf dem Stack eine Instruktion abarbeitet! Verwenden Sie execute, um die Funktion run zu definieren! execute :: Stack -> Instr -> Stack type Prog = [Instr] run :: Prog -> Stack Hinweis: Schreiben Sie run zunächst ohne weitere Hilfsfunktionen außer execute. Anschließend können Sie run auch sehr elegant mit der Higher Order Function foldl implementieren. Die Funktion foldl hat die Funktionalität foldl :: (a -> b -> a) -> a -> [b] -> a. Sie ruft die Funktion, die im ersten Parameter übergeben wird mit dem zweiten Parameter und dem ersten Element der Liste auf. Mit dem Ergebnis und dem zweiten Element der Liste wendet Sie die Funktion wieder an usw., bis das Ende der Liste erreicht ist. Beispiel: foldl (/) 64 [4,2,4] Das Ergebnis ist 2.0. Aufgabe 3: Compiler Jetzt ist endlich der Compiler dran! Setzen Sie die Funktion compile :: Expr -> Prog um! Anmerkung: Es gilt die folgende Gleichung: interprete = head . run . compile oder ausführlich: interprete expr = head (run (compile expr)) Der Operator . ist die Komposition von Funktionen, wobei zuerst die rechteste Funktion auf das 2 Argument angewendet wird. Es ist übrigens typisch für die funktionale Programmierung, die Argumente in der Funktionsdefinition wegzulassen. Aufgabe 4: Disassembler (uncompile) In dieser Aufgabe geht es darum, die Umkehrfunktion zu compile zu entwerfen. Sie soll aus einem gegebenen Programm den arithmetischen Ausdruck konstruieren. Daher gilt: uncompile (compile expr) = expr Tipp: Es ist nützlich, die Hilfsfunktion uncompileInstr zu definieren, die eine einzelne Instruktion decompiliert. Sie könnte ähnlich wie die Funktion execute einen Stack verwenden, mit dem Unterschied, dass diesmal Ausdrücke auf dem Stack liegen! uncompile :: Prog -> Expr type ExpStack = [Expr] uncompileInstr :: ExpStack -> Instr -> ExpStack Aufgabe 5: Expressions mit Variablen In dieser letzten Aufgabe sollen nun in dem Typ Expr neben Konstanten auch Variablen zugelassen werden: type Variable = String data Expr = Const Value | Var Variable | Exp Expr Op Expr deriving Show Den Speicher realisieren wir durch eine Funktion. Diese Funktion muss zuerst mittels buildStore aufgebaut werden. update nimmt eine Funktion, ein Tupel, und gibt eine neue Funktion zurück. buildStore wendet update auf eine Liste von Tupeln an und erzeugt somit die Funktion, die den Speicher repräsentiert. type Store = Variable -> Value buildStore :: [(Variable,Value)] -> Store buildStore = foldl update (\x -> error ((show x) ++ "not defined in store")) update :: Store -> (Variable,Value) -> Store update store (x,y) = \v -> if v==x then y else store v Ist z. B. ein Wert speicher vom Typ Store gegeben, so kann mit speicher ’x’ auf den Wert der Variable x zugegriffen werden. Der Speicher kann z. B. so erzeugt werden: store = buildStore [(’x’,20),(’y’,10)] Passen Sie alle Funktionen an die Verwendung von Variablen an! Bei den Funktionen interprete, execute und run soll der Speicher als Parameter übergeben werden. Der Typ Instr sieht bei der Verwendung von Variablen folgendermaßen aus: data Instr = Push Value | Load Variable | Apply Op deriving Show Geben Sie die Funktion buildStore ohne die Verwendung der Hilfsfunktion foldl an! 3