Die Präsentation wird geladen. Bitte warten

Die Präsentation wird geladen. Bitte warten

Grundlagen der Informatik 1 Thema 9: Interpreter nach dem

Ähnliche Präsentationen


Präsentation zum Thema: "Grundlagen der Informatik 1 Thema 9: Interpreter nach dem"—  Präsentation transkript:

1 Grundlagen der Informatik 1 Thema 9: Interpreter nach dem
Grundlagen der Informatik 1 Thema 9: Interpreter nach dem Substitutionsmodell und Streams Prof. Dr. Max Mühlhäuser Dr. Guido Rößling

2 Rückblick: Das Substitutionsmodell
Wir haben die Sprachmechanismen von Scheme bisher informal anhand des Substitutionsmodells verdeutlicht Zahlen, Symbole etc. sind selbstauswertend Primitive Operationen: entsprechende Maschinenbefehle ausführen make-struct, cons: selbstauswertend (lambda (…) …): selbstauswertend (define …): Erweitere die Umgebung (local …): Herausziehen der lokalen Definitionen auf oberste Ebene, jeweils neuen Namen vergeben (operator op-1 … op-n): Auswertung von operator sowie op-1 … op-n Operator muss (lambda (x-1 … x-n) exp) ergeben Ergebnis ist die Auswertung exp‘, wobei exp‘ sich aus exp durch Substitution der freien Vorkommen von x-1 … x-n durch die Ergebnisse der Auswertung von op-1 … op-n ergibt Lexikalisches Scoping

3 Rückblick: Das Substitutionsmodell
Der Auswertungsprozess ist selbst ein Rechenprozess, den wir als Prozedur präzisieren und implementieren können Erinnern sie sich an das Zitat „Programming is a Good Medium for Expressing Poorly Understood and Sloppily Formulated Ideas” vom Anfang der Vorlesung? Der Interpreter einer Programmiersprache, der die Bedeutung der Ausdrücke in der Programmiersprache festlegt, ist auch nur ein Programm.

4 Terminologie Die interpretierende Sprache oder Basissprache ist die Sprache, in der der Interpreter implementiert ist Die interpretierte Sprache ist die Sprache, die der Interpreter auswertet Prinzipiell ist es möglich, in jeder Programmiersprache, die ein paar Basiskriterien erfüllt einen Interpreter für jede andere Programmiersprache zu schreiben Sogenannte “Turing-Vollständigkeit” Das wird von nahezu jeder universellen Programmiersprache wie Java, C, Scheme, Pascal, Perl, … erfüllt Eines der fundamentalen Resultate der Informatik Wenn die interpretierte Sprache und die Basissprache identisch sind, spricht man von einem Meta-Interpreter oder metazirkulären Interpreter

5 Interpreter und Programmsemantik
Interpreter sind eine Möglichkeit, um die Bedeutung (so genannte “Semantik”) einer Programmiersprache zu definieren Ein solcher Interpreter kann daher keine “Fehler” enthalten, weil er die Bedeutung definiert Ein Fehler kann immer nur in Bezug auf eine Spezifikation festgestellt werden Zum Beispiel können wir dem Symbol “+” in einem Interpreter als Bedeutung eine Multiplikationsprozedur zuweisen Das ist kein Fehler, sondern eine mögliche Definition! Einige Programmiersprachen werden tatsächlich offiziell durch Interpreter definiert (z.B. Scheme, SML)

6 Interpreter und Programmsemantik
Allerdings kann die Bedeutung auch anders festgelegt werden Informelle Sprachdefinitionen Formale Sprachdefinitionen Denotationelle Semantik abstract state machines In diesem Fall macht es wieder Sinn, von Fehlern eines Interpreters relativ zu einer Sprachdefinition zu reden

7 Interpreter und Programmsemantik
Wenn man einen Interpreter zur Sprachdefinition benutzt, setzt man voraus, dass der Nutzer die Bedeutung der Basissprache bereits versteht. Rekursionsanker ist meistens entweder “Prosa” (deutsch/englisch) oder die Mathematik Ein vieldiskutiertes Problem in der Mathematik (Modelltheorie vs. Beweistheorie) und der Philosophie, aber nicht Thema von GdI-1. Insbesondere ist ein Meta-Interpreter keine vollständige Definition einer Programmiersprache! Ein Meta-Interpreter ist zunächst nur eine Menge rekursiver Funktionsgleichungen, die viele oder auch gar keine Lösung haben können.

8 Warum untersuchen wir Interpreter?
“Geheimnisse“ aufdecken, wie Prozesse implementiert sind  daraus tieferes Verständnis erlangen Wir betrachten uns selbst als Sprachentwickler und nicht nur als Nutzer einer Sprache, die andere entwickelt haben. Einblick gewinnen, wie neue Sprachen implementiert werden Die Definition neuer Sprachen ist ein mächtiges Mittel, um Komplexität im Design der Entwicklung zu kontrollieren. Neue Sprachen erhöhen unsere Fähigkeit, mit komplexen Problemen umzugehen, indem sie uns Mittel zur Verfügung stellen, mit deren Hilfe wir Probleme direkter beschreiben. Means: using primitives, means of combination, and means of abstraction that are particularly well suited to the problem at hand. The same idea is pervasive throughout all of engineering. For example, electrical engineers use many different languages for describing circuits. Two of these are the language of electrical networks and the language of electrical systems. The network language emphasizes the physical modeling of devices in terms of discrete electrical elements. The primitive objects of the network language are primitive electrical components such as resistors, capacitors, inductors, and transistors, which are characterized in terms of physical variables called voltage and current. When describing circuits in the network language, the engineer is concerned with the physical characteristics of a design. In contrast, the primitive objects of the system language are signal-processing modules such as filters and amplifiers. Only the functional behavior of the modules is relevant, and signals are manipulated without concern for their physical realization as voltages and currents.

9 Warum untersuchen wir Interpreter?
Man kann viele Programme als einen Interpreter einer Sprache betrachten. Beispiel: ein Programm zur Berechnung von Polynomen in Scheme Verkörpert die Rechenregeln für Polynome und implementiert sie als Operationen auf Listenstrukturen in Scheme Wenn wir dieses Programm durch Prozeduren zum Lesen und Ausgeben von Polynomen ergänzen, haben wir den Kern einer spezialisierten Sprache für die symbolische Mathematik.

10 Ein Interpreter nach dem Substitutionsmodell
Wir werden im Folgenden einen Meta-Interpreter für eine Teilmenge von Scheme betrachten, der in Scheme selbst implementiert wird Explizit implementierte Konstrukte Auswertungsreihenfolge Substitution Umgebung Implizit implementierte Konstrukte Primitive Operationen (Addition, Multiplikation, …) Primitive Werte (Zahlen, boolesche Werte) Rekursion, if Speicherverwaltung Von uns hier ignorierte Konstrukte define-struct local

11 Stopp #1 auf dem Weg Randnotiz #1: Scheme ist besonders gut geeignet, um Sprachinterpreter zu schreiben Einfache Syntax, wenige aber mächtige Sprachkonstrukte Randnotiz #2: Obwohl der Interpreter für Scheme geschrieben wird, enthält er die grundlegende Struktur eines Interpreters für jede Ausdrucks-orientierte Sprache, die verwendet werden kann, um Programme auf einer sequentiellen Maschine zu schreiben. Genau genommen enthalten viele Programme (nicht nur Interpreter) tief unten einen kleinen „Scheme“ Interpreter Greenspun's Tenth Rule of Programming: “Any sufficiently complicated C or Fortran program contains an ad-hoc, informally-specified bug-ridden slow implementation of half of Common Lisp”.

12 Stopp #2 auf dem Weg Studenten sind von Meta-Interpretern häufig verwirrt. Verwechseln Sie nicht die Basissprache mit der interpretierten Sprache! Wenn Sie einen Interpreter in Scheme schreiben, beeinflusst das nicht die Auswertung des DrScheme Interpreters Wenn Ihr Interpreter bei “+” multipliziert, ergibt (+ 3 5) in DrScheme nach wie vor 8

13 Aufbau des Interpreters
Der Interpreter hat eine ähnliche Struktur wie der Interpreter für arithmetische Ausdrücke, die wir bereits kennengelernt haben Syntaxdefinition anhand entsprechender Datentypen Parser – transformiert s-expressions in entsprechende Exemplare der Syntax-Datentypen (sogenannter abstract syntax tree - AST) Auswertungsprozedur (eval...) Neu hinzu kommen folgende Funktionalitäten: Substitution bei Funktionsanwendung Umgebung, enthält primitive Operationen und selbst definierte Namen Startprozedur, setzt die Umgebung auf und führt ein Scheme Programm aus

14 Syntax Definition der abstrakten Syntax ;; An expression exp is either
;; 1. (make-definition var val) where var is a symbol and ;; val is an exp ;; 2. (make-proc params exp) where params is a list-of-symbols ;; and exp is an exp ;; 3. (make-if-clause predicate c a) where predicate, c and a are exp ;; 4. (make-application operator operands) where operator is an exp ;; and operands is (listof exp) ;; 5. (make-variable n) where n is a symbol ;; 6. a number or a boolean or a string (define-struct definition (variable value)) (define-struct proc (parameters exp)) (define-struct if-clause (predicate consequent alternative)) (define-struct application (operator operands)) (define-struct variable (name)) make-definition – nur fuer variablen definitionen make-proc – zur definitiojn von prozeduren/funktionen – insbesondere fuer lambda-ausdruecke make-application – is the actual application of a previously defined procedure

15 Syntax Wir lassen einige Teile von Scheme, soweit wir es kennen, weg
local-Definitionen define-struct cond Direkte Definition von Prozeduren wie(define (f x) …) Der letzte Punkt ist keine Einschränkung, da wir lambda Ausdrücke zulassen Mit lambda Ausdrücken (make-proc …) können wir diese Form von Prozedurdefinition leicht kodieren: (define (f x-1 … x-n) exp) wird zu (define f (lambda (x-1 … x-n) exp)) Auch cond kann mittels if kodiert werden Die Menge der primitiven Operationen ist in der Syntax nicht festgelegt

16 Parser ;; parse converts an s-expression into an exp structure
;; parse : s-expression -> exp (define (parse sexp) (cond [(number? sexp) sexp] [(string? sexp) sexp] [(boolean? sexp) sexp] [(symbol? sexp) (make-variable sexp)] [(cons? sexp) (local ((define op (first sexp))) (cond [(and (symbol? op) (symbol=? op 'lambda)) (make-proc (second sexp) (parse (third sexp)))] [(and (symbol? op) (symbol=? op 'if)) (make-if-clause (parse (second sexp)) (parse (third sexp)) (parse (fourth sexp)))] [(and (symbol? op) (symbol=? op 'define)) (make-definition (second sexp) (parse (third sexp)))] [else (make-application (parse (first sexp)) (map parse (rest sexp)))]))]))

17 Parsing und abstrakte Syntax
Die Datentypdefinitionen sind eine Abstraktion von der konkreten Syntax des Programms So sind Klammern, Kommentare etc. verschwunden Die konkrete Syntax zu dieser abstrakten Syntax könnte auch anders aussehen, z.B. f(op1,op2) statt (f op1 op2) oder 3+5 statt (+ 3 5) Daher der Name abstrakte Syntax Der Parser ist durch die Verwendung von s-expressions einfach Ohne s-expressions erhält der Parser lediglich eine Liste von Zeichen und muss daraus die abstrakte Syntax extrahieren Eine „Wissenschaft für sich“, aber nicht Thema von GdI-1 Wichtig ist, dass wir anhand des ersten Elements einer Liste im Parser immer bereits entscheiden können, welchen AST-Datentyp wir erzeugen müssen Andernfalls würde das Parsen erheblich komplexer AST – Abstract Syntax Tree

18 Der abstrakte Datentyp map
Zur Implementierung des Interpreters verwenden wir einen neuen Datentyp: map Dieser Datentyp ähnelt Vektoren, doch statt Werte mit Zahlen zu assoziieren, assoziiert er Werte mit Symbolen Das Symbol ist der Schlüssel, der mit einem Wert assoziiert wird Der Datentyp map wird häufig benötigt, nicht nur in Interpretern, daher hat die Implementierung dieses Datentyps nichts mit dem Interpreter zu tun 1 (list 1 4) 2 (list 4 5) 3 (list 3) 4 empty 5 (list 2 5) 6 (list 3 6) ‘CDU 36.8 ‘SPD 36.7 ‘FDP 9.4 Ein Vektor Eine Map

19 Der abstrakte Datentyp map
Normalerweise würde man diesen Standard-Datentyp nicht selbst implementieren, sondern eine vorhandene Implementierung in einer Standard-Bibliothek benutzen Wir wollen aber genauer verstehen, wie maps funktionieren Daher implementieren wir diesen Datentyp selbst

20 Der abstrakte Datentyp map
Konstruktoren Selektoren Meist gibt es noch weitere Konstruktoren, Selektoren und Prädikate ;; Data type definition for map ;; ;; A (mapof X) is either ;; 1. (map-create names values) where names ;; is a (listof symbol) and values is a (listof X) ;; 2. (map-extend var val base-env) where var is ;; a symbol, val is an X, and base-env is a (mapof X) ;; 3. (map-remove name a-map) where name is a symbol and ;; a-map is a (mapof X) ;; 4. (map-remove-all names a-map) where names is a ;; (listof symbol) and a-map is a (mapof X) ;; map-lookup: symbol (mapof X) -> X or empty

21 Der abstrakte Datentyp map
Implementierung der Konstruktoren (define-struct binding (name value)) ;; hidden from users of map ;; map-create: (listof symbol) (listof X) -> (mapof X) (define (map-create names values) (map make-binding names values)) ;; map-extend: symbol X (mapof X) -> (mapof X) (define (map-extend var val base-env) (cons (make-binding var val) base-env)) ;; map-remove: symbol (mapof X) -> (mapof X) (define (map-remove name a-map) (filter (lambda (b) (not (symbol=? name (binding-name b)))) a-map)) ;; map-remove-all: (listof symbol) (mapof X) -> (mapof X) (define (map-remove-all names a-map) (foldr map-remove a-map names)) diese version von map verwendet funktion ‘make-binding’ und 2 listen (names, values) um eine neue liste zu genieren (foldr f base list)

22 Der abstrakte Datentyp map
Implementierung des Selektors ;; map-lookup: symbol (mapof X) -> X or empty (define (map-lookup name a-map) (if (empty? a-map) empty (if (symbol=? name (binding-name (first a-map))) (binding-value (first a-map)) (map-lookup name (rest a-map)))))

23 Der abstrakte Datentyp map
Maps gibt es in fast jeder Programmiersprache Meist in Form von Bibliotheksfunktionen oder (Seltener) direkt in die Sprache integriert Unsere Implementierung ist voll funktionsfähig, aber nicht sehr effizient Lookup kostet O(n), wobei n die Zahl der Einträge ist Es gibt wesentlich effizientere Implementierungen z.B. über „Hash tables“ (ermöglichen Lookup in fast konstanter Zeit) In vielen Implementierungen können auch andere Datentypen als Symbol als Schlüssel verwendet werden Wir können leicht unsere Implementierung verändern; alle Nutzer vom map Datentyp, die sich an das Konstruktor-Selektor Protokoll halten, funktionieren weiterhin Der Datentyp „map“ hat nichts mit der Listenfunktion „map“ zu tun

24 (Selbstauswertende) Werte
Betrachten wir die Werte, die der Interpreter produzieren kann (define-struct primitive-procedure (the-proc)) ;; A value val is either ;; 1. a number ;; 2. a String ;; 3. a Boolean ;; 4. (make-proc params exp) where params is a list-of-symbols ;; and exp is an exp ;; 5. (make-primitive-procedure p) where p is a Scheme procedure ;; all values are self-evaluating ;; self-evaluating :: exp -> boolean (define (self-evaluating? exp) (cond [(number? exp) true] [(string? exp) true] [(boolean? exp) true] [(proc? exp) true] ;; procedures are self-evaluating!! [(primitive-procedure? exp) true] [else false])) eval = evaluation of single expressions ! cannot evaulate any define expression yet – we will do that later

25 Auswertung und Umgebung
Zum Beispiel env = Zum Beispiel (eval (make-application (make-variable '+) (list (make-variable 'x) 3)) env)  8 ;; Data type env: ;; An environment env is a (mapof val) ;; eval evaluates an expression exp in an environment env ;; eval : exp env -> val (define (eval exp env) …) ‘+ (make-primitive-procedure +) ‘x 5

26 Die Auswertungsprozedur
Grundstruktur: Weiterleitung an Spezialprozeduren für jeden Fall ;; eval evaluates an expression exp in an environment env ;; eval : exp env -> val (define (eval exp env) (cond [(self-evaluating? exp) exp] [(variable? exp) (eval-var exp env)] [(if-clause? exp) (eval-if exp env)] [(application? exp) (eval-app (eval (application-operator exp) env) (map (lambda (expr) (eval expr env)) (application-operands exp)) env)] [else (error 'eval (format "Unknown expression type: ~v" exp))])) (map (lamda (expr) …) evaluates all the operands first ! applicative order as all operands are evaluated first and then the operator is applied to the operands applikative Reihenfolge

27 Die Auswertungsprozedur
Auswertung von Variablen Auswertung von if Anweisungen ;; evaluates a variable by looking it up in the environment ;; eval-var :: exp env -> val (define (eval-var exp env) (local ((define val (map-lookup (variable-name exp) env))) (if (empty? val) (error 'eval-var (format "Unbound variable: ~v" (variable-name exp))) val))) ;; eval-if : exp env -> val (define (eval-if exp env) (if (eval (if-clause-predicate exp) env) (eval (if-clause-consequent exp) env) (eval (if-clause-alternative exp) env)))

28 Die Auswertungsprozedur
Auswertung von Funktionsanwendungen ;; eval-app: val (listof exp) env -> val (define (eval-app procedure arguments env) (cond [(primitive-procedure? procedure) (apply (primitive-procedure-the-proc procedure) arguments )] [(proc? procedure) (eval (subst (map-create (proc-parameters procedure) arguments) (proc-exp procedure)) env)] [else (error 'eval-app (format "Procedure expected, found: ~v" procedure))])) vgl. Auswertungsregel für zusammengesetzte Prozeduren (eval exp env)

29 Die Auswertungsprozedur
Die Prozedur „subst“ ist zunächst einmal auf unserer Wunschliste und wird als nächstes implementiert Die Prozedur „apply“ die innerhalb der Auswertungsprozedur benutzt wird, ist eine primitive Prozedur, mit der die Parameter einer Prozedur als Liste übergeben werden können (apply + (list 1 2 3))  ( ) (apply f (list a b))  (f a b) Wir brauchen apply, weil wir nicht wissen, wieviele Argumente die primitive Prozedur benötigt

30 Substitution Substitution wird als rekursive Funktion über Ausdrücken definiert Fallunterscheidung je nach Typ des Ausdrucks, wie bei eval Die zu ersetzenden Namen und Werte werden in einer map übergeben ;; subst substitutes all occurences of names in exp that are ;; associated with a value in a-map with that value, according ;; to the lexical scoping rules ;; subst : (mapof val) exp -> exp (define (subst a-map exp) …)

31 Substitution Substitution innerhalb von Prozeduren, Variablen und selbst-auswertenden Ausdrücken (define (subst a-map exp) (cond [(proc? exp) (make-proc (proc-parameters exp) (subst (map-remove-all (proc-parameters exp) a-map) (proc-exp exp)))] [(self-evaluating? exp) exp] [(variable? exp) (local ((define newval (map-lookup (variable-name exp) a-map))) (if (empty? newval) exp newval))] ...)) Lokal mit lambda gebundene Namen überdecken äußere Bindungen Variable wird ersetzt, falls entsprechender Eintrag in Substitutionstabelle existiert

32 Substitution if-clause?, application?:
Einfach in Unterausdrücken ersetzen (define (subst a-map exp) (cond ... [(if-clause? exp) (make-if-clause (subst a-map (if-clause-predicate exp)) (subst a-map (if-clause-consequent exp)) (subst a-map (if-clause-alternative exp)))] [(application? exp) (make-application (subst a-map (application-operator exp)) (map (lambda (op) (subst a-map op)) (application-operands exp)))] [else (error 'subst (format "Unknown expression type: ~v" exp))]))

33 Substitution Die Substitutionsprozedur enthält noch einen Fehler
Wenn der Ausdruck, der eingesetzt wird, eine freie Variable enthält, wird diese möglicherweise durch die Substitution fälschlicherweise gebunden Wir werden diesen Fehler (noch) nicht beheben Die Lösung ist etwas kompliziert Basisidee: Vor der Substitution in einem Lambda-Ausdruck wird geprüft, ob einer der Parameternamen frei in einem der Ausdrücke in der Substitutionstabelle vorkommt Falls ja, wird der Parameter umbenannt

34 Primitive Prozeduren Die gewünschten primitiven Operationen bilden die initiale Umgebung Kann leicht um neue primitive Operationen erweitert werden Es besteht kein Zwang, dass eine primitive Operation durch die primitive Operation desselben Namens in der Basissprache implementiert wird Wir können auch nicht-primitive Scheme Prozeduren benutzen, die dann in der interpretierten Sprache primitiv sind Wir könnten hier festlegen, dass das Symbol ‘+ in unserer Sprache Multiplikation bedeutet (define primitive-procedures (local ((define names '(first rest cons list empty? empty * / - + < = abs)) (define procs (list first rest cons list empty? empty * / - + < = abs))) (map-create names (map make-primitive-procedure procs))))

35 Ausführen von Programmen
Die eval Prozedur kann nur einzelne Ausdrücke auswerten, jedoch keine (define…) Anweisungen Ein Scheme Programm besteht jedoch aus einer Reihe von Definitionen und einer Reihe von Ausdrücken Die Ausführung eines Programms wird von der Prozedur run-prog erledigt Erstellt initiale Umgebung Erweitert Umgebung nach Auswertung einer (define…) Anweisung Für andere Ausdrücke werden die jeweiligen Ergebnisse berechnet und zu einer Ergebnisliste zusammengefügt

36 Ausführen von Programmen
;; a program prog is a (listof exp) ;; run-prog :: prog env -> (listof val) (define (run-prog prog env) (if (empty? prog) empty (local ((define exp (parse (first prog)))) (if (definition? exp) (run-prog (rest prog) (map-extend (definition-variable exp) (eval (definition-value exp) env) env)) (cons (eval exp env) (run-prog (rest prog) env)))))) Erweiterung der Umgebung Erweiterung der Ergebnisliste

37 Benutzung des Interpreters
(define testprog '((define ! (lambda (n) (if (= n 0) 1 (* n (! (- n 1)))))) (! 5))) (run-prog testprog primitive-procedures)

38 Daten als Programme (define (factorial n)   (if (= n 1)       1       (* (factorial (- n 1)) n)))                                        Wir können factorial als Beschreibung einer Maschine verstehen, die Teile zum subtrahieren, multiplizieren und testen auf Gleichheit enthält. Darüber hinaus enthält die Maschine einen Kippschalter und eine andere factorial Maschine. Die factorial Maschine ist unendlich, da sie eine andere factorial Maschine enthält

39 Interpreter als universelle Maschine
1 1 6 = 720 * - factorial 1 Die factorial Maschine

40 Interpreter als universelle Maschine
Analog können wir einen Interpreter als eine sehr spezielle Maschine ansehen, die als Eingabe die Beschreibung einer anderen Maschine erwartet. Über diese Eingabe konfiguriert sich der Interpreter selbst, so dass er die in der Eingabe beschriebene Maschine nachbildet. 6 eval 720 Zum Beispiel: wenn wir dem Evaluator die Definition von factorial übergeben, kann er Fakultäten berechnen. (define (factorial n) (if (= n 1) 1 (* (factorial (- n 1)) n)))

41 Interpreter als universelle Maschine
Unser Interpreter kann als universelle Maschine betrachtet werden. Er imitiert andere Maschinen, wenn sie als Scheme Programme beschrieben sind. Stellen Sie sich einen analogen Interpreter für elektrische Schaltkreise vor … ein Schaltkreis, der als Signal die Pläne für andere Schaltkreise empfängt, z.B. ein Filter. Basierend auf diesem Input verhält sich der Schaltkreis wie ein Filter. So ein universeller elektrischer Schaltkreis ist so komplex, dass er kaum vorstellbar ist. Dagegen ist es überraschend, wie einfach der Code des Programm-Interpreters ist.

42 Daten als Programme Der Interpreter ist eine Brücke zwischen Datenobjekten, die von unserer Programmiersprache manipuliert werden, und der Programmiersprache selbst. Unser Interpreter-Programm läuft und ein Nutzer gibt Ausdrücke an und beobachtet die Ergebnisse… Aus dem Blickwinkel des Nutzers ist die Eingabe (* x x) ein Ausdruck in der Programmiersprache, den der Interpreter ausführen soll. Vom Blickwinkel des Interpreterprogramms ist der Ausdruck nur eine Liste von Symbolen, *, x, und x, die nach festgelegten Regeln manipuliert werden sollen.

43 Normale Auswertungsreihenfolge
Mit Hilfe des Interpreters können wir nun sehr präzise ausdrücken, was normale Auswertungsreihenfolge ist und wie sie sich von applikativer Reihenfolge unterscheidet Informal: Bei normaler Auswertungsreihenfolge wird nur der Operator ausgewertet, die Operanden werden hingegen unausgewertet in den Funktionskörper hineinsubstitutiert, nur bei primitiven Operationen auswerten

44 Der Unterschied… eval Funktion bei applikativer Reihenfolge
(define (eval exp env) (cond [(self-evaluating? exp) exp] [(variable? exp) (eval-var exp env)] [(if-clause? exp) (eval-if exp env)] [(application? exp) (eval-app (eval (application-operator exp) env) (map (lambda (expr) (eval expr env)) (application-operands exp)) env)] [else (error 'eval (format "Unknown expression type: ~a" exp))]))

45 Der Unterschied… eval Funktion bei normaler Reihenfolge
(define (eval exp env) (cond [(self-evaluating? exp) exp] [(variable? exp) (eval-var exp env)] [(if-clause? exp) (eval-if exp env)] [(application? exp) (eval-app (eval (application-operator exp) env) (application-operands exp) ;; normal order env)] [else (error 'eval (format "Unknown expression type: ~a" exp))]))

46 Der Unterschied… Die eval-app Funktion bei applikativer Reihenfolge
(define (eval-app procedure arguments env) (cond [(primitive-procedure? procedure) (apply (primitive-procedure-the-proc procedure) arguments )] [(proc? procedure) ... ] [else ... ]))

47 Der Unterschied… Die eval-app Funktion bei normaler Reihenfolge
(define (eval-app procedure arguments env) (cond [(primitive-procedure? procedure) (apply (primitive-procedure-the-proc procedure) (map (lambda (expr) (eval expr env)) arguments) )] [(proc? procedure) ... ] [else ... ]))

48 Vorteile von normaler Auswertungsreihenfolge
Wir brauchen weniger Sonderformen! Macht den Interpreter einfacher Jede Sonderform erfordert eine eigene Behandlung im Interpreter Macht die Sprache einfacher Weniger Regeln/Ausnahmen Macht die Sprache mächtiger So kann fold auch mit or/and benutzt werden Beispiel: if, cond, or, … müssen keine Sonderformen mehr sein, sondern können ganz “normale” Funktionen sein

49 Vorteile von normaler Auswertungsreihenfolge
Beispiel: if als normale Funktion Dieses Beispiel würde bei applikativer Reihenfolge nicht funktionieren (define testprog '((define my-if (lambda (c t e) (if c t e))) (define ! (lambda (n) (my-if (= n 0) 1 (* n (! (- n 1)))))) (! 5))) (run-prog testprog primitive-procedures)  (list 120)

50 Streams Normale Auswertungsreihenfolge ermöglicht zudem eine elegante Programmiertechnik: Stream-basierte Programme Modellieren von zeitbehafteten Objekten ohne Zuweisungen Idee: Beschreibe zeitabhängiges Verhalten eines Objektes als (unendliche) Sequenz x1, x2,… Man kann sich diese Sequenz als Repräsentation einer Funktion x(t) vorstellen Wir werden Streams als (unendliche) Listen darstellen. Funktionen arbeiten auf unendlich großen Strömen von Daten

51 Stream-basierte Programmierung
Zunächst “bauen” wir uns Listenkonstruktoren und Selektoren In einer echten Programmiersprache würde der Interpreter diese Konstrukte natürlich bereits (effizient) zur Verfügung stellen (define testprog '((define my-cons (lambda (x y) (lambda (n) (if (= n 0) x y)))) (define my-first (lambda (p) (p 0))) (define my-rest (lambda (p) (p 1)))))

52 Stream-basierte Programmierung
Beispiel: Wir möchten die Wurzel einer Zahl berechnen Näherungsverfahren für Berechnung von (sqrt n): Beliebige erste Schätzung s0 Nächster Schätzwert In Scheme: (define next (lambda (n) (lambda (x) (/ (+ x (/ n x)) 2)))) Mit Hilfe von Streams können wir dieses Näherungsverfahren implementieren, ohne uns über Abbruchbedingungen Gedanken machen zu müssen: Der Ausdruck (repeat (next n) a) erzeugt die (unendliche) Liste der immer besser werdenden Approximationen von (sqrt n) mit Startschätzung a: a, (f a), (f (f a)), (f (f (f a))), … wobei f = (next n) (define repeat (lambda (f a) (my-cons a (repeat f (f a))))) (define next (lambda (n) (lambda (x) (/ (+ x (/ n x)) 2))))

53 Stream-basierte Programmierung
Unabhängig von der Erzeugung dieses Stroms von Approximationen können wir nun verschiedene Abbruchbedingungen definieren within: Gib Listenelement zurück, sobald zwei aufeinander folgende Listenelemente sich weniger als eps unterscheiden relative-within: Schranke eps wird mit derzeitigem Wert multipliziert (besser bei kleinen Werten) (define within (lambda (eps l) (if (< (abs (- (my-first l) (my-first (my-rest l)))) eps) (my-first l) (within eps (my-rest l))))) (define relative-within (* eps (abs (my-first l)))) (relative-within eps (my-rest l)))))

54 Stream-basierte Programmierung
Zusammenbau der Funktionen und Tests Bei kleinen Werten ist sqrt-rel genauer (define sqrt (lambda (a eps n) (within eps (repeat (next n) a)))) (define sqrt-rel (lambda (a eps n) (relative-within eps (repeat (next n) a)))) (sqrt )  (sqrt-rel )  (sqrt )  (sqrt-rel ) 

55 Stream-basierte Programmierung
Bei normaler Auswertungsreihenfolge funktionieren Funktionen wie fold, map, filter automatisch auch auf Streams Mit derselben Implementierung Beispiel: (my-first (my-rest (filter prime? (enumerate-interval ))) Benötigt viel Zeit und Platz bei appl. Reihenfolge, bei norm. Reihenfolge wird erst bei Bedarf das nächste Listenelemt berechnet Auch möglich: wobei allnats die Liste aller natürlichen Zahlen ist (my-first (my-rest (filter prime? allnats))

56 Stream-basierte Programmierung
Weiteres Beispiel: Alle geraden Fibonacci Zahlen Um dies in unserem Interpreter laufen zu lassen, müssen die entsprechenden Hilfsfunktionen wie filter, even? natürlich erst implementiert werden Der Interpreter selbst muss dazu allerdings nicht verändert werden! (define (f a b) (my-cons a (f b (+ a b)))) (define fibs (f 1 1)) (define evenfibs (filter even? fibs))

57 Zusammenfassung Alle Funktionsteile sind wiederverwendbar und kombinierbar next, within, within-rel, repeat, … Ohne Streams wäre das sehr schwierig Man kann die Abbruchbedingung nicht von der Erzeugung der Werte trennen Die “pipes-and-filters” Architektur aus T5 lässt sich auch bei unendlichen Datenströmen anwenden Außerdem werden Ergebnisse dann inkrementell berechnet Die Idee unendlicher Datenströme wird in der Praxis sehr häufig eingesetzt (z.B. Unix Kommandozeile, Java Streams) Normale Auswertungsreihenfolge muss simuliert werden Streams sind eine wichtige Alternative zum Design mit Zustand und Zuweisungen Repräsentieren “Zeit” explizit und nicht implizit wie bei Zuweisungen Die Gründe, wieso normale Auswertungsreihenfolge trotzdem in vielen Sprachen nicht benutzt wird, wurden bereits diskutiert Das Hauptargument fuer applikative Reihenfolge (und nicht normale Auswertungsreihenfolge) ist i.w. Effizienz: wenn das Argument mehr als einmal benoetigt wird (was haeufig der Fall ist) muss in der normalen Auswertungsreihenfolge das Argument mehrfach auswertet werden…


Herunterladen ppt "Grundlagen der Informatik 1 Thema 9: Interpreter nach dem"

Ähnliche Präsentationen


Google-Anzeigen