Kapitel 5: Applikative Programmierung



Ähnliche Dokumente
Programmierkurs Java

Diana Lange. Generative Gestaltung Operatoren

Java Kurs für Anfänger Einheit 5 Methoden

50. Mathematik-Olympiade 2. Stufe (Regionalrunde) Klasse Lösung 10 Punkte

Grundbegriffe der Informatik

Informationsblatt Induktionsbeweis

Der Aufruf von DM_in_Euro 1.40 sollte die Ausgabe 1.40 DM = Euro ergeben.

Grundlagen der höheren Mathematik Einige Hinweise zum Lösen von Gleichungen

7 Rechnen mit Polynomen

Lernziele: Ausgleichstechniken für binäre Bäume verstehen und einsetzen können.

Grundbegriffe der Informatik

Erwin Grüner

Abituraufgabe zur Stochastik, Hessen 2009, Grundkurs (TR)

Analysis I für Studierende der Ingenieurwissenschaften

Primzahlen und RSA-Verschlüsselung

Theoretische Grundlagen des Software Engineering

Theoretische Informatik 2 (WS 2006/07) Automatentheorie und Formale Sprachen 19

Professionelle Seminare im Bereich MS-Office

Programmiersprachen und Übersetzer

Würfelt man dabei je genau 10 - mal eine 1, 2, 3, 4, 5 und 6, so beträgt die Anzahl. der verschiedenen Reihenfolgen, in denen man dies tun kann, 60!.

Lineare Gleichungssysteme

Verhindert, dass eine Methode überschrieben wird. public final int holekontostand() {...} public final class Girokonto extends Konto {...

Motivation. Formale Grundlagen der Informatik 1 Kapitel 5 Kontextfreie Sprachen. Informales Beispiel. Informales Beispiel.

Einführung in die Programmierung

1 Mathematische Grundlagen

Vorkurs Mathematik Übungen zu Polynomgleichungen

Formale Sprachen. Der Unterschied zwischen Grammatiken und Sprachen. Rudolf Freund, Marian Kogler

Lineargleichungssysteme: Additions-/ Subtraktionsverfahren

Grammatiken. Einführung

Zeichen bei Zahlen entschlüsseln

Kapitel 2: Formale Sprachen Kontextfreie Sprachen. reguläre Grammatiken/Sprachen. kontextfreie Grammatiken/Sprachen

4. AUSSAGENLOGIK: SYNTAX. Der Unterschied zwischen Objektsprache und Metasprache lässt sich folgendermaßen charakterisieren:

1. Man schreibe die folgenden Aussagen jeweils in einen normalen Satz um. Zum Beispiel kann man die Aussage:

Formale Sprachen und Grammatiken

Einfache Ausdrücke Datentypen Rekursive funktionale Sprache Franz Wotawa Institut für Softwaretechnologie

Wurzeln als Potenzen mit gebrochenen Exponenten. Vorkurs, Mathematik

Einführung in die Programmierung

Binäre Bäume. 1. Allgemeines. 2. Funktionsweise. 2.1 Eintragen

Einführung in die Programmierung

Rekursionen (Teschl/Teschl )

Bevor lineare Gleichungen gelöst werden, ein paar wichtige Begriffe, die im Zusammenhang von linearen Gleichungen oft auftauchen.

Approximation durch Taylorpolynome

Also kann nur A ist roter Südler und B ist grüner Nordler gelten.

Theorie der Informatik

Lineare Gleichungssysteme

Fachschaft Mathematik und Informatik (FIM) LA I VORKURS. Herbstsemester gehalten von Harald Baum

Semantik von Formeln und Sequenzen

Grundlagen der Theoretischen Informatik, SoSe 2008

Im Jahr t = 0 hat eine Stadt Einwohner. Nach 15 Jahren hat sich die Einwohnerzahl verdoppelt. z(t) = at + b

Java: Vererbung. Teil 3: super()

t r Lineare Codierung von Binärbbäumen (Wörter über dem Alphabet {, }) Beispiel code( ) = code(, t l, t r ) = code(t l ) code(t r )

Die reellen Lösungen der kubischen Gleichung

Mathematik: Mag. Schmid Wolfgang Arbeitsblatt 3 1. Semester ARBEITSBLATT 3 RECHNEN MIT GANZEN ZAHLEN

Scala kann auch faul sein

SEP 114. Design by Contract

1 topologisches Sortieren

Die Gleichung A x = a hat für A 0 die eindeutig bestimmte Lösung. Für A=0 und a 0 existiert keine Lösung.

Terme stehen für Namen von Objekten des Diskursbereichs (Subjekte, Objekte des natürlichsprachlichen Satzes)

Objektorientierte Programmierung

Softwarelösungen: Versuch 4

Repetitionsaufgaben Wurzelgleichungen

Theoretische Grundlagen der Informatik

Mathematischer Vorbereitungskurs für Ökonomen

Dokumentation für das Spiel Pong

1. LPC - Lehmanns Programmier Contest - Lehmanns Logo

Basis und Dimension. Als nächstes wollen wir die wichtigen Begriffe Erzeugendensystem und Basis eines Vektorraums definieren.

!(0) + o 1("). Es ist damit möglich, dass mehrere Familien geschlossener Orbits gleichzeitig abzweigen.

WS 2008/09. Diskrete Strukturen

Theoretische Informatik I

Systeme 1. Kapitel 6. Nebenläufigkeit und wechselseitiger Ausschluss

Whitebox-Tests: Allgemeines

1 Aussagenlogik und Mengenlehre

Elemente der Analysis I Kapitel 2: Einführung II, Gleichungen

Satz. Für jede Herbrand-Struktur A für F und alle t D(F ) gilt offensichtlich

Objektorientierte Programmierung. Kapitel 12: Interfaces

4. Versicherungsangebot

Entladen und Aufladen eines Kondensators über einen ohmschen Widerstand

4. Jeder Knoten hat höchstens zwei Kinder, ein linkes und ein rechtes.

Einführung in die Java- Programmierung

Umgekehrte Kurvendiskussion

Formale Systeme, WS 2012/2013 Lösungen zu Übungsblatt 4

Grundlagen der Programmierung (Vorlesung 14)

Überblick. Lineares Suchen

Vgl. Oestereich Kap 2.7 Seiten

Der Zwei-Quadrate-Satz von Fermat


Gleichungen Lösen. Ein graphischer Blick auf Gleichungen

a n auf Konvergenz. Berechnen der ersten paar Folgenglieder liefert:

Das Typsystem von Scala. L. Piepmeyer: Funktionale Programmierung - Das Typsystem von Scala

8: Zufallsorakel. Wir suchen: Einfache mathematische Abstraktion für Hashfunktionen

1. Formale Sprachen 1.2 Grammatiken formaler Sprachen

Objektorientierte Programmierung. Kapitel 3: Syntaxdiagramme und Grammatikregeln

2.11 Kontextfreie Grammatiken und Parsebäume

Computeranwendung und Programmierung (CuP)

Kapiteltests zum Leitprogramm Binäre Suchbäume

Charakteristikenmethode im Beispiel

Kontrollstrukturen - Universität Köln

Übungen für Woche 10

Was meinen die Leute eigentlich mit: Grexit?

Physik & Musik. Stimmgabeln. 1 Auftrag

Transkript:

Kapitel 5: Applikative Programmierung In der applikativen Programmierung wird ein Programm als eine mathematische Funktion von Eingabe-in Ausgabewerte betrachtet. Das Ausführen eines Programms besteht dann in der Berechnung des Funktionswertes zu einem gegebenen Eingabewert. Historisch erwachsen ist dieses Paradigma aus dem Church schen λ Kalkü (lambda-kalkül)l. 5.1 λ-kalkül Gegeben sei ein Alphabet A={x 1,,x n }. Ein λ Term ist definiert durch folgende Grammatik: λ Term ::= Variable (λ Term λ Term) λ Variable. λ Term Variable ::= x 1 x n λ Terme der Art (t 1 t 2 ) heißen Applikation, λ Terme der Art λx.t heißen λ-abstraktion. In der λ-abstraktion (λx.t) ist die Variable x innerhalb von t gebunden. Intuitiv wird der λ Term (t 1 t 2 ) gelesen als die Funktion t 1 angewendet auf Argument t 2 und der λ Term λx.t als diejenige Funktion, die x als Argument hat und t als Ergebnis liefert. Die Auswertung von λ Termen ist definiert durch die Regel der β-konversion: (λ x. t 1 ) t 2 = t 1 [x:=t 2 ] Hierbei bezeichnet t 1 [x:=t 2 ] den Term t 1, wobei jedes freie (d.h. nicht durch ein λ gebundene) Vorkommnis von x durch t 2 ersetzt wird. Klammern werden, soweit eindeutig, weggelassen, wobei Linksassoziativität unterstellt wird: (x y z) = ((x y) z) Eine weitere Regel ist die der α-konversion, d.h. die Umbenennung von gebundenen Variablen: λ x. t = λ y. t[x:=y] Beispiel für eine Reduktion ist etwa die folgende: ((λx. x x) ((λz. z y) x)) ((λz. z y) x) ((λz. z y) x) ((λz. z y) x) (x y) (x y) (x y) Eine andere Ableitung für denselben Term wäre z.b.: ((λx. x x) ((λz. z y) x)) (λx. x x) (x y) (x y) (x y) Alonzo Church zeigte, dass man mit dem λ-kalkül und einem kleinen Trick alle arithmetischen Funktionen berechnen kann: Sei 0 λf.λx. x 1 λf.λx. f x 2 λf.λx. f (f x) 3 λf.λx. f (f (f x))... n λf.λx. f n x Dann lassen sich die arithmetischen Funktionen wie folgt definieren: succ λn.λf.λx.n f (f x) 5-79

plus λm.λn.λf.λx. m f (n f x) mult λm.λn.λf. n (m f) Als Beispiel zeigen wir, dass 1+1=2: (λm.λn.λf.λx. m f (n f x)) (λf.λx. f x)(λf.λx. f x) = (λf.λx. (λf.λx. f x) f ((λf.λx. f x) f x)) = (λf.λx. (λx. f x) ((λx. f x) x)) = (λf.λx. (λx. f x) (f x)) = (λf.λx. f (f x)) Auch die Kodierung boolescher Ausdrücke ist möglich. Anforderung an True, False und If: (If True M N = M) und (If False M N = N) Definitionen: True = (λx.λy.x), False = (λx.λy.y), If = (λi.λt.λe.ite) Beweis: If True M N = (λi.λt.λe.ite) (λx.λy.x) M N = (λt.λe.(λx.λy.x) te) M N = (λe.(λx.λy.x) M e) N = (λx.λy.x) M N = (λy.m) N = M If False M N = (λiλtλe.ite)(λx.λy.y) M N = (λx.λy.y) M N = (λy.y) N = N Auf diese Weise lässt sich jedes Programm einer beliebigen Programmiersprache auch im λ- Kalkül ausdrücken (sogenannte Church sche These oder Church-Turing-These). Anmerkung: Natürlich lässt sich der λ-kalkül direkt in Groovy ausdrücken. Beispiel: nul = { f -> { x -> x}} one = { f -> { x -> f (x) }} two = { f -> { x -> f (f (x))}} plus = { m -> { n -> { f -> { x -> (m (f)) ((n(f)) (x))}}}} Leider schlägt aber der folgende Vergleich fehl: assert ((plus (one)) (one)) == two Das liegt daran, dass keine Programmiersprache die Gleichheit von Funktionen feststellen kann! Abhilfe kann dadurch geschaffen werden, dass wir für f etwa die Funktion "I"+x und für x die leere Zeichenreihe einsetzen: f = {x -> "I"+x} assert ((plus (one)) (one)) (f) ("") == two (f) ("") (plus (one) (one)) (plus (one) (one)) (f) ("") 5.2 Rekursion, Aufruf, Terminierung Eine Funktion oder Methode (auch: Funktionsprozedur) besteht aus einem Funktionskopf (auch Signatur genannt) und einem Funktionsrumpf. Der Kopf besteht im Wesentlichen aus dem Namen, den Parametern (oder Argumenten) und dem Ergebnistyp der Funktion. In Java 5-80

kann optional noch eine Anzahl von Modifikatoren (public, private, protected, static, final) davor stehen. Die folgenden Syntaxdiagramme sind aus http://www.infosun.fim.unipassau.de/cl/passau/kuwi05/syntaxdiagrams.pdf In Groovy gibt es die Möglichkeit, durch Angabe von def den Ergebnistyp dynamisch bestimmen zu lassen. Der Rumpf der Funktion ist (in Java) ein Anweisungsblock bzw. (in Groovy) eine Closure. Ein Anweisungsblock ist ein in { } durch Semikolon getrennte Liste von Anweisungen. In Groovy kann ein solcher Anweisungsblock auch einer Variablen zugewiesen werden, so dass er später aufgerufen werden kann (closed block oder closure). Ein solcher Anweisungsblock darf auch genau wie im λ Kalkül formale Parameter enthalten. def f(x) {3*(x**2) + 2*x + 5} assert f(5) == 90 vollkommen gleichwertig dazu sind folgende Groovy-Anweisungen: def g = {x -> 3*x**2+2*x+5} assert g(5) == 90 Das bedeutet, der Term λ x. t wird in Groovy notiert als {x -> t}. Natürlich kann eine Funktion mehrere Parameter unterschiedlichen Typs enthalten: Rekursion def gerade (int a,b, float x) {return a*x+b} def parabel = { int a,b,c, float x -> a*x**2+b*x+c} Der Wert einer Funktion ist das Ergebnis der letzen ausgeführten Anweisung. Falls innerhalb des Blocks eine Anweisung einen Ausdruck enthält, der den Namen der Funktion selbst enthält, sagt man, die Funktion ist rekursiv. Bei einer einfachen Rekursion gibt es nur einen einzigen rekursiven Aufruf (Beispiel: Fakultätsfunktion); falls dieser jeweils die letzte Aktion bei der Ausführung ist, sprechen wir von einer Endrekursion (Tail-end-Rekursion). Bei einer Kaskadenrekursion gibt es mehrere rekursive Aufrufe auf der gleichen Schachtelungstiefe 5-81

(Beispiel: Fibonacci). In einer geschachtelten Rekursion kommen rekursive Aufrufe innerhalb von rekursiven Aufrufen vor; Beispiel ist die McCarthy sche 91-er Funktion: 5-82 f = {n -> (n>100)? (n-10) : f(f(n+11))} g = {n -> (n>100)? (n-10) : 91} assert (1..200).each {f(it) == g(it)} Intuitiv lässt sich folgende Analogie herstellen: Endrekursion reguläre (Chomsky-3) Grammatik einfache Rekursion kontextfreie (Chomsky-2) Grammatik Kaskadenrekursion kontextsensitive (Chomsky-1) Grammatik geschachtelte Rekursion allgemeine (Chomsky-0) Grammatik Diese Analogie gilt allerdings nur bedingt, weil sich bei Verfügbarkeit von Zuweisungen jede Funktion als Endrekursion darstellen lässt. Da sich Endrekursionen auf von-neumann- Rechnern besonders effizient ausführen lassen (mit nur einer Schleife), war das Thema der Entrekursivierung lange Zeit von Bedeutung. Die so genannte Ackermann-Péter-Funktion ist ein Beispiel für eine arithmetische Funktion, die sich nicht mit einfachen Mitteln entrekursivieren lässt: ack(0,m) = m+1 ack(n+1,0) = ack(n,1) ack(n+1,m+1) = ack(n, ack(n+1,m)) oder in Java/Groovy: def ack(n,m) {(n==0)? m+1 : (m==0)? ack (n-1,1) : ack(n-1, ack(n,m-1))} Hier sind die Werte der Funktion für verschiedene Eingabeparameter n=0 n=1 n=2 n=3 m=0 1 2 3 5 m=1 2 3 5 13 m=2 3 4 7 29 m=3 4 5 9 61 m=4 5 6 11 125 m=5 6 7 13 253 m=6 7 8 15 509 m=7 8 9 17 1021 m=8 9 10 19 2045 m=9 10 11 21 4093 Es ist eine interessante Übung, die Werte für n 4 zu berechnen. Aufrufmechanismen Falls innerhalb eines λ-terms ein Teilterm ( ((λ x. t 1 ) t 2 ) ) vorkommt, so kann auf diesen die β-konversion angewendet werden. Im Falle mehrerer solcher Teilterme erlaubt es der λ- Kalkül, diese Regel an beliebiger Stelle anzuwenden. In einer Programmiersprache muss hierfür jedoch eine Reihenfolge festgelegt werden. Beispiel: f= {x,y -> (x==y)? x : f(f(x,y),f(x,y))} Wie erfolgt die Berechnung von f(0,1)?

Unter der leftmost-innermost -oder normalen Auswertungsregel versteht man die Regel, dass jeweils der am weitesten links stehende Ausdruck, der keinen weiteren rekursiven Aufruf mehr enthält, expandiert wird. Beispiel: f(0,1) = (0==1)? 0: f(f(0,1), f(0,1)) = f(f(0,1), f(0,1)) = f((0==1)? 0: f(f(0,1), f(0,1)), f(0,1)) = f(f(f(0,1), f(0,1)), f(0,1)) = f(f(f(f(0,1), f(0,1)), f(0,1)), f(0,1)) = Bei der leftmost-outermost oder verzögerten Ausführung ( lazy evaluation ) wird jeweils der äußerste linke Aufruf expandiert: Beispiel: f(0,1) = (0==1)? 0: f(f(0,1), f(0,1)) = f(f(0,1), f(0,1)) = (f(0,1) == f(0,1))? f(0,1) : f(f(f(0,1),f(0,1)),f(f(0,1),f(0,1))) = f(0,1) = Bei der full substitution -Regel werden alle Aufrufe gleichzeitig expandiert. Für g= {x,y -> (x==y)? 0 : g(g(x,y),g(x,y))} terminiert die verzögerte, nicht jedoch die normale Auswertung. Java/Groovy verwenden wie die meisten anderen Programmiersprachen die normale Auswertungsregel: fun = {x,y -> println ("Aufruf mit x: "+x+", y: "+y); (y==0)? y : fun (1, y-1) + fun (2, y-1) } fun(0,2) ergibt Aufruf mit x: 0, y: 2 Aufruf mit x: 1, y: 1 Aufruf mit x: 1, y: 0 Aufruf mit x: 2, y: 0 Aufruf mit x: 2, y: 1 Aufruf mit x: 1, y: 0 Aufruf mit x: 2, y: 0 Result: 0 Das Church-Rosser-Theorem im λ Kalkül besagt, dass die Relation, mit t 1 t 2 g.d.w. t 2 durch β-reduktion aus t 1 entstanden ist, die Rauteneigenschaft ( diamond property ) besitzt: Für alle Terme x, y z gilt: falls x y und x z, so existiert ein Term w mit y w und z w. Daraus folgt unmittelbar, dass die Rauteneigenschaft auch für Ableitungsfolgen gilt: Falls x * y und x * z, so existiert w mit y * w und z * w. Daraus lässt sich folgern, dass wenn ein Term nicht mehr weiter reduziert werden kann, das Ergebnis eindeutig sein muss: Falls x * y und x * z und y ist irreduzibel, so gilt z * y Also ist für terminierende Berechnungen die Frage der Reihenfolge letztlich nur für die Effizienz der Berechnung, nicht aber für das richtige Ergebnis wichtig. 5-83

Terminierungsbeweise Um für eine rekursive Funktion zu beweisen, dass sie terminiert, müssen wir eine Abbildung der Parameter in die natürlichen Zahlen (oder, allgemeiner, in eine fundierte Ordnung) finden, so dass für jeden Aufruf die aufgerufenen Parameterwerte echt kleiner sind als die aufrufenden. Beispiel: fun = { x -> (x>=10)? x**2 : x * fun(x+1) } Um zu zeigen, dass die Funktion fun terminiert, betrachten wir die Abbildung f: Z N0, f(x) = max(10 x, 0). Dann ist für x 10 auch f(x+1) = 10 (x+1) = 9 x < 10 x = f(x). Also können wir folgern, dass fun(x) für alle x aus Z terminiert. Formal ist das ein Schluss nach dem Schema der transfiniten Induktion, siehe Kap. 1.2: Aussage: Für alle i und alle x gilt: Wenn f(x) = i, so terminiert fun(x) (a) Induktionsanfang: Wenn f(x) = 0, so terminiert fun(x) Beweis: Wenn f(x)=0, so ist x 10, in diesem Fall terminiert fun Induktionsvoraussetzung: Für alle x mit f(x) < i gilt dass fun(x) terminiert, zeige: Wenn f(x)=i, so terminiert fun(x). Beweis: fun(x) ruft fun(x+1); oben gezeigt: f(x+1)<f(x), also f(x+1)<i, also terminiert fun(x+1), also auch fun(x). 5-84