Programmierung - TCS RWTH - RWTH

Werbung
Programmierung
Programmierung
Bachelor Informatik
Peter Rossmanith
Wintersemester 2013/14
Programmierung
Inhalt
1
Einführung in die objektorientierte Programmierung
2
4
Rekursion
Fehler finden und vermeiden
Objektorientiertes Design
5
Effiziente Programme
6
Nebenläufigkeit
7
Laufzeitfehler
Refactoring
3
8
10
Persistenz
Eingebettete Systeme
11
Funktionale Programmierung
12
Logische Programmierung
9
Programmierung
Programmierung
Organisatorisches:
http://programmierung.informatik.rwth-aachen.de
Algorithmus
≈ Eindeutige, genaue Anleitung für die Lösung eines Problems.
Programm
≈ Eindeutige, genaue Anleitung für die Lösung eines Problems in
einer Programmiersprache formuliert.
Programmierung
Programme und Algorithmen
Es gibt sehr alte Algorithmen:
Euklidischer Algorithmus
→ größter gemeinsamer Teiler
Sieb des Erathostenes
→ Berechnung von Primzahlen
Babylonisches Wurzelziehen
→ Näherung der Quadratwurzel
Programmierung
Literatur zur Vorlesung
Handapparat der Informatik-Bibliothek.
Programmierung
Literatur zur Vorlesung
Ausschnitt zur Programmierung.
Programmierung
Tabelliermaschinen (1890)
Bild von Arnold Reinhold
Neues Kapitel
1
Einführung in die objektorientierte Programmierung
2
Rekursion
3
Fehler finden und vermeiden
4
Objektorientiertes Design
5
Effiziente Programme
6
Nebenläufigkeit
7
Laufzeitfehler
8
Refactoring
9
Persistenz
10
Eingebettete Systeme
11
Funktionale Programmierung
12
Logische Programmierung
Überblick
1
Einführung in die objektorientierte Programmierung
Klassen und Objekte
Vererbung, Setter, Getter
Primitive Datentypen
Kontrollstrukturen
Arrays
Programmierung
Einführung in die objektorientierte Programmierung
Klassen und Objekte
Erstes Beispiel einer Klasse
class Person {
String nachname;
String vorname;
}
Vorname und Nachname können gelesen und geschrieben werden.
Person fahrer = new Person();
fahrer .vorname = "Karl";
fahrer .nachname = "Ranseier";
String x = fahrer .nachname;
Programmierung
Einführung in die objektorientierte Programmierung
Klassen und Objekte
Private Attribute
class Person {
private String nachname;
private String vorname;
String getNachname() {
return nachname;
}
String getVorname() {
return vorname;
}
void setNachname(String name) {
nachname = name;
}
void setVorname(String name) {
vorname = name;
}
}
Programmierung
Einführung in die objektorientierte Programmierung
Klassen und Objekte
Private Attribute
Auf vorname und nachname kann nicht direkt zugegriffen werden.
Dies ist nur über dafür vorgesehene Methoden möglich.
Person fahrer = new Person();
fahrer .setVorname("Karl");
fahrer .setNachname("Ranseier");
String x = fahrer .getNachname();
Programmierung
Einführung in die objektorientierte Programmierung
Klassen und Objekte
Konstruktoren
class Person {
private String nachname;
private String vorname;
public Person(String vorname, String nachname) {
this.nachname = nachname;
this.vorname = vorname;
}
String getNachname() {
return nachname;
}
...
Programmierung
Einführung in die objektorientierte Programmierung
Klassen und Objekte
Konstruktoren
Nicht mehr möglich:
Person fahrer = new Person();
Wir müssen unseren Konstruktor verwenden:
Person fahrer = new Person("Karl", "Ranseier");
Überblick
1
Einführung in die objektorientierte Programmierung
Klassen und Objekte
Vererbung, Setter, Getter
Primitive Datentypen
Kontrollstrukturen
Arrays
Programmierung
Einführung in die objektorientierte Programmierung
Vererbung, Setter, Getter
Vererbung
Eine Oberklasse kann Unterklassen haben:
class Person {
String nachname;
String vorname;
...
}
Unterklasse:
class Student extends Person {
String matrikelnummer ;
public Student(String nachname, String vorname, String nummer ) {
this.nachname = nachname;
this.vorname = vorname;
matrikelnummer = nummer ;
}
}
Programmierung
Einführung in die objektorientierte Programmierung
Vererbung, Setter, Getter
Vererbung
Student karl = new Student("Karl", "Ranseier", "123456");
String x = karl.matrikelnummer ;
Das geht aber nicht:
Person karl = new Student("Karl", "Ranseier", "123456");
String x = karl.matrikelnummer ;
Stattdessen:
Person karl = new Student("Karl", "Ranseier", "123456");
String x = ((Student)karl).matrikelnummer ;
Dies nennen wir einen cast.
Programmierung
Einführung in die objektorientierte Programmierung
Vererbung, Setter, Getter
Vererbung
Eine Unterklasse kann Methoden überschreiben.
class Person {
...
String toString () {
return vorname + " " + nachname;
}
}
class Student extends Person {
...
String toString {
return vorname + " " + nachname + " " + matrikelnummer ;
}
}
Programmierung
Einführung in die objektorientierte Programmierung
Vererbung, Setter, Getter
Virtuelle Methoden
Person joe = new Person("Joe", "Doe");
Person karl = new Student("Karl", "Ranseier", "123456");
Student sue = new Student("Sue", "Winter", "654321");
String x = joe.toString ();
String y = karl.toString ();
String z = sue.toString ();
Was enthalten x, y , z?
In Java sind alle Methoden virtuell:
Die aufgerufene Methode richtet sich nicht nach dem Typ der
Variablen, sondern des Objekts selbst.
Programmierung
Einführung in die objektorientierte Programmierung
Vererbung, Setter, Getter
Variablen
Eine Klasse kann Instanzvariablen haben. Jede Variable muß einen
Typ haben.
z. B.
private String name;
private int anzahl;
private float geschwindigkeit;
Jede Variable wird im Rumpf der Klassendefinition deklariert.
Programmierung
Einführung in die objektorientierte Programmierung
Vererbung, Setter, Getter
Datentypen
Ein Typ einer Variable ist
eine selbstdefinierte Klasse oder
eine Klasse, die von anderen definiert wurde, insbesondere aus
einer Softwarebibliothek oder
ein primitiver Typ:
boolean, int, short, byte, char, float, double.
Überblick
1
Einführung in die objektorientierte Programmierung
Klassen und Objekte
Vererbung, Setter, Getter
Primitive Datentypen
Kontrollstrukturen
Arrays
Programmierung
Einführung in die objektorientierte Programmierung
Primitive Datentypen
Primitive Datentypen
long: ganze Zahl n mit −263 ≤ n ≤ 263 − 1
Zweierkomplement-Darstellung mit 32 bit
int: ganze Zahl n mit −231 ≤ n ≤ 231 − 1
Zweierkomplement-Darstellung mit 32 bit
short: ganze Zahl n mit −32768 ≤ n ≤ 32767
Zweierkomplementdarstellung mit 16 bit
byte: ganze Zahl n mit −128 ≤ n ≤ 127
Zweierkomplementdarstellung mit 8 bit
char: Ein Zeichen in 16 bit Unicode-Kodierung.
Programmierung
Einführung in die objektorientierte Programmierung
Primitive Datentypen
Fließkommazahlen
float: Fließkommazahl nach IEEE 754–Standard mit 32 bit
Vorzeichen Exponent 8 bit Mantisse 23 bit
double: Fließkommazahl nach IEEE 754–Standard mit 64 bit
Vorzeichen
Exponent 11 bit
Mantisse 52 bit
Die Mantisse ist eine binär kodierte natürliche Zahl, aber die
führende Eins wird nicht gespeichert.
Der Exponent wird als binär kodierte natürliche Zahl gespeichert
und daher um 127 bzw. 1023 erhöht. Vorteil: Fließkommazahlen
können wir normale Binärzahlen verglichen werden.
Programmierung
Einführung in die objektorientierte Programmierung
Primitive Datentypen
Zuweisungen
Einer Variable können neue Werte zugewiesen werden:
int x;
..
.
x
x
x
x
= 5;
= y;
= y + z;
= (42 ∗ y + z) − y ∗ y + 2 ∗ x;
Programmierung
Einführung in die objektorientierte Programmierung
Primitive Datentypen
Arithmetische Ausdrücke
In absteigender Priorität:
∗, / Multiplikation, Division
+, − Addition, Subtraktion
x + y ∗ z bedeutet x + (y ∗ z)
x − y − z bedeutet (x − y ) − z
Programmierung
Einführung in die objektorientierte Programmierung
Primitive Datentypen
Beispiel
Kugeloberfläche und -inhalt
class Kugel {
private double radius;
public Kugel(double r ) {
radius = r ;
}
public double getSurface() {
return 4.0 ∗ Math.PI ∗ radius ∗ radius;
}
public double getVolume() {
return 4.0/3.0 ∗ Math.PI ∗ radius ∗ radius ∗ radius;
}
}
Programmierung
Einführung in die objektorientierte Programmierung
Primitive Datentypen
Wahrheitswerte
Primitiver Typ: boolean
Vergleichsoperationen liefern als Ergebnis boolean.
x == y Gleichheit
x ! = y Ungleichheit
x < y Kleiner
x <= y Kleiner oder gleich
Es gibt die Konstanten true und false.
int x, y , z;
...
boolean b = (5 ∗ x <= y + z);
Programmierung
Einführung in die objektorientierte Programmierung
Primitive Datentypen
Wahrheitswerte
Wahrheitswerte können mit logischen Operationen verknüpft
werden.
!a Negation
a && b logisches Und
a || b logisches Oder
Logische Operatoren haben niedrigere Priorität als
Vergleichsoperatoren.
Beispiel
boolean b = (0 <= x && x < 10);
Überblick
1
Einführung in die objektorientierte Programmierung
Klassen und Objekte
Vererbung, Setter, Getter
Primitive Datentypen
Kontrollstrukturen
Arrays
Programmierung
Einführung in die objektorientierte Programmierung
Kontrollstrukturen
Verzweigungen
public int maximum(int x, int y ) {
int result;
if(x < y ) {
result = y ;
}
else {
result = x;
}
return result;
}
Mit einer if-Anweisung können wir eine Bedingung prüfen und
dann den then- oder else-Zweig ausführen lassen. Der else-Zweig
ist optional.
Programmierung
Einführung in die objektorientierte Programmierung
Kontrollstrukturen
Verzweigungen
public int sign(int x) {
int result;
if(x < 0) {
result = −1;
}
else if(x > 0) {
result = +1;
}
else {
result = 0;
}
return result;
}
Es darf mehrere else if-Teile geben.
Programmierung
Einführung in die objektorientierte Programmierung
Kontrollstrukturen
Schleifen
public int dreiecksZahl(int n) {
int sum = 0;
int k = 0;
while(k <= n) {
num = num + k;
k = k + 1;
}
return sum;
}
Wir berechnen hier die Summe
n
X
k=0
k=
n(n + 1)
2
Programmierung
Einführung in die objektorientierte Programmierung
Kontrollstrukturen
Schleifen
while(bedingung ) {
anweisung1 ;
anweisung2 ;
...
}
Solange bedingung erfüllt ist, wird der Rumpf der Schleife
ausgeführt. Ist bedingung anfangs nicht erfüllt, wird der Rumpf gar
nicht ausgeführt.
Programmierung
Einführung in die objektorientierte Programmierung
Kontrollstrukturen
for-Schleifen
for(anweisung1 ; bedingung ; anweisung2 ) {
rumpf ;
}
ist gleichbedeutend mit
anweisung1 ;
while(bedingung ) {
rumpf ;
anweisung2 ;
}
Programmierung
Einführung in die objektorientierte Programmierung
Kontrollstrukturen
for-Schleifen
Beispiel
public int dreiecksZahl(int n) {
int sum = 0;
for(int k = 1; k <= n; k = k + 1) {
sum = sum + k;
}
return sum;
}
Wieder berechnen wir
n 7→
n
X
k=0
k=
n(n + 1)
.
2
Programmierung
Einführung in die objektorientierte Programmierung
Kontrollstrukturen
for-Schleifen
public int tetraederZahl(int n) {
int sum = 0;
for(int k = 1; k <= n; k = k + 1) {
for(int j = 1; j <= k; j = j + 1) {
sum = sum + j;
}
}
return sum;
}
Wir berechnen
n 7→
n X
k
X
k=1 j=1
j=
n(n + 1)(n + 2)
.
6
Überblick
1
Einführung in die objektorientierte Programmierung
Klassen und Objekte
Vererbung, Setter, Getter
Primitive Datentypen
Kontrollstrukturen
Arrays
Programmierung
Einführung in die objektorientierte Programmierung
Arrays
Arrays
Wir wollen häufig viele Dinge gemeinsam speichern.
int a[ ] = new int[10]; // Array der Größe 10
a[0] = 7;
a[4] = 3;
a[9] = 10;
a[10] = 12; // nicht erlaubt!
a[3] = a[0] + a[4];
Programmierung
Einführung in die objektorientierte Programmierung
Arrays
Was ist die größte Zahl in einem Array?
int a[ ];
public int maximum() {
int n = a.length;
if(n == 0) {
return 0;
}
int max = a[0];
for(int i = 1; i < n; i ++) {
if(a[i] > max) {
max = a[i];
}
}
return max;
}
Programmierung
Einführung in die objektorientierte Programmierung
Arrays
Einfacher Sortieralgorithmus
Wir sortieren ein Array a[ ].
public void sort() {
int n = a.length; // Länge von a
for(int j = 0; j < n; j ++) {
for(int k = 0; k < n − 1; k ++) {
if(a[k] > a[k + 1]) { // Falsche Reihenfolge
// Vertausche a[k] mit a[k+1]
int temp = a[k];
a[k] = a[k + 1];
a[k + 1] = temp;
}
}
}
}
Neues Kapitel
1
Einführung in die objektorientierte Programmierung
2
Rekursion
3
Fehler finden und vermeiden
4
Objektorientiertes Design
5
Effiziente Programme
6
Nebenläufigkeit
7
Laufzeitfehler
8
Refactoring
9
Persistenz
10
Eingebettete Systeme
11
Funktionale Programmierung
12
Logische Programmierung
Überblick
2
Rekursion
Rekursive Methoden
Backtracking
Memorization
Einfache rekursive Datenstrukturen
Bäume
Aufzählen, Untermengen, Permutationen, Bitmengen
Programmierung
Rekursion
Rekursive Methoden
Fibonacci-Zahlen
F0 = 0,
F1 = 1,
Fn = Fn−1 + Fn−2 falls n > 1.
int fibo(int n) {
if(n == 0) {
return 0;
}
else if(n == 1) {
return 1;
}
else {
return fibo(n − 1) + fibo(n − 2);
}
}
Programmierung
Rekursion
Rekursive Methoden
Die Türme von Hanoi
Bewege die Scheiben von der linken Stange auf die rechte Stange.
Nur je eine Scheibe darf bewegt werden.
Eine Scheibe darf nicht auf einer kleineren liegen.
Programmierung
Rekursion
Rekursive Methoden
Die Türme von Hanoi
Bewege n Scheiben von sourcePeg nach destPeg .
Benutze thirdPeg als Ablage.
static String move(int n, int sourcePeg , int destPeg , int thirdPeg ) {
if(n == 0) {
return "";
}
String phase1 = move(n − 1, sourcePeg , thirdPeg , destPeg );
String phase2 = " " + sourcePeg + "->" + destPeg ;
String phase3 = move(n − 1, thirdPeg , destPeg , sourcePeg );
return phase1 + phase2 + phase3 ;
}
Überblick
2
Rekursion
Rekursive Methoden
Backtracking
Memorization
Einfache rekursive Datenstrukturen
Bäume
Aufzählen, Untermengen, Permutationen, Bitmengen
Programmierung
Rekursion
Backtracking
Das Acht-Damen-Problem
QZ0Z0Z0Z
Z0Z0Z0L0
0Z0L0Z0Z
Z0Z0ZQZ0
0Z0Z0Z0L
ZQZ0Z0Z0
0Z0ZQZ0Z
Z0L0Z0Z0
Programmierung
Rekursion
Backtracking
Backtracking
Wir verwenden Backtracking:
QZ0Z0Z0Z
Z0Z0Z0L0
0Z0L0Z0Z
Z0Z0ZQZ0
0Z0Z0Z0L
ZQZ0Z0Z0
0Z0ZQZ0Z
Z0L0Z0Z0
Setze Damen in Zeilen von links nach rechts.
Wenn nächstes Setzen unmöglich, nimm letzten Zug zurück und
versuche nächsten.
Programmierung
Rekursion
Backtracking
void setzeDamenAbZeile(int y ) {
if(y >= n) {
geloest = true;
return;
}
for(int x = 0; x < n; x ++) {
if(safePosition(y , x)) {
brett[y ][x] = true;
setzeDamenAbZeile(y + 1);
if(geloest) {
return;
}
brett[y ][x] = false;
}
}
}
Überblick
2
Rekursion
Rekursive Methoden
Backtracking
Memorization
Einfache rekursive Datenstrukturen
Bäume
Aufzählen, Untermengen, Permutationen, Bitmengen
Programmierung
Rekursion
Memorization
Nochmals Fibonacci-Zahlen
int fibo(int n) {
if(n == 0) {
return 0;
}
else if(n == 1) {
return 1;
}
else {
return fibo(n − 1) + fibo(n − 2);
}
}
Wie schnell ist dieses Programm?
Sehr langsam!
Programmierung
Rekursion
Memorization
Nochmals Fibonacci-Zahlen
Problem: Gleiche Werte werden mehrfach berechnet.
F5
F4
F3
F3
F2
F2
F1
F1
F2
F0
F1
F1
F0
F1 F 0
Lösung: Memorization
In Wirklichkeit heißt es Memoization“, aber immer mehr Leute verwenden
”
Memorization“. In naher Zukunft wird das also der korrekte Begriff durch
”
Mehrheitsmeinung werden. . .
Programmierung
Rekursion
Memorization
Memorization
Speichere neu berechnete Werte.
Verwende gespeicherte Werte, wenn vorhanden.
long fibo2 (int n) {
if(n == 0) {
return 0;
}
else if(n == 1) {
return 1;
}
if(f [n] == 0) {
f [n] = fibo2 (n − 1) + fibo2 (n − 2);
}
return f [n];
}
Überblick
2
Rekursion
Rekursive Methoden
Backtracking
Memorization
Einfache rekursive Datenstrukturen
Bäume
Aufzählen, Untermengen, Permutationen, Bitmengen
Programmierung
Rekursion
Einfache rekursive Datenstrukturen
Rekursive Datenstrukturen
Wir möchten eine Liste bzw. eine endliche Folge implementieren.
Operationen:
int head(): Was ist das erste Element?
Liste tail(): Die Liste ohne das erste Element.
boolean isempty (): Ist die Liste leer?
Liste add(int elem): Dieselbe Liste, aber elem davor.
Rekursiver Entwurf:
Eine Liste ist entweder
leer oder
besteht aus einem Element und einer Liste.
Programmierung
Rekursion
Einfache rekursive Datenstrukturen
class Liste {
private boolean empty ; // Liste leer?
private int value; // erstes Element
private Liste rest; // restliche Elemente
//
int head() {...}
Liste tail() {...}
boolean isempty () {...}
// Erzeuge neue Liste [elem, alte Liste]
Liste add(int elem) {...}
}
Eine Liste darf eine Liste enthalten“!
”
Programmierung
Rekursion
Einfache rekursive Datenstrukturen
Interne Repräsentation
Liste 1, 18, 5, 3:
l1
false
1
Leere Liste:
l2
true
?
?
false
18
false
5
false
3
true
?
?
Programmierung
Rekursion
Einfache rekursive Datenstrukturen
Beispiel
public static void main(String args[ ]) {
Liste l1 = new Liste();
l1 = l1 .add(5);
l1 = l1 .add(7);
l1 = l1 .add(3);
System.out.println(l1 ); // [3, 7, 5]
Liste l2 = l1 ;
l2 = l2 .add(17);
l1 = l1 .add(23);
System.out.println(l1 );
System.out.println(l2 );
}
Programmierung
Rekursion
Einfache rekursive Datenstrukturen
public static void main(String args[ ]) {
Liste l1 = new Liste();
l1 = l1 .add(5);
l1 = l1 .add(7);
l1 = l1 .add(3);
System.out.println(l1 ); // [3, 7, 5]
Liste l2 = l1 ;
l2 = l2 .add(17);
l1 = l1 .add(23);
System.out.println(l1 );
System.out.println(l2 );
}
l1
false
23
l2
false
17
false
3
false
7
false
5
true
?
?
Programmierung
Rekursion
Einfache rekursive Datenstrukturen
Reference- und Value-Semantik
Student karl = new Student("Karl", "Ranseier", "123456");
...
Student klausurTeilnehmer = karl;
...
karl.setVorname("Charles");
klausurTeilnehmer .getVorname(); // ergibt Charles
Sowohl karl als auch klausurTeilnehmer beziehen sich auf dieselbe
Person.
Bei einer Zuweisung wird keine Kopie angefertigt.
Dieses Verhalten ist natürlich für eine Klasse Student.
Programmierung
Rekursion
Einfache rekursive Datenstrukturen
Ein Algorithmus, wie er in Lehrbüchern steht:
procedure Dijkstra(s) :
Q := V − {s};
for v ∈ Q do d[v ] := ∞ od;
d[s] := 0;
while Q ! = ∅ do
choose v ∈ Q with minimal d[v ];
Q := Q − {v };
forall u adjacent to v do
d[u] := min{d[u], d[v ] + length(v , u)}
od
od
Q und V sind Mengen.
In Java z.B. V .subtract(s) statt V − {s}.
Bei Mengen erwarten wir eine Value-Semantik.
Programmierung
Rekursion
Einfache rekursive Datenstrukturen
Immutable Klassen
Es seien A, B, C Mengen.
A := {1, 2, 3}
B := A
A := A ∪ {4}
Mit Mengen sind wir gewohnt so zu arbeiten.
Wir erwarten, daß B = {1, 2, 3} und A = {1, 2, 3, 4}.
Dieses Verhalten können wir durch immutable Klassen erreichen.
Programmierung
Rekursion
Einfache rekursive Datenstrukturen
Immutable Klassen
Liste ist immutable:
Es gibt keine Methode, die ein Objekt des Typs Liste ändern
kann.
Auf die Instanzvariablen kann man nicht direkt zugreifen.
Daher verhält sich Liste genauso wie int und double.
int n = 5;
int k = n + 3; // n ist immer noch 5
Liste list = new Liste();
list = list.add(5);
list = list.add(3); // list ist [3, 5]
Liste lang = list.add(1); // list bleibt [3, 5]
Programmierung
Rekursion
Einfache rekursive Datenstrukturen
Immutable Klassen
Wollen wir eine Klasse immutable machen, dann können wir alle
Instanzvariablen final deklarieren:
class Liste {
private final boolean empty ; // ist diese Liste leer?
private final int value; // das erste Element dieser Liste
private final Liste rest; // restliche Liste ohne erstes Element
Eine final-Variable kann nur in einem Konstruktor verändert
werden.
So ist die immutable-Eigenschaft garantiert.
Überblick
2
Rekursion
Rekursive Methoden
Backtracking
Memorization
Einfache rekursive Datenstrukturen
Bäume
Aufzählen, Untermengen, Permutationen, Bitmengen
Programmierung
Rekursion
Bäume
Weitere rekursive Datenstrukturen
Ein Baum ist eine Wurzel, die mehrere Kinder haben kann, welche
selbst Bäume sind. Einen Baum ohne Kinder nennt man Blatt.
public class Tree {
private int root; // Ein Baum aus Zahlen
private ListhTreei children;
public Tree(int r ) {
root = r ;
children = new ListhTreei();
}
public ListhTreei getChildren() {return children; }
public int getRoot() {return root; }
public void addChild(Tree newChild) {
children = children.add(newChild);
}
Programmierung
Rekursion
Bäume
Was ist ListhTreei genau?
class ListhT i {
private final boolean empty ; // ist diese Liste leer?
private final T value; // das erste Element dieser Liste
private final ListhT i rest; // restliche Liste ohne erstes Element
public List() { // erzeuge eine neue leere Liste
empty = true;
value = null;
rest = null;
}
private List(T elem, ListhT i rest) {
this.empty = false;
this.value = elem;
this.rest = rest;
}
public boolean isempty () {
return empty ;
}
Programmierung
Rekursion
Bäume
Generische Klassen
ListhTreei verwendet die generische Klasse class ListhT i.
Eine generische Klasse hat einen zusätzlichen Typparameter.
Auf diese Weise können wir z.B. folgendes schreiben:
ListhPersoni freunde = new ListhPersoni();
Person karl = new Student("Karl", "Ranseier", "123456");
Person joe = new Person("Joe", "Miller");
freunde.add(karl);
freunde.add(joe);
So haben wir Liste sehr verallgemeinert!
Programmierung
Rekursion
Bäume
Beispiel
Tree t1 = new Tree(1);
Tree t2 = new Tree(2);
Tree t3 = new Tree(3);
Tree t4 = new Tree(4);
Tree t5 = new Tree(5);
Tree t6 = new Tree(6);
t1 .addChild(t2 );
t1 .addChild(t3 );
t3 .addChild(t4 );
t3 .addChild(t5 );
t3 .addChild(t6 );
Programmierung
Rekursion
Bäume
Beispiel
Tree t1 = new Tree(1);
Tree t2 = new Tree(2);
Tree t3 = new Tree(3);
Tree t4 = new Tree(4);
Tree t5 = new Tree(5);
Tree t6 = new Tree(6);
t1 .addChild(t2 );
t1 .addChild(t3 );
t3 .addChild(t4 );
t3 .addChild(t5 );
t3 .addChild(t6 );
1
3
6
5
2
4
Programmierung
Rekursion
Bäume
Beispiel Fibonacci-Bäume
static Tree fiboTree(int n) {
if(n < 2) {
return new Tree(n);
}
else {
Tree t = new Tree(n);
t.addChild(fiboTree(n − 2));
t.addChild(fiboTree(n − 1));
return t;
}
}
Programmierung
Rekursion
Bäume
8
7
6
6
5
5
4
3
4
3
3
2
3
3
10
4
2 2 1 3
2 2 1 2 110 2 11010
2 11010 10
10
4
5
4
3
3
2 2 1 2 110
2 11010 10
10
2
Programmierung
Rekursion
Bäume
Noch ein Beispiel
static Tree misteryTree(int n) {
Tree t = new Tree(n);
for(int i = 0; i < n; i ++) {
t.addChild(misteryTree(n − 1));
}
return t;
}
Welchen Baum erzeugt misteryTree(4)?
Programmierung
Rekursion
Bäume
Noch ein Beispiel
static Tree misteryTree(int n) {
Tree t = new Tree(n);
for(int i = 0; i < n; i ++) {
t.addChild(misteryTree(n − 1));
}
3
return t;
}
2 2 2 2
4
3
2
3
2
2
2
3
2
2
2
2
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Programmierung
Rekursion
Bäume
Wir berechnen die Summe aller Knoten eines Baumes.
public int totalSumRecursive() {
int sum = getRoot();
ListhTreei children = getChildren();
while(!children.isempty ()) {
sum = sum + children.head().totalSumRecursive();
children = children.tail();
}
return sum;
}
Diese Funktion ist halbwegs übersichtlich und einfach.
Später werden wir dies noch verbessern!
Programmierung
Rekursion
Bäume
Jetzt eine (schlechtere) Lösung ohne Rekursion.
public int totalSumIterative() {
int sum = 0;
ListhTreei treesToSum = new ListhTreei();
treesToSum = treesToSum.add(this);
while(!treesToSum.isempty ()) {
Tree tree = treesToSum.head();
treesToSum = treesToSum.tail();
sum = sum + tree.getRoot();
ListhTreei children = tree.getChildren();
while(!children.isempty ()) {
treesToSum = treesToSum.add(children.head());
children = children.tail();
}
}
return sum;
}
Überblick
2
Rekursion
Rekursive Methoden
Backtracking
Memorization
Einfache rekursive Datenstrukturen
Bäume
Aufzählen, Untermengen, Permutationen, Bitmengen
Programmierung
Rekursion
Aufzählen, Untermengen, Permutationen, Bitmengen
Aufzählen
Oft müssen wir verschiedene Dinge aufzählen.
Zum Beispiel die Untermengen einer Menge:
Sei M = {3, 7, 25}.
Die Untermengen von M sind
∅, {3}, {7}, {25}, {3, 7}, {3, 25}, {7, 25}, {3, 7, 25}.
Neues Kapitel
1
Einführung in die objektorientierte Programmierung
2
Rekursion
3
Fehler finden und vermeiden
4
Objektorientiertes Design
5
Effiziente Programme
6
Nebenläufigkeit
7
Laufzeitfehler
8
Refactoring
9
Persistenz
10
Eingebettete Systeme
11
Funktionale Programmierung
12
Logische Programmierung
Überblick
3
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Debugging
Testen
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Hoare-Tripel
Ein Hoare-Tripel ist hφi P hψi, wobei
φ ist die Vorbedingung,
P ist ein Programm,
ψ ist die Nachbedingung.
Falls immer, wenn die Vorbedingung vor dem Ausführen von P
wahr ist, die Nachbedingung nach dem Ausführen von P wahr ist,
dann ist das Tripel korrekt.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Hoare-Tripel
Beispiele
hx = 5i x = x ∗ x; hx = 25i
hx = 5 ∧ i ≥ 4i
while(i > 0) {x = x − x ∗ x; i = i − 1; }
hx ≤ −176820i
hx = 5i if(y > 0) {x = x + y ; } hx ≥ 4i
hx = 3i while(x > 0) {x = x + 1; } hx = 10i
Vorsicht: Nur nach dem Ausführen des Programms muß die
Nachbedingung erfüllt sein.
Obige Hoare-Tripel sind alle korrekt.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Hoare-Tripel
Ein größeres Beispiel: Fibonacci-Zahlen
hn ≥ 0i
int a = 0;
int b = 1;
int k = n;
while(k > 0) {
b = a + b;
a = b − a;
k = k − 1;
}
ha = Fn i
Wir können die Korrektheit durch Induktion beweisen.
Der Hoare-Kalkül dient zum mechanischen Herleiten von korrekten
Hoare-Tripeln.
Er besteht aus verschiedenen Schlußregeln.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
1
2
3
4
5
6
7
8
hφi {} hφi
hφ[x/t]i x = t; hφi
hφi P hψi
α→φ
hαi P hψi
hφi P hψi
ψ→β
hφi P hβi
hφi P hψi
hψi Q hβi
hφi P; Q hβi
hφ ∧ Bi P hψi
φ ∧ ¬B → ψ
hφi if(B) {P} hψi
hφ ∧ Bi P hψi
hφ ∧ ¬Bi Q hψi
hφi if(B) {P} else {Q} hψi
hφ ∧ Bi P hφi
hφi while(B) {P} hφ ∧ ¬Bi
leere Anweisung
Zuweisungsregel
Konsequenzregel 1
Konsequenzregel 2
Sequenzregel
Bedingungsregel 1
Bedingungsregel 2
Schleifenregel
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Herleitung des Fibonacci-Programms
Wir verwenden zweimal die Zuweisungsregel:
h0 = 0 ∧ 1 = 1i int a = 0; ha = 0 ∧ 1 = 1i
ha = 0 ∧ 1 = 1i int b = 1; ha = 0 ∧ b = 1i
Und zweimal Konsequenzregeln (eigentlich unnötig):
h0 = 0 ∧ 1 = 1i int a = 0; ha = 0 ∧ 1 = 1i
h0 = 0i int a = 0; ha = 0i
ha = 0 ∧ 1 = 1i int b = 1; ha = 0 ∧ b = 1i
ha = 0i int b = 1; ha = 0 ∧ b = 1i
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Und zweimal Konsequenzregeln (Wiederholung):
h0 = 0 ∧ 1 = 1i int a = 0; ha = 0 ∧ 1 = 1i
h0 = 0i int a = 0; ha = 0i
ha = 0 ∧ 1 = 1i int b = 1; ha = 0 ∧ b = 1i
ha = 0i int b = 1; ha = 0 ∧ b = 1i
Jetzt kommt die Sequenzregel:
h0 = 0i int a = 0; ha = 0i
ha = 0i int b = 1; ha = 0 ∧ b = 1i
h0 = 0i int a = 0; int b = 1; ha = 0 ∧ b = 1i
Dies ergibt das erste Zwischenergebnis T1 :
T1 ≡ h0 = 0i int a = 0; int b = 1; ha = 0 ∧ b = 1i
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Weiter mit Zuweisung und Konsequenz:
ha = 0 ∧ b = 1 ∧ n = ni int k = n; ha = 0 ∧ b = 1 ∧ k = ni
ha = 0 ∧ b = 1 ∧ n = ni int k = n; ha = 0 ∧ b = 1 ∧ k = ni
ha = 0 ∧ b = 1i int k = n; ha = 0 ∧ b = 1 ∧ k = ni
Wir verwenden T1 in der Sequenzregel:
T1
ha = 0 ∧ b = 1i int k = n; ha = 0 ∧ b = 1 ∧ k = ni
h0 = 0i int a = 0; int b = 1; int k = n; ha = 0 ∧ b = 1 ∧ k = ni
Dies sei unser zweites Zwischenergebnis T2 :
h0 = 0i int a = 0; int b = 1; int k = n; ha = 0 ∧ b = 1 ∧ k = ni
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Etwas übersichtlicher schreibt sich T2 so:
h0 = 0i
int a = 0;
int b = 1;
int k = n;
ha = 0 ∧ b = 1 ∧ k = ni
Die Vorbedingung ist leer“, sie gilt immer.
”
Die Nachbedingung garantiert, daß die Variablen die richtigen
Inhalte haben.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Wir versuchen nun, folgendes Hoare-Tripel herzuleiten:
ha = 0 ∧ b = 1 ∧ k = n ∧ n ≥ 0i
while(k > 0) {
b = a + b;
a = b − a;
k = k − 1;
}
ha = Fn i
Zusammen mit T2 und der Sequenzregel können wir dann fast das
ganze Fibonacci-Programm herleiten. T2 lautete:
h0 = 0i int a = 0; int b = 1; int k = n; ha = 0 ∧ b = 1 ∧ k = ni
Leider passen die zwei Teile nicht genau zusammen.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Statt
h0 = 0i int a = 0; int b = 1; int k = n; ha = 0 ∧ b = 1 ∧ k = ni
benötigen wir
hn ≥ 0i int a = 0; int b = 1; int k = n; ha = 0 ∧ b = 1 ∧ k = n ∧ n ≥ 0i.
Wenn so etwas passiert, kann man es oft so reparieren:
Wir gehen die ganze Herleitung von T2 noch einmal durch und
fügen n ≥ 0 sowohl bei allen Vor- als auch Nachbedingungen ein.
Hier geht das ohne Problem.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Auf diese Weise erhalten wir das Zwischenergebnis T3 :
hn ≥ 0i
int a = 0;
int b = 1;
int k = n;
ha = 0 ∧ b = 1 ∧ k = n ∧ n ≥ 0i
und wir versuchen jetzt dies herzuleiten:
ha = 0 ∧ b = 1 ∧ k = n ∧ n ≥ 0i
while(k > 0) {
b = a + b;
a = b − a;
k = k − 1;
}
ha = Fn i
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Mit einer Konsequenzregel erhalten wir:
ha = Fn−k ∧ b = Fn−k+1 ∧ k ≥ 0i ≡: φ
while(k > 0) {
b = a + b;
a = b − a;
k = k − 1;
}
ha = Fn i
ha = 0 ∧ b = 1 ∧ k = n ∧ n ≥ 0i ≡: ψ
while(k > 0) {
b = a + b;
a = b − a;
k = k − 1;
}
ha = Fn i
Denn ψ → φ. (Vorbedingung aufgeweicht“.)
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Wir müssen also noch herleiten:
ha = Fn−k ∧ b = Fn−k+1 ∧ k ≥ 0i
while(k > 0) {
b = a + b;
a = b − a;
k = k − 1;
}
ha = Fn i
Um dies zu schaffen, müssen wir die Schleifenregel verwenden:
hφ ∧ Bi P hφi
hφi while(B) {P} hφ ∧ ¬Bi
Wir wählen a = Fn−k ∧ b = Fn−k+1 ∧ k ≥ 0 als φ und k > 0 als B.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Die Schleifenregel wird so angewendet:
ha = Fn−k ∧ b = Fn−k+1 ∧ k ≥ 0 ∧ k > 0i
b = a + b;
a = b − a;
k = k − 1;
ha = Fn−k ∧ b = Fn−k+1 ∧ k ≥ 0i
ha = Fn−k ∧ b = Fn−k+1 ∧ k ≥ 0i
while(k > 0) {
b = a + b;
a = b − a;
k = k − 1;
}
ha = Fn−k ∧ b = Fn−k+1 ∧ k ≥ 0 ∧ ¬(k > 0)i
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Mit einer Abschwächung der Nachbedingung erhalten wir:
ha = Fn−k ∧ b = Fn−k+1 ∧ k ≥ 0i
while(k > 0) {
b = a + b;
a = b − a;
k = k − 1;
}
ha = Fn−k ∧ b = Fn−k+1 ∧ k ≥ 0 ∧ ¬(k > 0)i
ha = Fn−k ∧ b = Fn−k+1 ∧ k ≥ 0i
while(k > 0) {
b = a + b;
a = b − a;
k = k − 1;
}
ha = Fn i
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Was müssen jetzt noch dies herleiten:
ha = Fn−k ∧ b = Fn−k+1 ∧ k > 0i
b = a + b;
a = b − a;
k = k − 1;
ha = Fn−k ∧ b = Fn−k+1 ∧ k ≥ 0i
Mit der Zuweisungsregel erhalten wir diese Tripel:
ha = Fn−k ∧ a + b = Fn−k+2 ∧ k > 0i
b = a + b;
ha = Fn−k ∧ b = Fn−k+2 ∧ k > 0i und
hb − a = Fn−k+1 ∧ b = Fn−k+2 ∧ k > 0i
a = b − a;
ha = Fn−k+1 ∧ b = Fn−k+2 ∧ k > 0i und
ha = Fn−k+1 ∧ b = Fn−k+1 ∧ k > 0i
k = k − 1;
ha = Fn−k ∧ b = Fn−k+2 ∧ k + 1 > 0i
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Stellt man alles zusammen, haben wir dieses Tripel erzeugt:
hn ≥ 0i
int a = 0;
int b = 1;
int k = n;
while(k > 0) {
b = a + b;
a = b − a;
k = k − 1;
}
ha = Fn i
Dies war eine lange Herleitung.
In Praxis oft zu schwierig oder teuer.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Übersichtliche Zusammenfassung
(1) Zuweisungsregel
hn ≥ 0i int a = 0; hn ≥ 0 ∧ a = 0i
(2) Zuweisungsregel
hn ≥ 0 ∧ a = 0i int b = 1; hn ≥ 0 ∧ a = 0 ∧ b = 1i
(3) Sequenzregel aus (1), (2)
hn ≥ 0i int a = 0; int b = 1; hn ≥ 0 ∧ a = 0 ∧ b = 1i
(4) Zuweisungsregel
hn ≥ 0 ∧ a = 0 ∧ b = 1iint k = n; hn ≥ 0 ∧ a = 0 ∧ b = 1 ∧ k = ni
(5) Sequenzregel aus (3), (4)
hn ≥ 0i int a = 0; int b = 1; int k = n; hn ≥ 0 ∧ a = 0 ∧ b = 1 ∧ k = ni
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
(6) Zuweisungsregel
ha = Fn−k ∧ a + b = Fn−k+2 ∧ k > 0i
b = a + b;
ha = Fn−k ∧ b = Fn−k+2 ∧ k > 0i
(7) Zuweisungsregel
hb − a = Fn−k+1 ∧ b = Fn−k+2 ∧ k > 0i
a = b − a;
ha = Fn−k+1 ∧ b = Fn−k+2 ∧ k > 0i
(8) Zuweisungsregel
ha = Fn−k+1 ∧ b = Fn−k+2 ∧ k > 0i
k = k − 1;
ha = Fn−k ∧ b = Fn−k+1 ∧ k + 1 > 0i
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
(9) Sequenzregel aus (6), (7)
ha = Fn−k ∧ a + b = Fn−k+2 ∧ k > 0i
b = a + b;
a = b − a;
ha = Fn−k+1 ∧ b = Fn−k+2 ∧ k > 0i
(10) Sequenzregel aus (9), (8)
ha = Fn−k ∧ b = Fn−k+1 ∧ k ≥ 0 ∧ k > 0i
b = a + b;
a = b − a;
k = k − 1;
ha = Fn−k ∧ b = Fn−k+1 ∧ k ≥ 0i
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
(11) Schleifenregel aus (10)
ha = Fn−k ∧ b = Fn−k+1 ∧ k ≥ 0i
while(k > 0) {
b = a + b;
a = b − a;
k = k − 1;
}
ha = Fn−k ∧ b = Fn−k+1 ∧ k ≥ 0 ∧ ¬(k > 0)i
(12) Konsequenzregel aus (11)
ha = 0 ∧ b = 1 ∧ k = n ∧ n ≥ 0i
while(k > 0) {
b = a + b;
a = b − a;
k = k − 1;
}
ha = Fn i
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
(13) Sequenzregel aus (5), (12)
hn ≥ 0i
int a = 0;
int b = 1;
int k = n;
while(k > 0) {
b = a + b;
a = b − a;
k = k − 1;
}
ha = Fn i
Damit ist das gesamte Hoare-Tripel in dreizehn Schritten
hergeleitet.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Vorsicht: Der Hoare-Kalkül geht von echten ganzen Zahlen aus
und nicht von beschränkter Arithmetik.
Das folgende Tripel ist korrekt:
hk ≥ 0i
while(k >= 0) {
k = k + 1;
}
hk = 25i
Das Tripel sagt, daß nach Verlassen der while-Schleife die Variable
k den Wert 25 enthält.
Das ist tatsächlich wahr! (Denn die while-Schleife wird nie
verlassen.)
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Hilfsvariablen
hn ≥ 0i
k = n;
z = 0;
while(k > 0) {
z = z + 1;
k = k − 1;
}
hz = ni
(1) Zuweisungsregel
hn ≥ 0i k = n; hn ≥ 0 ∧ k = ni
(2) Zuweisungsregel
hn ≥ 0 ∧ k = ni z = 0; hn ≥ 0 ∧ k = n ∧ z = 0i
(3) Sequenzregel aus (1), (2)
hn ≥ 0i k = n; z = 0; hn ≥ 0 ∧ k = n ∧ z = 0i
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
(4) Zuweisungsregel
hz + k = n ∧ k > 0i z = z + 1; hz − 1 + k = n ∧ k > 0i
(5) Zuweisungsregel
hz + k − 1 = n ∧ k > 0i k = k − 1; hz + k = n ∧ k ≥ 0i
(6) Sequenzregel aus (4), (5)
hz + k = n ∧ k > 0i z = z + 1; k = k − 1; hz + k = n ∧ k ≥ 0i
(7) Schleifenregel aus (6)
hz + k = n ∧ k ≥ 0i
while(k > 0) {
z = z + 1;
k = k − 1;
}
hz + k = n ∧ k ≥ 0 ∧ ¬(k > 0)i
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
(8) Konsequenzregel aus (7)
hn ≥ 0 ∧ k = n ∧ z = 0i
while(k > 0) {
z = z + 1;
k = k − 1;
}
hz = ni
(9) Sequenzregel aus (3), (8)
hn ≥ 0i
k = n;
z = 0;
while(k > 0) {
z = z + 1;
k = k − 1;
}
hz = ni
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Wir konnten beweisen, daß am Ende z = n gilt.
Wie steht es mit diesem Programm:
z = 0;
while(n > 0) {
z = z + 1;
n = n − 1;
}
Wie beweisen wir, daß z am Ende denselben Wert hat wie n am
Anfang?
Dies scheint nicht ausdrückbar zu sein.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Hilfsvariablen
Lösung: Verwende eine Hilfsvariable.
hn0 = ni
z = 0;
while(n > 0) {
z = z + 1;
n = n − 1;
}
hz = n0 i
Die Hilfsvariable n0 kommt im Programm gar nicht vor.
Die Herleitung im Hoare-Kalkül geschieht analog zum letzten
Beispiel.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
htruei
if(x > y ) {
r = x;
}
else {
r = y;
}
hr = max(x, y )i
(1) Zuweisungsregel
hx = max(x, y )i r = x; hr = max(x, y )i
(2) Konsequenzregel aus (1)
hx > y i r = x; hr = max(x, y )i
(3) Zuweisungsregel
hy = max(x, y )i r = y ; hr = max(x, y )i
(4) Konsequenzregel aus (3)
h¬(x > y )i r = y ; hr = max(x, y )i
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Die 2. Bedingungsregel lautet:
hφ ∧ Bi P hψi
hφ ∧ ¬Bi Q hψi
hφi if(B) {P} else {Q} hψi
(5) Bedingungsregel aus (2), (4)
htruei
if(x > y ) {
r = x;
}
else {
r = y;
}
hr = max(x, y )i
Die Herleitung läßt sich gut rückwärts“ finden.
”
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Kompakte Herleitungen im Hoare-Kalkül
Die Sequenzregel
hφi P hψi
hψi Q hβi
hφi P; Q hβi
können wir kompakt geschrieben so anwenden:
hφi
P
hψi
Q
hβi
Die Herleitungen von hφi P hψi und hψi Q hβi werden einfach
aneinandergehängt“.
”
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Die Konsequenzregel
hφi P hψi
α→φ∧β →ψ
hαi P hβi
wenden wir kompakt geschrieben so an:
hαi
hφi
P
hψi
hβi
Innen“ wird hφi P hψi abgeleitet. Implikationen α → φ und
”
ψ → β werden von oben nach unter“ eingefügt.
”
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Die Schleifenregel
hφ ∧ Bi P hφi
hφi while(B) {P} hφ ∧ ¬Bi
wird kompakt so angewandt:
hφi
while(B) {
hφ ∧ Bi
P
hφi
}
hφ ∧ ¬Bi
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Beispiel
Wir leiten das folgende Hoare-Tripel durch die kompakte
Schreibweise her:
hn = n0 ∧ n ≥ 0i
k = 1;
while(n > 0) {
k = 2 ∗ k;
n = n − 1;
}
0
hk = 2n i
In der Herleitung wird keine Programmanweisung mehrfach
geschrieben.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
hn = n0 ∧ n ≥ 0i
hn = n0 ∧ 1 = 1 ∧ n ≥ 0i
k = 1;
hn = n0 ∧ k = 1 ∧ n ≥ 0i
0
hk = 2n −n ∧ n ≥ 0i
while(n > 0) {
0
hk = 2n −n ∧ n ≥ 0 ∧ n > 0i
0
h2k = 2n −n+1 ∧ n > 0i
k = 2 ∗ k;
0
hk = 2n −n+1 ∧ n > 0i
0
hk = 2n −(n−1) ∧ n − 1 ≥ 0i
n = n − 1;
0
hk = 2n −n ∧ n ≥ 0i
}
0
hk = 2n −n ∧ n ≥ 0 ∧ ¬(n > 0)i
0
hk = 2n i
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Die Bedingungsregel
hφ ∧ Bi P hψi
hφ ∧ ¬Bi Q hψi
hφi if(B) {P} else {Q} hψi
wird zuletzt kompakt so angewandt:
hφi
if(B) {
hφ ∧ Bi
P
hψi
}
else {
hφ ∧ ¬Bi
Q
hψi
}
hψi
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Terminierung von Programmen
Wir haben uns mit partieller Korrektheit beschäftigt:
Falls das Programm terminiert, dann ist das Ergebnis korrekt.
Wie zeigen wir, daß es terminiert?
Terminiert dieses Programm? Für alle n??
while(n > 1) {
if(n % 2 == 1) { // ist n ungerade?
n = 3 ∗ n + 1;
}
else {
n = n/2;
}
}
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Einfache Terminierungsbeweise
Nur while-Schleifen können nicht terminieren.
Für die totale Korrektheit eines Programms genügt es zu zeigen,
daß alle while-Schleifen terminieren.
Die Schleife while(B) {P} terminiert, falls
ht = t 0 ∧ Bi P ht < t 0 ∧ t ≥ 0i gilt.
Hier ist t eine ganzzahliger Ausdruck und t 0 eine Hilfsvariable.
Der Ausdruck t wird immer kleiner, kann aber nicht negativ
werden. Daher muß die Schleife irgendwann enden. Wir nennen t
die Variante der Schleife.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Einfache Terminierungsbeweise
Beispiel:
while(k > 0) {
k = k − 1;
}
Wir wählen t := k. Dieser Ausdruck wird immer kleiner, kann aber
nicht negativ werden.
Wir können zeigen, daß
hk = k 0 ∧ k > 0i k = k − 1; hk < k 0 ∧ k ≥ 0i gilt.
Damit terminiert diese Schleife.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Schwierigere Situationen
Für die totale Korrektheit muß (und kann) man manchmal nicht
von allen while-Schleifen isoliert die Terminierung beweisen.
Der Kontext ist wichtig.
Beispiel:
if(x > 0) {
while(y > 0) {
y = y − x;
}
}
Dieses Programm terminiert immer.
Die Variante ist einfach y .
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Contract Programming
Contract Programming ist durch die Hoare-Tripel motiviert.
Wenn wir ein Programm P schreiben, machen wir einen Vertrag“:
”
Wir verlangen im Vertrag, daß unser Programm nur bestimmte
Eingaben erhält (Vorbedingungen).
Wir garantieren im Vertrag, daß die Ausgabe unseres Programms
bestimmte Eigenschaften hat (Nachbedingungen).
Wir versprechen also, daß ein Hoare-Tripel hφi P hψi gültig ist.
Geht etwas schief, dann ist unser Programm schuldig“, falls φ
”
gilt, aber ψ nicht.
Die Vor- und Nachbedingungen lassen sich unter Umständen im
Programm leicht testen.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Beispiel
Wir möchten zu n > 0 ein k finden mit k 2 ≤ n < (k + 1)2 .
static int sqrt(int n) {
assert n > 0; // Vorbedingung
int l = 1, r = n;
while(r − l > 1) {
int k = (l + r )/2;
if(k ∗ k <= n) {
l = k;
}
else {
r = k;
}
}
assert l ∗ l <= n && n < (l + 1) ∗ (l + 1); // Nachbedingung
return l;
}
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Programme können neben dem eigentlichen Ergebnis weitere
Informationen liefern, die eine Überprüfung der Korrektheit des
Ergebnissen erleichtern.
Beispiel: Berechnung des größten gemeinsamen Teilers
int gcd1 (int a, int b) {
while(b > 0) {
int temp = b;
b = a%b;
a = temp;
}
return a;
}
Dies ist der euklidische Algorithmus.
Wie überprüfen, daß Ergebnis korrekt?
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Der euklidische Algorithmus berechnet den größten gemeinsamen
Teiler.
Der erweiterte euklidische Algorithmus berechnet den größten
gemeinsamen Teiler zweier Zahlen a und b und zusätzlich zwei
ganze Zahlen x und y mit
xa + yb = gcd(a, b).
Falls a und b Vielfache von r sind und xa + yb = r , dann muß
r = gcd(a, b) sein.
Mithilfe von x und y können wir also die Korrekheit beweisen.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
int gcd2 (int a, int b) {
assert a > 0 && b > 0; // Vorbedingung
int x = 1, y = 0, xx = 0, yy = 1, quot, temp;
int r = a, rr = b;
while (rr ! = 0) {
quot = r /rr ;
temp = rr ; rr = r − quot ∗ rr ; r = temp;
temp = xx; xx = x − quot ∗ xx; x = temp;
temp = yy ; yy = y − quot ∗ yy ; y = temp;
}
// Nachbedingung
assert x ∗ a + y ∗ b == r && a%r == 0 && b%r == 0;
return r ;
}
Die assert-Anweisung überprüft eine Bedingung.
Vorsicht: Assertions müssen im Java-Laufzeitsystem
aktiviert werden (durch java -ea GCD).
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Java unterstützt Contract Programming nicht direkt. Beispiel in D:
int gcd(int a, int b, out int x, out int y )
in {assert(a > 0 && b > 0); }
out(r ) {assert(x ∗ a + y ∗ b == r && a%r == 0 && b%r == 0); }
body {
x = 1; y = 0;
int xx = 0, yy = 1, quot, temp;
int r = a, rr = b;
while (rr ! = 0) {
quot = r /rr ;
temp = rr ; rr = r − quot ∗ rr ; r = temp;
temp = xx; xx = x − quot ∗ xx; x = temp;
temp = yy ; yy = y − quot ∗ yy ; y = temp;
}
return r ;
}
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Contract Programming – Zusammenfassung
Hilft Fehler zu lokalisieren.
Auch wenn Programm schon Jahre in Verwendung.
Testen der Nachbedingung muß effizient sein.
Benötigt Zertifizierende Algorithmen.
Außer Vor- und Nachbedingung auch Invarianten.
Ein Zertifizierender Algorithmus liefert neben dem Ergebnis ein
Zertifikat, das eine einfache Überprüfung erlaubt.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Selbstkorrigierende Programme
Defensive Programmierung: Fehlertolerant sein.
Selbstkorrigierendes Programm:
1
2
3
Ergebnis und Zertifikat mit einem effizienten, aber
komplizierten Programm berechnen.
Zertifikat überprüfen.
Überprüfung negativ: Ergebnis neu berechnen mit einem
langsamen, aber einfachen und sicheren Programm.
Programmierung
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
int gcd3 (int a, int b) {
int x = 1, y = 0, xx = 0, yy = 1, quot, temp;
int r = a, rr = b;
while (rr ! = 0) {
quot = r /rr ;
temp = rr ; rr = r − quot ∗ rr ; r = temp;
temp = xx; xx = x − quot ∗ xx; x = temp;
temp = yy ; yy = y − quot ∗ yy ; y = temp;
}
if(!(x ∗ a + y ∗ b == r && a%r == 0 && b%r == 0)) {
r = a;
while(a%r ! = 0 || b%r ! = 0) {
r = r − 1;
}
}
return r ;
}
Überblick
3
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Debugging
Testen
Programmierung
Fehler finden und vermeiden
Debugging
Debugging – Fehler beseitigen
Fehler entstehen weniger, wenn das Programm gut geschrieben
wird.
Gute Dokumentation
Gute Klassenstruktur
Aussagekräftige Namen (Klassen, Methoden, Variablen)
Einheitliche Namensstruktur
Kurze Methoden
Code Duplication vermeiden
. . . (später mehr)
Programmierung
Fehler finden und vermeiden
Debugging
Debugging – Fehler beseitigen
Fehler können leichter entdeckt werden, wenn das Programm gut
geschrieben wird.
Debug-Ausgaben, verbose mode“ (abschaltbar)
”
asserts einbauen
Contract Programming
Tests parallel entwickeln
Methoden einzeln testbar auslegen
Zusätzliche Visualisierung
...
Programmierung
Fehler finden und vermeiden
Debugging
Debugging – Fehler beseitigen
Fehler finden:
Programm genau inspizieren
Code Walkthrough
Teile nacheinander auskommentieren
Debug-Ausgaben einbauen
asserts einbauen
Breakpoints setzen, Variablen inspizieren
Einzelschrittmodus
Programmierung
Fehler finden und vermeiden
Debugging
Einschub: Statische Methoden
class Kegelbahn {
...
public static double sqdistance(Point p, Point q) {
double dx = p.x − q.x;
double dy = p.y − q.y ;
return dx ∗ dx + dy ∗ dy ;
}
...
}
Eine statische Methode gehört zur Klasse, nicht zum einzelnen
Objekt. Sie kann daher nicht auf Instanzvariablen zugreifen.
Ausserhalb der Klasse Kegelbahn können wir sie so aufrufen:
...
double dist = Kegelbahn.sqdistance(point1 , point2 );
...
Programmierung
Fehler finden und vermeiden
Debugging
Einschub: Mengen in der Java-Bibliothek
SethPersoni freunde = new HashSethStudenti();
freunde.add(karl);
freunde.add(eva);
int anzahl = freunde.size();
for(Person p : freunde) {
System.out.println(p);
}
Programmieren mit solchen Container-Klassen“ ist sehr bequem.
”
Später mehr dazu.
Vorsicht: Es fehlen noch wichtige Voraussetzungen.
Programmierung
Fehler finden und vermeiden
Debugging
Arten von Fehlern
Es gibt viele verschiedene Arten von Fehlern in Programmen.
Typos (z.B. n + 1 statt n − 1)
Fall vergessen
Schnittstelle falsch erinnert
Fehler im Algorithmus
Mißverständnis
...
Sie sind unterschiedlich schwer zu finden und zu beseitigen.
Überblick
3
Fehler finden und vermeiden
Contract Programming und der Hoare-Kalkül
Debugging
Testen
Neues Kapitel
1
Einführung in die objektorientierte Programmierung
2
Rekursion
3
Fehler finden und vermeiden
4
Objektorientiertes Design
5
Effiziente Programme
6
Nebenläufigkeit
7
Laufzeitfehler
8
Refactoring
9
Persistenz
10
Eingebettete Systeme
11
Funktionale Programmierung
12
Logische Programmierung
Überblick
4
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Composition, Decorator
Schichtenmodell und Filter-Pipelines
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Mehrfachvererbung
Eine Klasse kann mehrere Unterklassen haben:
Person
Student
Angestellter
Aber: Ein Tutor ist ein Student und Angestellter .
Darf eine Klasse mehrere Oberklassen haben?
Das nennen wir Mehrfachvererbung.
Tutor
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
class Student extends Person {
...
double getGehalt() {return 0.0; }
...
Student
}
class Angestellter extends Person {
...
double getGehalt() {return 458.80; }
...
}
class Tutor extends Student, Angestellter {
...
}
...
Tutor egon = new Tutor (...);
double geld = egon.getGehalt();
Person
Angestellter
Tutor
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Mehrfachvererbung
Mehrfachvererbung kann Probleme bereiten.
Java verbietet Mehrfachvererbung.
Andere Sprachen erlauben sie aber (z.B. C++).
Können wir ohne Mehrfachvererbung leben?
Ja, durch den interface-Mechanismus.
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Abstrakte Klassen
Eine abstrakte Klasse kann Methoden enthalten, die nicht
implementiert sind: Ihr Rumpf fehlt.
abstract class Tonerzeuger {
void macheTon();
}
class Katze extends Tonerzeuger {
void macheTon() {
System.out.println("Miau");
}
}
class Trompete extends Tonerzeuger {
void macheTon() {
System.out.println("Trara");
}
}
Jeder Tonerzeuger hat so sicherlich macheTon().
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Abstrakte Klassen
Abstrakte Klassen werden oft für die Spezifikation abstrakter
Datentypen verwendet:
abstract class StackhT i {
public boolean isempty ();
public T top();
public void pop();
public void push(T x);
}
Jede Unterklasse von StackhT i hat jetzt garantiert diese
Methoden.
(Sie sollte auch die Semantik eines Stacks richtig umsetzen.)
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
class LinkedStackhT i implements StackhT i {
private T element = null;
private LinkedStackhT i next = null;
public boolean isempty () {return element == null; }
public T top() {return element; }
public void pop() {
if(next.isempty ()) {element = null; }
else {
element = next.element;
next = next.next;
}
}
public void push(T x) {
LinkedStackhT i s = new LinkedStackhT i();
s.element = element; s.next = next;
next = s; element = x;
}
}
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
LinkedStack ist eine konkrete Implementierung eines Stack.
(Sie ist nicht besonders schön, aber kurz. . . )
Es kann auch andere Implementierungen geben.
StackhString i s = new LinkedStackhString i();
s.push("A");
s.push("B");
System.out.println(s.top());
LinkedStackhString i s = new LinkedStackhString i();
s.push("A");
s.push("B");
System.out.println(s.top());
Die erste Möglichkeit ist besser!
Warum???
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Abstrakte Klassen und Interfaces
Eine abstrakte Klassen kann auch einige Methoden vollständig
implementieren.
Wenn dies nicht so ist, dann gibt die abstrakte Klasse nur ein
Versprechen ab, daß es bestimmte Methoden gibt.
So etwas nennt man auch ein Interface.
Ein Interface bestimmt, daß bestimmte Methoden existieren
müssen.
interface Tonerzeuger {
void macheTon();
}
Java stellt explizit interfaces zur Verfügung.
In Sprachen mit Mehrfachvererbung werden sie durch
abstrakte Klassen umgesetzt.
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
interface Tonerzeuger {
void macheTon();
}
interface StackhT i {
public boolean isempty ();
public T top();
public void pop();
public void push(T x);
}
class LinkedStackhT i implements StackhT i {
...
}
class LinkedStackMitTonhT i
extends LinkedStackhT i implements Tonerzeuger {
void macheTon() {
System.out.println("Klack");
}
}
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Iterator
Iteratoren sind ein Standardverfahren, um auf Daten zuzugreifen.
public interface Iterator hE i {
boolean hasNext(); // gibt es ein weiteres Element?
E next(); // gib das nächste Element zurück
}
public interface IterablehE i {
Iterator hE i iterator (); // gibt einen Iterator über alle Elemente zurück
}
Ist coll ein IterablehString i, dann sind solche Schleifen möglich:
Iterator hString i it = coll.iterator ();
while(it.hasNext()) {
String s = it.next();
System.out.println(s);
}
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Iterator
In Java gibt es eine hübsche Abkürzung.
Es sei wieder coll ein IterablehString i. Anstatt
Iterator hString i it = coll.iterator ();
while(it.hasNext()) {
String s = it.next();
System.out.println(s);
}
können wir auch dies schreiben:
for(String s : coll) {
System.out.println(s);
}
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Beispiel: Unsere Liste
Erinnern wir uns an unsere Listenimplementierung ListhT i.
l1
false
1
false
18
false
5
Die öffentlichen Methoden sind
void isempty (),
T head(),
ListhT i tail() und
ListhT i add(T x).
Wir implementieren einen Iterator.
false
3
true
?
?
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
class ListIterator hT i implements Iterator hT i {
private ListhT i currentNode;
public ListIterator (ListhT i list) {
currentNode = list;
}
public T next() {
T result = currentNode.head();
currentNode = currentNode.tail();
return result;
}
public boolean hasNext() {
return!currentNode.isempty ();
}
public void remove() {}
}
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Die Klasse ListhT i muß nun noch das Interface IterablehT i
implementieren.
class ListhT i implements IterablehT i {
private final boolean empty ; // ist diese Liste leer?
private final T value; // das erste Element dieser Liste
private final ListhT i rest; // restliche Liste ohne das erste Element
public Iterator hT i iterator () {
return new ListIterator hT i(this);
}
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Nun können wir Iteratoren für die Klasse ListhT i sofort verwenden.
public static void main(String args[ ]) {
ListhString i list = new ListhString i();
list = list.add("Informatik");
list = list.add("Physik");
list = list.add("Mathematik");
list = list.add("Chemie");
list = list.add("Biologie");
for(String s : list) {
System.out.println(s);
}
}
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Einschub: Autoboxing
Können wir Listhinti verwenden?
Nein, denn int ist keine Klasse.
Java ist keine reine okjektorientierte Sprache: Nicht alles ist ein
Objekt.
Grund: Effizienz.
Lösung in Java: Wrapper-Klassen.
Primitiver Typ
int
double
boolean
Wrapper-Klasse
Integer
Double
Boolean
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Autoboxing
SethInteger i zahlen = new HashSethInteger i();
zahlen.add(34);
zahlen.add(23);
zahlen.add(117);
zahlen.add(3);
zahlen.add(−345);
zahlen.add(23);
zahlen.add(35);
for(int k : zahlen) {
System.out.println(k ∗ k);
}
No children were harmed in the making of these slides.
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Autoboxing
MaphShort, String i map;
map = new HashMaphShort, String i();
short n = 17;
map.put(n, "Siebzehn");
System.out.println(map.get(17));
Vorsicht! Es gibt merkwürdige Effekte und schwer zu findende
Fehler.
Tip: Nur Integer , Double, Boolean verwenden.
Nur mit Vorsicht Short, Long , etc.
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Container-Klassen
Klassen die viele Elemente zusammenfassen, sind sehr nützlich.
Die wichtigsten solchen Klassen modellieren Mengen, Listen,
Arrays und Abbildungen.
Interfaces in der Java-Bibliothek:
SethE i, Mengen
ListhE i, Listen und Arrays
MaphK , E i, Abbildungen und Wörterbücher
Um auf sie zuzugreifen, müssen wir java.util.Set schreiben, oder
am Anfang der Datei:
import java.util.Set; // importiert Set
import java.util.∗; // importiert alles aus java.util
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Set
SethE i ist nur ein Interface.
Implementierungen davon sind z.B. HashSethE i und TreeSethE i.
Um SethE i korrekt verwenden zu können, muß die Klasse E
gewisse Anforderungen erfüllen.
boolean equals(Object o), ist o das gleiche?
int hashCode(), berechnet einen Hashwert.
int compareTo(E e), vergleiche und gib <0, 0, >0 zurück
Jede Klasse erbt equals und hashCode von Object.
compareTo ist im Interface ComparablehE i definiert.
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
class Person implements ComparablehPersoni {
String nachname, vorname;
public Person(String v , String n) {
vorname = v ; nachname = n;
}
public boolean equals(Object o) {
if(o == null ||!(o instanceof Person)) {
return false;
}
Person other = (Person)o;
return nachname.equals(other .nachname) &&
vorname.equals(other .vorname);
}
public int hashCode() {
return nachname.hashCode() + vorname.hashCode();
}
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Wir können nun HashSethPersoni verwenden. Hierzu haben wir
equals und hashCode implementiert.
Für TreeSethPersoni muß Person auch noch das Interface
ComparablehPersoni implementieren.
public int compareTo(Person other ) {
int nachnameOrder = nachname.compareTo(other .nachname);
if(nachnameOrder ! = 0) {
return nachnameOrder ;
}
else {
return vorname.compareTo(other .vorname);
}
}
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Gegeben sei eine Menge von Zahlen {3, 5, 10}.
Welche Zahlen können wir bilden, indem wir die Zahlen einer
Untermenge addieren?
Antwort: {0, 3, 5, 8, 10, 13, 15, 18}.
static SethInteger i subsetsums(int size[ ]) {
SethInteger i sums = new TreeSethInteger i();
sums.add(0);
for(int k : size) {
SethInteger i newsums = new TreeSethInteger i();
for(int l : sums) {
newsums.add(k + l);
}
sums.addAll(newsums);
}
return sums;
}
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Abbildungen und Wörterbücher
Das Interface MaphK , E i modelliert Abbildungen K → E .
So können wir ein Wörterbuch implementieren:
MaphString , String i deuita = new HashMaphString , String i();
deuita.put("schneiden", "tagliare");
deuita.put("Butter", "burro");
deuita.put("Igel", "riccio");
String x = deuita.get("Butter");
Mit put legen wir Werte fest und mit get schlagen wir sie nach.
Typische Implementierungen sind HashMap und TreeMap.
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Bei den Subsetsums berechneten wir die Summen, aber nicht
durch welche Untermenge sie gebildet werden. Hier berechnen wir
eine Map, welche uns zu jeder Summe eine Zahl ihrer Menge gibt.
static MaphInteger , Integer i subsetmap(int size[ ]) {
MaphInteger , Integer i sums = new TreeMaphInteger , Integer i();
sums.put(0, 0);
for(int k : size) {
MaphInteger , Integer i newsums;
newsums = new TreeMaphInteger , Integer i();
for(int l : sums.keySet()) {
newsums.put(k + l, k);
}
newsums.putAll(sums);
sums = newsums;
}
return sums;
}
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
List
Es gibt in der Java-Standardbibliothek das Interface ListhE i (nicht
zu verwechseln mit unserer Implementierung).
Folgende Operationen gibt es u.a.:
void add(E x), füge x am Ende ein.
void add(int n, E x), füge x an Position n ein.
void set(int n, E x), ersetzt die nte Position durch x.
E get(int n), was ist an Position n?
int size(), wieviele Elemente enthält es?
Es wird sowohl ein wachsendes“ Array als auch eine Liste
”
modelliert.
Typische Implementierungen: ArrayListhE i und LinkedListhE i.
Oft ist es bequemer eine List als ein
normales Array zu verwenden.
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Visitor
Erinnern wir uns an die Klasse Tree.
Sie implementiert Bäume, deren Knoten Zahlen enthalten.
4
3
2
2
3
2
2
2
3
2
2
2
3
2
2
2
2
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Öffentlichen Methoden getRoot(), getChildren(), addChild(Tree t)
und Konstruktor new Tree(int root).
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Visitor
Wie können wir das Gesamtgewicht eines Baumes berechnen?
Wir machten dies früher durch Schreiben einer geeigneten
Methode.
Nachteil: Enge Kopplung“.
”
So müssen wir oft die Klasse selbst verändern, was nicht gut ist.
Alternative: Nur die Grundoperationen getRoot() und
getChildren() verwenden.
Weitere Möglichkeit: Manchmal hilft es immens einen Visitor oder
Iterator zu verwenden.
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Visitor
Wir erweitern die Klasse Tree so, daß sie einen TreeVisitor
akzeptiert“.
”
interface TreeVisitor {
public void visit(Tree t);
}
Die Klasse Tree ermöglicht es einem TreeVisitor durch einen
einfachen Aufruf alle Knoten des Baumes zu besuchen“.
”
Tree tree = new Tree(24);
...
TreeVisitor visitor = new MyTreeVisitor ();
tree.accept(visitor );
Hierin ist MyTreeVisitor eine konkrete Klasse,
die TreeVisitor implementiert.
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Klassendiagramm
Tree
«interface»
TreeVisitor
int r oot ;
List<Tree> children;
v oid visit (Tr ee t );
List < Tr ee> g et Child r en();
int g et Root ();
v oid ad d Child (Tr ee t );
v oid accep t (Tr eeVisit or v);
SumVisitor
MaxVisitor
int sum m e;
int max;
int g et Sum m e();
intg et Maxim um ();
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Visitor
So implementieren wir die Methode accept(Tree t) in der Klasse
Tree:
public void accept(TreeVisitor v ) {
v .visit(this);
for(Tree child : children) {
child.accept(v );
}
}
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Die Gesamtsumme aller Knoten können wir jetzt so berechnen:
public static void main(String args[ ]) {
Tree t1 = new Tree(1);
Tree t2 = new Tree(2);
Tree t3 = new Tree(3);
Tree t4 = new Tree(4);
Tree t5 = new Tree(5);
Tree t6 = new Tree(6);
t1 .addChild(t2 );
t1 .addChild(t3 );
t3 .addChild(t4 );
t3 .addChild(t5 );
t3 .addChild(t6 );
SumVisitor visitor = new SumVisitor ();
t1 .accept(visitor );
System.out.println(visitor .summe());
}
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Hierfür müssen wir nur den geeigneten Visitor implementieren:
class SumVisitor implements TreeVisitor {
private int sum = 0;
public void visit(Tree t) {
sum = sum + t.getRoot();
}
public int summe() {
return sum;
}
}
Dies ist eine Art neue Funktionalität einer Klasse hinzuzufügen.
Programmierung
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Es ist jetzt leicht, weitere Visitors zu schreiben.
Zum Beispiel, um das Maximum zu berechnen:
class MaxVisitor implements TreeVisitor {
private int max = −1;
public void visit(Tree t) {
int value = t.getRoot();
if(value > max) {
max = value;
}
}
public int maximum() {
return max;
}
}
Überblick
4
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Composition, Decorator
Schichtenmodell und Filter-Pipelines
Programmierung
Objektorientiertes Design
Composition, Decorator
“Composition over Inheritance”
Eine Möglichkeit, daß eine Klasse B die Funktionalität von A
übernimmt, ist Vererbung:
class B extends A {
...
}
Eine andere Möglichkeit ist Komposition.
Klasse B enthält ein Object A:
class B {
A a;
...
}
Dadurch kann B im Prinzip auch alles, was A kann.
Komposition kann flexibler sein und
ist oft besser als Vererbung.
Programmierung
Objektorientiertes Design
Composition, Decorator
Vererbung
Betrachten wir folgendes Klassendiagramm:
Wenn wir mehr Tiere hinzufügen, gibt es eine Unzahl von
spezialisierten Klassen.
Wir können keinen angezogenen, angemalten Hund“ durch
”
Vererbung erzeugen (keine Mehrfachvererbung).
Programmierung
Objektorientiertes Design
Composition, Decorator
interface Tier {
public double getGewicht();
public String zeichnung ();
}
Einige konkrete Tiere:
class Katze implements Tier {
public double getGewicht() {return 2.5; }
public String zeichnung () {return "Katze"; }
}
class Hund implements Tier {
public double getGewicht() {return 3.5; }
public String zeichnung () {return "Hund"; }
}
Programmierung
Objektorientiertes Design
Composition, Decorator
Composition: Decorator
Eine mögliche Lösung: Wir verwenden einen Decorator, der
dynamisch Funktionalität zu einer Klasse hinzufügen kann.
«interface»
Tier
double getGewicht();
String zeichnen();
TierDecorator
Tier tier;
double getGewicht();
String zeichnen();
Hund
double getGewicht();
String zeichnen();
Katze
double getGewicht();
String zeichnen();
AngemaltesTier
AngezogenesTier
String farbe;
double getGewicht();
String zeichnen();
double getGewicht();
String zeichnen();
Programmierung
Objektorientiertes Design
Composition, Decorator
interface Tier {
public double getGewicht();
public String zeichnung ();
}
Die abstrakte Klasse TierDecorator ist auch ein Tier :
abstract class TierDecorator implements Tier {
private Tier tier ;
public TierDecorator (Tier tier ) {
this.tier = tier ;
}
public double getGewicht() {
return tier .getGewicht();
}
public String zeichnung () {
return tier .zeichnung ();
}
}
Programmierung
Objektorientiertes Design
Composition, Decorator
Wir können jetzt einige TierDecorator s implementieren:
class AngezogenesTier extends TierDecorator {
public AngezogenesTier (Tier tier ) {
super (tier );
}
public double getGewicht() {
return super .getGewicht() + 0.5;
}
public String zeichnung () {
return super .zeichnung () + " mit Mantel";
}
}
Ein angezogenes Tier ist ein Tier mit einem Mantel.
Der Mantel wiegt ein bißchen.
Programmierung
Objektorientiertes Design
Composition, Decorator
Ein angemaltes Tier“ hat eine Farbe, die wir dem Konstruktor
”
mitteilen müssen:
class AngemaltesTier extends TierDecorator {
private String farbe;
public AngemaltesTier (Tier tier , String farbe) {
super (tier );
this.farbe = farbe;
}
public String zeichnung () {
return super .zeichnung () + " " + farbe + " angemalt";
}
}
Programmierung
Objektorientiertes Design
Composition, Decorator
Wir können jetzt beliebige Dekorationen an einem Tier
anbringen“:
”
public static void main(String args[ ]) {
Tier hachiko = new Hund();
hachiko = new AngezogenesTier (hachiko);
hachiko = new AngemaltesTier (hachiko, "weiss");
Tier garfield = new Katze();
garfield = new AngemaltesTier (garfield, "gelb-schwarz");
System.out.println(hachiko.zeichnung ());
System.out.println(garfield.zeichnung ());
}
Ausgabe:
Hund mit Mantel blau angemalt
Katze gelb-schwarz angemalt
Überblick
4
Objektorientiertes Design
Interfaces, Container, Iterator, Visitor
Composition, Decorator
Schichtenmodell und Filter-Pipelines
Programmierung
Objektorientiertes Design
Schichtenmodell und Filter-Pipelines
Beispiel: Morsecode dekodieren
Morsecode, sauberer Ton:
Etwas verrauscht:
Wie können wir den Klartext erhalten?
Programmierung
Objektorientiertes Design
Schichtenmodell und Filter-Pipelines
Die Audiodaten werden durch mehrere Filter geschickt.
buffer = filter .readFile("withnoise.raw");
buffer = filter .average(200, buffer )
buffer = filter .threshold(buffer )
Programmierung
Objektorientiertes Design
Schichtenmodell und Filter-Pipelines
buffer = filter .threshold(buffer );
buffer = filter .cummulate(buffer );
[-1234, 968, -1085, 2951, -44005, 2954, -1076, 977, -1051, 976,
-1052, 977, -43977, 2954, -1076, 977, -1077, 2955, -1075, 976,
-43978, 2954, -1076, 976, -1053, 976, -43951, 976, -43952, 977,
-1051,. . . ]
String dotdashs = filter .symbolize(buffer );
.- -... -.-. -.. . ..-. --. .... .. .--- -.- .-.. --. --- .--. --.- .-. ... - ..- ...- .-- -..- -.-- --..
ListhString i tokens = filter .tokenize(dotdashs);
[.-, -..., -.-., -.., ., ..-., --., ...., .., .---,
-.-, .-.., --, -., ---, .--., --.-, .-., ..., -, ..-,
...-, .--, -..-, -.--, --..]
String cleartext = filter .decode(tokens);
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Programmierung
Objektorientiertes Design
Schichtenmodell und Filter-Pipelines
double[ ] average(int k, double a[ ]) {
int n = a.length;
assert n > k;
double sum[ ] = new double[n];
sum[0] = a[0];
for (int i = 1; i < n; i ++) {
sum[i] = sum[i − 1] + Math.abs(a[i]);
}
double mittel[ ] = new double[n];
for (int i = k; i < n; i ++) {
mittel[i] = (sum[i] − sum[i − k])/k;
}
for (int i = 0; i < k; i ++) {
mittel[i] = mittel[k];
}
return mittel;
}
Programmierung
Objektorientiertes Design
Schichtenmodell und Filter-Pipelines
double[ ] threshold(double a[ ], double mean[ ]) {
assert mean.length == 2;
int n = a.length;
double result[ ] = new double[n];
int diff = 0;
for (int i = 0; i < n; i ++) {
if (Math.abs(mean[0] − a[i]) < Math.abs(mean[1] − a[i])) {
diff = diff − 1;
} else {
diff = diff + 1;
}
final int d = 10;
if (diff > d) {diff = d; result[i] = 1; }
if (diff < −d) {diff = −d; result[i] = 0; }
}
return result;
}
Programmierung
Objektorientiertes Design
Schichtenmodell und Filter-Pipelines
ListhInteger i cummulate(double a[ ]) {
int n = a.length, count = 0;
ListhInteger i result = new ArrayListhInteger i();
for (int i = 1; i < n; i ++) {
if((a[i] > 0) ! = (a[i − 1] > 0)) {
result.add(count);
count = 0;
}
if(a[i] > 0) {
count = count + 1;
}
else {
count = count − 1;
}
}
result.add(count);
return result;
}
Programmierung
Objektorientiertes Design
Schichtenmodell und Filter-Pipelines
String symbolize(ListhInteger i len) {
String s = "";
double pulse[ ] = kMeans.getCenters(2, signFilter (+1, len));
double pause[ ] = kMeans.getCenters(2, signFilter (−1, len));
for(int dur : len) {
if(dur < 0) {
if(Math.abs(pause[0] − dur ) < Math.abs(pause[1] − dur )) {
s = s + " ";
}
}
else {
if(Math.abs(pulse[0] − dur ) < Math.abs(pulse[1] − dur )) {
s = s + "."; } else {s = s + "-"; }
}
}
return s;
}
Programmierung
Objektorientiertes Design
Schichtenmodell und Filter-Pipelines
double[ ] signFilter (int sign, ListhInteger i list) {
ListhInteger i result = new ArrayListhInteger i();
for (int n : list) {
if ((sign > 0) == (n > 0)) {
result.add(n);
}
}
double r [ ] = new double[result.size()];
for(int i = 0; i < r .length; i ++) {
r [i] = result.get(i);
}
return r ;
}
Programmierung
Objektorientiertes Design
Schichtenmodell und Filter-Pipelines
ListhString i tokenize(String dotdashs) {
ListhString i tokens = new ArrayListhString i();
String z = "";
for (int i = 0; i < dotdashs.length(); i ++) {
if (dotdashs.charAt(i) == ’ ’) {
tokens.add(z);
z = "";
} else {
z = z + dotdashs.charAt(i);
}
}
tokens.add(z);
return tokens;
}
Programmierung
Objektorientiertes Design
Schichtenmodell und Filter-Pipelines
String decode(ListhString i tokens) {
String result = "";
for (String morseToken : tokens) {
result + = translateMorseCodeToChar (morseToken);
}
return result;
}
char translateMorseCodeToChar (String code) {
Character c = morseTable.get(code);
if(c == null) {
return ’?’;
}
else {
return c;
}
}
Programmierung
Objektorientiertes Design
Schichtenmodell und Filter-Pipelines
Pipelines, um schrittweise zum Ergebnis zu kommen.
Ergebnis
decode()
tokenize()
symbolize()
cummulate()
threshold()
average()
readFile()
wav-Datei
Programmierung
Objektorientiertes Design
Schichtenmodell und Filter-Pipelines
Programmieren in Schichten
Pipeline: Die Daten laufen in einer Richtung.
Verallgemeinerung: Viele Schichten, die immer mehr abstrahieren.
Anwendungsprogramm
Dateisystem
Betriebssystemkern
Treiber
Hardware
Neues Kapitel
1
Einführung in die objektorientierte Programmierung
2
Rekursion
3
Fehler finden und vermeiden
4
Objektorientiertes Design
5
Effiziente Programme
6
Nebenläufigkeit
7
Laufzeitfehler
8
Refactoring
9
Persistenz
10
Eingebettete Systeme
11
Funktionale Programmierung
12
Logische Programmierung
Überblick
5
Effiziente Programme
Schnelle und langsame Programme
Speicherplatz
Programmierung
Effiziente Programme
Schnelle und langsame Programme
Wann ist ein Programm schnell?
Die Geschwindigkeit von Programmen ist schwer zu vergleichen.
Welcher Computer?
Welcher Compiler?
Größe des Speichers?
Cache-Speicher?
Welche anderen Programme laufen?
etcetera, etcetera.
Die Geschwindigkeit eines Programms kann auch vom Wetter
abhängen.
Programmierung
Effiziente Programme
Schnelle und langsame Programme
Schnelle Programme
Wir können Programme durch Tuning“ verbessern.
”
Dies lohnt sich aber nur bei den Teilen, welche die meiste Zeit
verbrauchen: innere Schleifen“
”
Und:
Premature optimization is the root of all evil.
Donald Knuth
Ein überlegener Algorithmus kann die bessere Wahl sein.
Programmierung
Effiziente Programme
Schnelle und langsame Programme
450
400
350
300
250
200
150
100
50
0
+
+
+
+
+
+
+
+
+
+
+
+
+
++
+
+
+ ++
+
+
+ ++++
++
+
+++
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
++
++
+
++
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
++
+
+
++
+
+
+
+
+
+
+
+
0
50000 100000 150000 200000 250000 300000 350000 400000 450000 500000
Zehnmalige Wiederholung von Quicksort.
Programmierung
Effiziente Programme
Schnelle und langsame Programme
0.0015
+
0.0014
0.0013
0.0012
+
0.0011
++
+ ++
+ ++++
+
++ +
++
++
+
++
+++
+
+++
+
+
+
++
+
+
+
+
+
+
++ +
0.001 +
++
++
++ +
++ ++
+
+++
+
+++
+
+
+
++
++
++
+
+
+
++
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ++
++
+
+
+
++
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
++
+++
0.0009
0.0008
0.0007
0
50000 100000150000200000250000300000350000400000450000500000
Laufzeit pro Datenelement.
Programmierung
Effiziente Programme
Schnelle und langsame Programme
600
500
400
300
200
100
0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
++
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0
50000 100000 150000 200000 250000 300000 350000 400000 450000 500000
Zehnmalige Wiederholung von Heapsort.
Programmierung
Effiziente Programme
Schnelle und langsame Programme
0.00125
0.0012
0.00115
0.0011
0.00105
+ ++
+ +
+
++
++
+++
+
+
+
+
+
+
+
+
+
+++
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+++++
+ ++
+
+ ++
+
++++
++
+++
+
+
+
+++++
+
++ +
+
+
++
+
+
+
+
+
+
+
+
+
+
+
++
+
+
+
+
+
+
+
+
+
++
+
++
+
+
+
++
+
+
+
+
+
+
+
+
+
+
+++
+
+
+
+
+++
++
++
+
+
+
+
0.001 +++
0.00095
0.0009
+
0.00085 +
0
50000 100000150000200000250000300000350000400000450000500000
Laufzeit pro Datenelement.
Programmierung
Effiziente Programme
Schnelle und langsame Programme
Das n-Damenproblem
QZ0Z0Z0Z
Z0Z0Z0L0
0Z0L0Z0Z
Z0Z0ZQZ0
0Z0Z0Z0L
ZQZ0Z0Z0
0Z0ZQZ0Z
Z0L0Z0Z0
Wie können wir das n-Damenproblem schneller lösen?
Programmierung
Effiziente Programme
Schnelle und langsame Programme
Dies war unser Programm:
void setzeDamenAbZeile(int y ) {
if(y >= n) {
geloest = true;
return;
}
for(int x = 0; x < n; x ++) {
if(safePosition(y , x)) {
brett[y ][x] = true;
setzeDamenAbZeile(y + 1);
if(geloest) {
return;
}
brett[y ][x] = false;
}
}
}
Programmierung
Effiziente Programme
Schnelle und langsame Programme
10000
+
1000
100
+
+
10
+
+
1
+
0.01
5
+
+
10
+
++
+
+
+
15
Laufzeit des n-Damenproblems.
+
+
++
+
+
0.1
0.001
+
+
+
+
20
25
30
35
Programmierung
Effiziente Programme
Schnelle und langsame Programme
void setzeDamenAbZeile(int y , int totallives) {
lives = totallives;
if(y >= n) {
geloest = true;
return;
}
lives = lives − 1;
if(lives < 0) {return; }
int offset = generator .nextInt(n);
for(int k = 0; k < n; k ++) {
int x = (k + offset)%n;
if(safePosition(y , x)) {
brett[y ][x] = true;
setzeDamenAbZeile(y + 1, lives);
if(geloest) {return; }
brett[y ][x] = false;
}
}
}
Programmierung
Effiziente Programme
Schnelle und langsame Programme
Zwei Innovationen:
Nach gewisser Zeit aufgeben und neu anfangen.
Die Damen in zufälliger Reihenfolge setzen.
void setzeDamenAbZeile(int y ) {
int totallives = 1000;
while(!geloest) {
for(int yy = y ; yy < n; yy ++) {
for(int x = 0; x < n; x ++) {
brett[yy ][x] = false;
}
}
setzeDamenAbZeile(y , totallives);
totallives + = 100;
}
}
Programmierung
Effiziente Programme
Schnelle und langsame Programme
100
10
1
0.1
0.01
0.001
+
+
+
+
+ +
+ +
++ + + ++
+
+
+
+++
++
++
+ ++
+
+
+
+
+
+
+++ +
++
+ + ++
+
+++
+ ++
+
+
+
++ +
+ ++ +
+ +
+ ++
+
+
+
+
+
+
+
+
++
+
+ + ++
++
+ ++
++
++ ++
+
+ +
+
+
+
+
+
+
+
+
+ ++
+ ++ ++ +
++
+
+++
+
+
++ ++ + +
++
+ +
+
+
+
+
+ ++
+
+
+
+ +
+
+
+
++++ +
+
+
+
+
+
+
+
+
+ ++
+
+
+++ +
+ + ++
+++
+
+
++
+ +++
+ ++
+
++++ ++ ++
++ +
+
+
+
+
+ +
+ + ++
+
+ ++
+++
+
+
+
+
+
+
+
+
+
++++
+
+
+ ++
++ +
++ + +
+
+
++
+++
+
+
+
+
+
+
+
+
+
+ +
+
++
+
+
+
++
+
++
+
+
+++
+++ +
++ + +
+
++ + +
+
++ +
++
+ +
+++
+
+ ++
+
+
++
+++
+
+ +
+
+
+
0
50
100
150
200
250
300
350
Laufzeit des n-Damenproblems mit Wiederstart.
Programmierung
Effiziente Programme
Schnelle und langsame Programme
0Z0Z0Z0Z0Z0Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0
qZ0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ0Z0Z0Z0Z0Z0Z0Z0
0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0l0Z0Z0Z0Z0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0
0Z0Z0Z0Z0Z0Z0Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z
Z0Z0Z0Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0
0Z0Z0Z0Z0ZqZ0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ0Z0Z0Z0Z0Z0
0Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0l0Z0Z0Z0Z0Z0Z0
0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0
0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0
0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ0Z0Z0Z0Z0Z0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0l0Z0Z0Z0Z0Z0
0Z0Z0Z0Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0
0Z0Z0Z0Z0Z0ZqZ0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ0
0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0l
Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0
0Z0Z0Z0Z0Z0Z0Z0Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0
0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ0Z
ZqZ0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0
0Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0l0Z0Z0Z0Z0Z0Z0Z0
0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0
0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0l0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0l0Z0Z0Z0Z0
0Z0Z0ZqZ0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0
0Z0Z0Z0Z0Z0Z0ZqZ0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ0Z0
0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0
0Z0Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0
0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0l0Z0Z0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ0Z0Z0Z0Z0Z0Z0Z0Z0
0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0ZqZ0Z0Z0Z0
0Z0Z0Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z
Z0Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0
0Z0Z0Z0Z0Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z
Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0Z0l0Z0Z0Z0Z0Z0Z0Z0Z0Z0
Überblick
5
Effiziente Programme
Schnelle und langsame Programme
Speicherplatz
Programmierung
Effiziente Programme
Speicherplatz
Speichersystem
Java verwendet ein sehr bequemes und zuverlässiges
Speichersystem.
Mit x = new Person(...) wird Speicher für ein neues Objekt
alloziert.
Durch eine garbage collection werden regelmäßig nicht mehr
benötigte Objekte wieder freigegeben.
Dies passiert transparent.
Aber: Jedes new kostet Zeit und jedes Objekt verschwendet ein
bißchen Speicherplatz.
Wenn dies eine Rolle spielt, dann sind primitive Datentypen und
Arrays die schnellste und sparsamste Lösung.
Programmierung
Effiziente Programme
Speicherplatz
Initialisierung eines Arrays:
static int[ ] arrayTest(int n) {
int a[ ] = new int[n];
for(int i = 0; i < n; i ++) {
a[i] = i;
}
return a;
}
Dies dauert mit n = 100.000.000 z.B. 0.16 Sekunden.
Programmierung
Effiziente Programme
Speicherplatz
Jetzt verwenden wir eine ArrayListhInteger i.
static ListhInteger i listTest(int n) {
ListhInteger i l = new ArrayListhInteger i();
for(int i = 0; i < n; i ++) {
l.add(i);
}
return l;
}
Laufzeit jetzt 14.2 Sekunden.
Neues Kapitel
1
Einführung in die objektorientierte Programmierung
2
Rekursion
3
Fehler finden und vermeiden
4
Objektorientiertes Design
5
Effiziente Programme
6
Nebenläufigkeit
7
Laufzeitfehler
8
Refactoring
9
Persistenz
10
Eingebettete Systeme
11
Funktionale Programmierung
12
Logische Programmierung
Überblick
6
Nebenläufigkeit
Threads
Synchronisation
Programmierung
Nebenläufigkeit
Threads
Computer haben oft mehrere Prozessoren.
Tendenz ist zunehmend.
Aufgenommen im Deutschen Museum von Clemens Pfeiffer.
Fazit: Paralleles Programmieren immer wichtiger.
Programmierung
Nebenläufigkeit
Threads
Ein Programm kann mehrere laufende threads besitzen.
Diese laufen gleichzeitig oder scheinbar gleichzeitig.
So können wir einen thread starten:
Thread thread = new Thread(somethingRunnable);
thread.start();
Hierbei ist somethingRunnable ein Objekt, das das Interface
Runnable implementiert.
Hierfür muß es eine Methode public void run() besitzen.
Programmierung
Nebenläufigkeit
Threads
Beispiel
class ZahlenZeiger implements Runnable {
private int id;
private volatile boolean finished = false;
public ZahlenZeiger (int id) {
this.id = id;
}
public void run() {
for(int i = 0; i < 100000; i ++) {
System.out.println(id + ": " + i);
}
finished = true;
}
public boolean finished() {
return finished;
}
}
Programmierung
Nebenläufigkeit
Threads
Volatile variables
Wir können eine Variable volatile deklarieren.
Damit sagen wir, daß auf diese Variable von mehreren Threads
zugegriffen werden kann.
Folgendes Programmstück kann vom Compiler optimiert werden,
da sich i niemals ändert.
int i = 1;
while(i == 1) {}
Die Schleife kann aber beendet werden, wenn i von einem anderen
Thread verändert wird.
Ein weiteres Problem sind Caches, die den Speicher von
verschiedenen Prozessoren verschieden aussehen lassen.
Lösung: volatile int i = 1;
Programmierung
Nebenläufigkeit
Threads
“Busy waiting” (oder “polling”):
static void test1 () {
ListhZahlenZeiger i zeigerList = new ArrayListhZahlenZeiger i();
for(int i = 1; i <= 10; i ++) {
ZahlenZeiger z = new ZahlenZeiger (i);
zeigerList.add(z);
Thread thread = new Thread(z);
thread.start();
}
boolean stillRunning = true;
while(stillRunning ) {
stillRunning = false;
for(ZahlenZeiger z : zeigerList) {
if(!z.finished()) {stillRunning = true; }
}
}
}
Programmierung
Nebenläufigkeit
Threads
Warten durch join():
static void test2 () {
ListhThreadi threads = new ArrayListhThreadi();
for(int i = 1; i <= 10; i ++) {
ZahlenZeiger z = new ZahlenZeiger (i);
Thread t = new Thread(z);
t.start();
threads.add(t);
}
try {
for(Thread t : threads) {t.join(); }
}
catch(InterruptedException e) {System.out.println(e); }
}
join() wartet, bis der Thread endet.
Programmierung
Nebenläufigkeit
Threads
Unterklasse von Thread statt Runnable:
class QueensSolver extends Thread {
private int n; // Groesse des Bretts
private volatile Brett brett;
private volatile boolean finished = false;
public QueensSolver (int n) {this.n = n; }
public void run() {
brett = new RandomBrett(n);
brett.setzeDamenAbZeile(0);
finished = true;
}
public void printSolution() {brett.print(); }
public boolean finished() {return finished; }
public void finish() {brett.geloest = true; }
}
Programmierung
Nebenläufigkeit
Threads
static void test3 () {
final int n = 250;
ListhQueensSolver i solvers = new ArrayListhQueensSolver i();
for(int i = 0; i < 4; i ++) {
QueensSolver solver = new QueensSolver (n);
solver .start(); solvers.add(solver );
}
while(true) { // Polling!
for(QueensSolver solver : solvers) {
if(solver .finished()) {
solver .printSolution();
for(QueensSolver s : solvers) {s.finish(); }
return;
}
}
}
}
Überblick
6
Nebenläufigkeit
Threads
Synchronisation
Programmierung
Nebenläufigkeit
Synchronisation
Es ist schwer, Daten konsistent zu halten, wenn mehrere Threads
auf sie zugreifen:
class WortPaarVersuch implements Runnable {
private String deutsch;
private String englisch;
public void run() {
while(true) {
setNames("Hallo", "hello");
setNames("Hund", "dog");
setNames("Katze", "cat");
}
}
public void setNames(String d, String e) {
deutsch = d; englisch = e;
}
public String getNames() {
return deutsch + "=" + englisch;
}
}
Programmierung
Nebenläufigkeit
Synchronisation
Deklarieren wir Methoden synchronized, dann können keine von
ihnen zeitlich überlappt ablaufen.
class WortPaar implements Runnable {
private volatile String deutsch;
private volatile String englisch;
public void run() {
while(true) {
setNames("Hallo", "hello");
setNames("Hund", "dog");
setNames("Katze", "cat");
}
}
public synchronized void setNames(String d, String e) {
deutsch = d; englisch = e;
}
public synchronized String getNames() {
return deutsch + "=" + englisch;
}
}
Neues Kapitel
1
Einführung in die objektorientierte Programmierung
2
Rekursion
3
Fehler finden und vermeiden
4
Objektorientiertes Design
5
Effiziente Programme
6
Nebenläufigkeit
7
Laufzeitfehler
8
Refactoring
9
Persistenz
10
Eingebettete Systeme
11
Funktionale Programmierung
12
Logische Programmierung
Programmierung
Laufzeitfehler
Fehlerbehandlung
Unter einem Laufzeitfehler verstehen wir eine aussergewöhnliche
Situtation, die normalerweise nicht auftreten sollte.
Beispiel:
Wir öffnen eine Datei, die eigentlich existieren muß.
DataInputStream stream = null;
try {
stream = new FileInputStream("dateiname");
}
catch(FileNotFoundException exception) {
... // Fehlerbehandlung, z.B. Abbruch mit Fehlermeldung
}
... // Lese aus der Datei etc.
Gegenbeispiel:
Wie lesen Instruktionen aus einer optionalen Datei.
Falls diese nicht existiert, liegt kein Fehler vor.
Programmierung
Laufzeitfehler
Beispiel: Menschen, die heiraten können.
class Mensch {
private int alter ;
private String name;
private Mensch partner ;
public Mensch(String name, int alter ) {
this.alter = alter ;
this.name = name;
this.partner = null;
}
public int getAlter () {return alter ; }
public String getName() {return name; }
public Mensch getPartner () {return partner ; }
Programmierung
Laufzeitfehler
Mit throw können wir ein Objekt vom Typ Exception werfen“,
”
welches von jemanden mit catch gefangen werden muß.
Annahme: Heiraten ist nur erlaubt, wenn das Durchschnittsalter
mindestens 18 beträgt.
public void heiraten(Mensch partner ) throws ZuJungException {
if(this.alter + partner .alter < 36) {
throw new ZuJungException();
}
this.partner = partner ;
}
Für das geworfene Objekt erstellen wir eine eigene Klasse:
class ZuJungException extends Exception {}
Programmierung
Laufzeitfehler
So können wir die Methode heiraten jetzt aufrufen:
public static void main(String args[ ]) {
Mensch karl = new Mensch("Karl Ranseier", 17);
Mensch sue = new Mensch("Sue Winter", 17);
try {
karl.heiraten(sue);
} catch (ZuJungException e) {
System.out.println("Zu jung. Sie haben nicht geheiratet.");
}
}
Programmierung
Laufzeitfehler
Beispiel
Wir berechnen die größte Zahl in einem Array.
static int maximum(int a[ ]) throws EmptyArrayException {
int n = a.length;
if(n == 0) {
throw new EmptyArrayException();
}
int max = a[0];
for(int i = 0; i < n; i ++) {
if(max < a[i]) {
max = a[i];
}
}
return max;
}
Neues Kapitel
1
Einführung in die objektorientierte Programmierung
2
Rekursion
3
Fehler finden und vermeiden
4
Objektorientiertes Design
5
Effiziente Programme
6
Nebenläufigkeit
7
Laufzeitfehler
8
Refactoring
9
Persistenz
10
Eingebettete Systeme
11
Funktionale Programmierung
12
Logische Programmierung
Programmierung
Refactoring
Refactoring
Ein (komplexes) Programm muß sich während seiner Lebenszeit
Erweiterungen und Veränderungen unterwerfen.
Oft zeigt sich bei einer Erweiterung, daß die derzeitige Struktur
ungeeignet ist.
Bestes Vorgehen:
1
Die Struktur des Programms verändern.
2
Die neue Funktionalität hinzufügen.
Den ersten Schritt nennen wir Refactoring.
Die Struktur des Programms wird verbessert, ohne Neues
hinzuzufügen.
Programmierung
Refactoring
Typische Refactoringschritte (es gibt viel mehr):
Doppelten Code durch einfachen ersetzen.
Zu lange Methoden durch kürzere ersetzen.
Lokale Variablen abschotten (getter und setter).
Funktionalität verallgemeinern für besseres Wiederbenutzen.
Neue Unterklassen einführen.
Unterklassen zusammenfassen.
Variablen oder Methoden in Ober- oder Unterklassen
verschieben.
Variablen oder Methoden in andere Klassen verschieben.
. . . und sogar einfache Umbenennungen in sinnvollere Namen.
Neues Kapitel
1
Einführung in die objektorientierte Programmierung
2
Rekursion
3
Fehler finden und vermeiden
4
Objektorientiertes Design
5
Effiziente Programme
6
Nebenläufigkeit
7
Laufzeitfehler
8
Refactoring
9
Persistenz
10
Eingebettete Systeme
11
Funktionale Programmierung
12
Logische Programmierung
Programmierung
Persistenz
Wie können wir Daten dauerhaft speichern?
Häufigste Methoden:
In einer Datei speichern.
In einer Datenbank speichern.
Lesen aus einer Datei:
double readDouble(String name) {
FileInputStream is = null;
DataInputStream dataStream = null;
double result;
try {
is = new FileInputStream(name);
dataStream = new DataInputStream(is);
result = dataStream.readDouble();
}
catch (Exception e) {e.printStackTrace(); }
return result;
}
Programmierung
Persistenz
Serializable
Implementiert eine Klasse das Interface Serializable, dann kann ihr
Inhalt direkt in eine Datei geschrieben und aus ihr gelesen werden.
class Person implements Serializable {
private String vorname;
private String nachname;
public Person(String vorname, String nachname) {
this.vorname = vorname;
this.nachname = nachname;
}
public String toString () {
return vorname + " " + nachname;
}
}
Programmierung
Persistenz
So schreiben wir eine Person in eine Datei:
Person karl = new Person("Karl", "Ranseier");
try {
FileOutputStream stream = new FileOutputStream("karl");
ObjectOutputStream objstream =
new ObjectOutputStream(stream);
objstream.writeObject(karl);
stream.close();
}
catch(IOException e) {
System.out.println("Oje: " + e);
}
Programmierung
Persistenz
So lesen wir eine Person:
Person unbekannt = null;
try {
FileInputStream stream = new FileInputStream("karl");
ObjectInputStream objstream =
new ObjectInputStream(stream);
unbekannt = (Person) objstream.readObject();
stream.close();
}
catch(ClassNotFoundException e) {
System.out.println("Falsche Klasse gelesen: " + e);
}
catch(IOException e) {
System.out.println("Oje: " + e);
}
System.out.println(unbekannt);
Neues Kapitel
1
Einführung in die objektorientierte Programmierung
2
Rekursion
3
Fehler finden und vermeiden
4
Objektorientiertes Design
5
Effiziente Programme
6
Nebenläufigkeit
7
Laufzeitfehler
8
Refactoring
9
Persistenz
10
Eingebettete Systeme
11
Funktionale Programmierung
12
Logische Programmierung
Neues Kapitel
1
Einführung in die objektorientierte Programmierung
2
Rekursion
3
Fehler finden und vermeiden
4
Objektorientiertes Design
5
Effiziente Programme
6
Nebenläufigkeit
7
Laufzeitfehler
8
Refactoring
9
Persistenz
10
Eingebettete Systeme
11
Funktionale Programmierung
12
Logische Programmierung
Überblick
11
Funktionale Programmierung
Allgemeines
Haskell
MapReduce
Programmierung
Funktionale Programmierung
Allgemeines
Funktionen
Es gibt verschiedene Notationen für Funktionen:
f : N → N, n 7→ n · n
f : N → N, f (n) = n · n
λn.n · n (bedeutet n 7→ n · n)
Die ersten beiden Definitionen geben den Definitions- und
Wertebereich an (die Signatur“ der Funktion).
”
Sie geben der Funktion auch den Namen f .
Die dritte Definition entstammt dem λ-Kalkül. Sie gibt der
Funktion keinen Namen.
Programmierung
Funktionale Programmierung
Allgemeines
Referentielle Integrität
class X {
int n = 0;
int f (int k) {
return k ∗ k;
}
int g (int k) {
n = n + 1;
return n ∗ k;
}
}
Was ergeben f (5) und g (5)?
Referentielle Integrität: Das Ergebnis eines Funktionsaufrufs hängt
nur von den Parametern ab.
Java hat sie nicht.
Programmierung
Funktionale Programmierung
Allgemeines
Referentielle Integrität
Die referentielle Integrität wird durch das Vorhandensein von
Variablen verhindert.
Auch Ein- und Ausgaben verhindern sie:
Die Funktion double readDouble() liest in Java eine Zahl aus einer
Datei.
Diese ist nicht immer die gleiche.
Ein weiteres Gegenbeispiel ist eine Methode, die uns eine
Zufallszahl gibt.
Programmierung
Funktionale Programmierung
Allgemeines
Referentielle Integrität
Referentielle Integrität hat den Vorteil, daß die Reihenfolge, in der
Parameter ausgewertet werden, keine Rolle spielt (Parallelität).
Beispiel:
f : n 7→ n · n
g : (n, m) 7→ n + m
Was ist g (f (3), g (f (2), f (1)))?
g (f (3), g (f (2), f (1))) = g (9, g (f (2), f (1)))
= g (9, g (4, f (1)))
= g (9, g (4, 1))
= g (9, 5)
= 14
Andere Auswertungsreihenfolgen ergeben auch 14.
Programmierung
Funktionale Programmierung
Allgemeines
Gilt dies auch für Java-Programme?
class Reihenfolge {
static int n = 0;
static int f (int x) {
n = n + x;
return n;
}
static int g (int x, int y ) {
return x − y ;
}
public static void main(String args[ ]) {
int result = g (f (20), f (50));
System.out.println(result);
}
}
Was ist die Ausgabe?
Programmierung
Funktionale Programmierung
Allgemeines
Aus der Java Language Specification:
Siehe hier: docs.oracle.com
The argument expressions, if any, are evaluated in order,
from left to right. If the evaluation of any argument
expression completes abruptly, then no part of any
argument expression to its right appears to have been
evaluated, and the method invocation completes abruptly
for the same reason. The result of evaluating the j’th
argument expression is the j’th argument value, for
1 ≤ j ≤ n. Evaluation then continues, using the argument
values, as described below.
Die Parameter werden von links nach rechts evaluiert.
Ein Programm, das sich darauf verläßt, ist nicht ideal.
Programmierung
Funktionale Programmierung
Allgemeines
Die Programmiersprache C gibt keine Garantie für die Reihenfolge,
in der Parameter einer Funktion evaluiert werden.
#include hstdio.hi
int n = 0;
int f (int x) {
n = n + x;
return n;
}
int g (int x, int y ) {
return x − y ;
}
void main() {
printf ("%d\n", g (f (20), f (50)));
}
Ergebnis unterscheidet sich für SUN Workstations
und Intel PCs.
Programmierung
Funktionale Programmierung
Allgemeines
Call by Value
Diese Art der Evaluierung nennen wir Call by Value:
g (f (3), g (f (2), f (1))) = g (9, g (f (2), f (1)))
= g (9, g (4, f (1)))
= g (9, g (4, 1))
= g (9, 5)
= 14
Bevor eine Funktion evaluiert wird, werden rekursiv alle ihre
Parameter evaluiert.
Normale Evaluierung: Call by Value mit Reihenfolge von links nach
rechts.
Programmierung
Funktionale Programmierung
Allgemeines
Call by Value
Call by Value kann ineffizient sein:
int h(int x, int y ) {
if(x < 0) {
return 0;
}
else {
return x ∗ y ;
}
}
...
h(−3, sehrTeuer (25));
Um h(−3, x) zu berechnen, ist der Wert von x irrelevant.
Trotztdem wird sehrTeuer (25) aufgerufen.
Programmierung
Funktionale Programmierung
Allgemeines
Wir können eine Funktion auch so weit wie möglich außen
evaluieren:
g (f (3), g (f (2), f (1))) = f (3) + g (f (2), f (1)))
= (3 ∗ 3) + g (f (2), f (1))
= 9 + g (f (2), f (1))
= 9 + (f (2) + f (1))
= 9 + ((2 ∗ 2) + f (1))
= 9 + (4 + f (1))
= 9 + (4 + (1 ∗ 1))
= 9 + (4 + 1)
=9+5
= 14
Dies nennen wir Call by Name.
Hat dies irgendeinen Vorteil???
Programmierung
Funktionale Programmierung
Allgemeines
Call by Name
Evaluierung“ von h mittels Call by Name:
”
h(−3, sehrTeuer (25))
if(−3 < 0) {return 0; } else {return (−3) ∗ sehrTeuer (25)};
if(true) {return 0; } else {return (−3) ∗ sehrTeuer (25); }
return 0;
0
Es stellt sich heraus, daß sehrTeuer (25) gar nicht evaluiert wird.
Call by Name und Call by Value sind unter referentieller Integrität
fast äquivalent.
Wenn sehrTeuer (25) nicht definiert ist (Fehler oder Endlosschleife)
dann sind die Ergebnisse verschieden.
Programmierung
Funktionale Programmierung
Allgemeines
list(n) = n : list(n + 1)
Es sei [ ] eine leere Liste und x : l möge die Liste sein, deren erstes
Element x ist gefolgt von der Liste l.
Dann ist list(4) = [4, 5, 6, 7, 8, 9, . . .] eine unendliche Liste!
(
x
falls n = 1,
take(n, x : rest) =
take(n − 1, rest) sonst.
take(n, l) gibt uns das nte Element der Liste l.
Beispiele:
take(3, [1, 4, 9, 16, 25]) = 9
take(6, [1, 2, 3, 4, 5, 6, 7, . . .]) = 6
take(3, list(4)) = 6
Programmierung
Funktionale Programmierung
Allgemeines
Lazy Evaluation
take(3, list(4)) = take(3, 4 : list(4 + 1))
= take(3 − 1, list(4 + 1))
= take(2, list(4 + 1))
= take(2, (4 + 1) : list((4 + 1) + 1))
= take(2 − 1, list((4 + 1) + 1))
= take(1, list((4 + 1) + 1))
= take(1, ((4 + 1) + 1) : list(((4 + 1) + 1) + 1)
= (4 + 1) + 1
=5+1
=6
Wir nennen dies auch Lazy Evaluation, denn es wird
nur das evaluiert, was wir wirklich brauchen.
Überblick
11
Funktionale Programmierung
Allgemeines
Haskell
MapReduce
Programmierung
Funktionale Programmierung
Haskell
Funktionale Programmierung
Vereinfacht gesagt:
Wir definieren einige Funktionen.
Wir lassen eine von ihnen z.B. mit Lazy Evaluation auswerten und
erhalten das Ergebnis.
Das ist funktionale Programmierung.
Ein Programm besteht aus Funktionsdeklarationen:
list(n) = n : list(n + 1)
take(n, x : rest) =
if n = 1
then x
else take(n − 1, rest)
Programmierung
Funktionale Programmierung
Haskell
Haskell
Wir verwenden die funktionale Programmiersprache Haskell.
Ein Programm besteht aus einer Folge von Deklarationen, die
Funktionen definieren.
factorial(1) = 1
factorial(n) = n ∗ factorial(n − 1)
Gibt es mehrere Deklarationen für factorial, dann wird die erste
verwendet, welche paßt“.
”
Für factorial(1) wird die erste Zeile verwendet.
Für factorial(5) wird die zweite Zeile verwendet.
Programmierung
Funktionale Programmierung
Haskell
Evaluierung
factorial(1) = 1
factorial(n) = n ∗ factorial(n − 1)
Wie wird factorial(3) evaluiert?
factorial(3)
3 ∗ factorial(3 − 1)
3 ∗ factorial(2)
3 ∗ (2 ∗ factorial(2 − 1))
3 ∗ (2 ∗ factorial(1))
3 ∗ (2 ∗ 1)
3∗2
6
Es wird die erste passende linke Seite, durch die rechte Seite
ersetzt.
Dabei passt n zu jeder natürlichen Zahl.
Programmierung
Funktionale Programmierung
Haskell
Typdeklarationen
Eine anständige“ Definition einer Funktion umfaßt auch ihren
”
Typ:
factorial : N → N, factorial(n) = ...
In Haskell können wir neben Funktionsdeklarationen auch
Typdeklarationen erstellen:
factorial :: Integer →Integer
factorial(1) = 1
factorial(n) = n ∗ factorial(n − 1)
Typdeklarationen sind nicht immer nötig, aber wir sollten sie
immer erstellen. Dadurch wird die Fehlersuche viel leichter.
Programmierung
Funktionale Programmierung
Haskell
Typdeklarationen
Es gibt in Haskell bereits primitive Typen:
Integer : ganze Zahlen, z.B. 1289736781236
Int: ganze Zahlen mit Computerarithmetik, z.B. 123
Double: Fließkommazahlen, z.B. 3.14159
String : Zeichenketten, z.B. "Hello, World"
Char : ein Zeichen, z.B. ’A’
Wir können auch neue Typen deklarieren.
Programmierung
Funktionale Programmierung
Haskell
Notation
Hat eine Funktion ein Argument muß dieses nicht geklammert
werden.
Wir schreiben also lieber
factorial 1 = 1
factorial n = n ∗ factorial (n − 1)
Die (n − 1) muß geklammert sein, weil sonst implizit so
geklammert würde:
(n ∗ (factorial(n))) − 1
Programmierung
Funktionale Programmierung
Haskell
Tupel
Es gibt in Haskell Tupel:
Beispiele: (1, 2), ("John", "Doe"), (17, "plus", 4)
Eine Funktion hat streng genommen immer nur ein Argument.
Mehrstellige Funktionen können durch Tupel simuliert“ werden:
”
sum1 :: (Integer , Integer )→Integer
sum1 (x, y ) = x + y
Man bevorzugt aber solche Funktionen:
sum2 :: Integer →Integer →Integer
sum2 x y = x + y
Technisch gesehen ist sum2 eine Funktion, die eine Zahl auf eine
andere Funktion abbildet, die dann eine Zahl auf eine Zahl abbildet.
So werden aber praktisch mehrstellige Funktionen umgesetzt.
Programmierung
Funktionale Programmierung
Haskell
Was steckt hinter sum2 ?
sum2 :: Integer →Integer →Integer
sum2 x y = x + y
Tatsächlich ist sum2 y eine Funktion.
Insbesondere ist sum2 5 die Funktion x 7→ x + 5.
Wir können (sum2 5)(3) schreiben.
Die Deklaration f = sum2 5 ist auch möglich.
So können wir eine Exponentialfunktion definieren:
power3 :: Integer →Integer →Integer
power3 n 0 = 1
power3 n m = n ∗ power3 n (m − 1)
Programmierung
Funktionale Programmierung
Haskell
Folgendes Programm ist ineffizient, weil fib(x − 1) und fib(x − 2)
beide aufgerufen werden.
fib
fib
fib
fib
::
0
1
x
Integer →Integer
= 0
= 1
= fib (x − 1) + fib (x − 2)
Die Laufzeit ist exponentiell.
Die Ausführung könnte durch Caching von bereits berechneten fibs
automatisch optimiert werden.
Dies macht der Compiler aber leider nicht.
Programmierung
Funktionale Programmierung
Haskell
In funktionalen Sprachen können wir nicht einfach Werte
speichern“.
”
Um Fn zu berechnen, benötigen wir Fn−1 und Fn−2 .
Lösung: Statt nur Fn berechnen wir das Paar Pn = (Fn , Fn+1 ).
Jetzt läßt sich Pn+1 aus Pn allein berechnen.
fibpair :: Integer → (Integer , Integer )
fibpair 0 = (0, 1)
fibpair n = (b, a + b)
where (a, b) = fibpair (n − 1)
Durch eine where-Anweisung können wir verhindern,
fibpair (n − 1) zweimal aufzurufen. Durch where-Anweisungen
können wir Zwischenergebnissen einen Namen geben.
Programmierung
Funktionale Programmierung
Haskell
Listen
Listen sind in funktionalen Sprachen allgemein eine wichtige
Datenstruktur.
In Haskell sind Listen vordefiniert:
1
2
[ ] ist die leere Liste.
x : xs ist die Liste deren Kopf aus x besteht, gefolgt von den
Elementen aus der Liste xs.
Beispiel: 1 : 2 : 3 : 4 : 5 : [ ]
Abkürzung:
[a, b, c, d] ist eine Abkürzung für a : b : c : d : [ ].
Typnamen:
[Integer ] ist der Typ Liste von Integer s“.
Programmierung
Funktionale Programmierung
Haskell
Arbeiten mit Listen
So verdoppeln wir die Einträge einer Liste:
verdoppeln :: [Integer ]→[Integer ]
verdoppeln [ ] = [ ]
verdoppeln (x : xs) = 2 ∗ x : verdoppeln xs
So können wir die Reihenfolge umdrehen:
umdrehen :: [Integer ]→[Integer ]
umdrehen [ ] = [ ]
umdrehen (x : xs) = (umdrehen xs)
Der Operator
++
++
[x]
konkateniert zwei Listen.
Programmierung
Funktionale Programmierung
Haskell
Beispiel: Mergesort
Ein einfaches Sortierverfahren heißt Mergesort.
Es funktioniert so:
1
Teile die Liste in zwei etwa gleich lange Listen l1 und l2.
2
Sortiere l1 und l2.
3
Mische l1 und l2 in eine sortierte Liste.
Wir implementieren daher drei Funktionen teile, mische und
mergesort.
Programmierung
Funktionale Programmierung
Haskell
Die folgende Funktion verteilt Elemente einer Liste immer
abwechselnd auf zwei neue Listen.
teile :: [Integer ]→([Integer ], [Integer ])
teile [ ] = ([ ], [ ])
teile (x : [ ]) = ([x], [ ])
teile (x : y : [ ]) = ([x], [y ])
teile (x : y : xs) = (x : r1 , y : r2 )
where (r1 , r2 ) = teile xs
Programmierung
Funktionale Programmierung
Haskell
mische :: [Integer ]→[Integer ]→[Integer ]
mische [ ] l = l
mische l [ ] = l
mische (x : xs) (y : ys) =
if x < y
then x : mische xs (y : ys)
else y : mische (x : xs) ys
mergesort :: [Integer ]→[Integer ]
mergesort [ ] = [ ]
mergesort (x : [ ]) = [x]
mergesort l = mische (mergesort l1 ) (mergesort l2 )
where (l1 , l2 ) = teile l
Programmierung
Funktionale Programmierung
Haskell
Beispiel: Partition-Exchange-Sort
Ein weiteres schnelles Sortierverfahren heißt
Partition-Exchange-Sort oder Quicksort.
Es geht so vor:
1
2
3
4
Wähle ein Pivot-Element p (z.B. das erste Element im Array).
Partitioniere das Array in zwei Arrays. Das erste enthält alles
was kleiner als p ist, das zweite den Rest.
Sortiere beide Arrays rekursive.
Packe p in die Mitte.
Bei der imperativen Implementierung mit Arrays geschieht das
Partitionieren mithilfe von Vertauschungen, daher der Name
Partition-Exchange.
Das Vergleichen mit dem Pivot-Element und weitersuchen benötigt
typischerweise nur vier Maschineninstruktionen. Daher der Name
Quicksort.
Wir implementieren dieses Verfahren mit Listen.
Programmierung
Funktionale Programmierung
Haskell
Hierzu implementieren wir die Funktionen psort und partition,
welches ein Pivot-Element und eine Liste erhält.
psort :: [Integer ]→[Integer ]
psort [ ] = [ ]
psort (x : xs) = (psort l1 ) ++ [x] ++ (psort l2 )
where (l1 , l2 ) = partition x xs
partition :: Integer →[Integer ]→([Integer ], [Integer ])
partition x [ ] = ([ ], [ ])
partition x (y : ys) =
if x < y
then (l1 , y : l2 )
else (y : l1 , l2 )
where (l1 , l2 ) = partition x ys
Programmierung
Funktionale Programmierung
Haskell
Typparameter
Betrachten wir diese Funktion:
zweites :: [Integer ]→Integer
zweites (x : y : ys) = y
Sie findet das zweite Element in einer Liste von Integer .
Die Funktionalität sollte aber auch für beliebige Listen
funktionieren.
Wir können Typvariablen in einer Signatur verwenden, um solch
allgemeine Funktionen zu definieren. Dies ähnelt den generischen
Typen von Java.
zweites :: [a]→a
zweites (x : y : ys) = y
Programmierung
Funktionale Programmierung
Haskell
Eigene Datentypen
So können wir einen einfachen Datentyp Note definieren.
Der Typ Note kann nur bestimmte Werte annehmen.
data Note =
eineschlechter
eineschlechter
eineschlechter
Gut | Schlecht | Mies deriving Show
:: Note → Note
Gut = Schlecht
Schlecht = Mies
Wir können eigene Datentypen ebenso wie Integer etc. verwenden.
deriving Show sagt, daß Note ein Untertyp von Show ist.
Dadurch können seine Werte einfach angezeigt werden.
Programmierung
Funktionale Programmierung
Haskell
Eigene Datentypen
Ein Datentyp kann von einem anderen Typ abhängen.
So definieren wir eine eigene Liste von a’s, wobei a wieder eine
Typvariable ist. Wir können also auch so einen eigenen Listentyp
für Integer -Listen definieren, aber auch für beliebige andere.
data List a = Nil | Cons a (List a) deriving Show
kopf :: List a → a
kopf (Cons x y ) = x
schwanz :: List a → List a
schwanz (Cons x xs) = xs
append :: List a → List a → List a
append Nil l = l
append (Cons x xs) l = Cons x (append xs l)
laenge :: List a → Integer
laenge Nil = 0
laenge (Cons xs) = 1 + (laenge xs)
Programmierung
Funktionale Programmierung
Haskell
Eigene Datentypen
Hier ist ein Beispiel für Binärbäume, die Werte des Typs a in ihren
Blättern besitzen.
data Bintree a = Leaf a | Node (Bintree a) (Bintree a)
deriving Show
size :: Bintree a → Integer
size (Leaf x) = 1
size (Node l r ) = (size l) + (size r )
data OrdBintree a = Ext | In a (OrdBintree a) (OrdBintree a)
deriving Show
insert :: Ord a => a→OrdBintree a→OrdBintree a
insert x Ext = In x Ext Ext
insert x (In y yl yr ) =
if x < y
then In y (insert x yl) yr
else In y yl (insert x yr )
Programmierung
Funktionale Programmierung
Haskell
Geordnete Binärbäume. Die linken Nachkommen eines Knotens
sind stets kleiner, die rechten stets größer (oder gleich).
data OrdBintree a = Ext | In a (OrdBintree a) (OrdBintree a)
deriving Show
insert :: Ord a => a→OrdBintree a→OrdBintree a
insert x Ext = In x Ext Ext
insert x (In y yl yr ) =
if x < y
then In y (insert x yl) yr
else In y yl (insert x yr )
listtotree :: Ord a => [a]→OrdBintree a
listtotree [ ] = Ext
listtotree (x : xs) = insert x (listtotree xs)
Durch Ord a => a wird ein beliebiger Typ bezeichnet, der aber
geordnet ist. Sonst kann < nicht verwendet werden.
Programmierung
Funktionale Programmierung
Haskell
Funktionen höherer Ordnung
Eine besonders mächtiges Werkzeug der funktionalen
Programmierung sind Funktionen, die Funktionen auf Funktionen
abbilden. Wir nennen diese auch Funktionen höhrerer Ordnung.
Einfaches Beispiel:
Wandle eine Funktion f : A × B → C in eine Funktion
g : B × A → C um, so daß g (x, y ) = f (y , x) gilt.
umdrehen :: (a→b→c)→(b→a→c)
umdrehen f x y = f y x
loesche :: Integer →[Integer ]→[Integer ]
loesche x [ ] = [ ]
loesche x (y : ys) =
if x == y
then loesche x ys
else y : loesche x ys
uloesche = umdrehen loesche
Programmierung
Funktionale Programmierung
Haskell
22
Mit toweroftwo 4 können wir 22
berechnen.
zweimal :: (a→a)→(a→a)
zweimal f x = f (f x)
kmal :: Integer →(a→a)→(a→a)
kmal 0 f x = x
kmal n f x = f (kmal (n − 1) f x)
zweierpotenz :: Integer →Integer
zweierpotenz k = kmal k (∗2) 1
toweroftwo :: Integer →Integer
toweroftwo k = kmal k zweierpotenz 1
Programmierung
Funktionale Programmierung
Haskell
Mit nach können wir aus f und g ihre Komposition f ◦ g
berechnen.
nach :: (b→c)→(a→b)→(a→c)
nach f g = \x → f (g x)
Beispiel:
f = nach (∗2) (+4) definiert die Funktion
f : n 7→ 2(n + 4).
Anstatt f = nach g h können wir auch f = g ‘nach‘ h
schreiben.
Eine solche Funktion ist schon vordefiniert: f = g . h
Programmierung
Funktionale Programmierung
Haskell
lmap wendet eine Funktion auf alle Elemente einer Liste an.
lmap :: (a→b)→[a]→[b]
lmap f [ ] = [ ]
lmap f (x : xs) = f x : lmap f xs
Beispiel:
lmap (∗2) [1, 2, 3, 4, 5, 6]
lmap head [[1, 2], [3, 4], [5, 6]]
doppeln = lmap (∗2)
Auch lmap ist schon vordefiniert und heißt einfach map.
Programmierung
Funktionale Programmierung
Haskell
Lambda-Ausdrücke
Wir können eine Funktion n 7→ 5n definieren, ohne ihr einen
Namen zu geben:
\n→5 ∗ n
Dies ist an den λ-Kalkül angelehnt. Dort schreibt man λn.5n.
Beispiel:
map (\x→2 ∗ x + 5) [1, 2, 3, 4, 5, 6]
umgekehrt f = \x y →f y x
Frage:
Was ergibt (\x y →(/) y x) 2 3?
Programmierung
Funktionale Programmierung
Haskell
Fragen
f [] = []
f (x : y : ys) = x ∗ y : f ys
f (x : xs) = f (x : x : xs)
Wozu evaluiert f [1, 2, 3] (Abkürzung für f (1 : 2 : 3 : [ ]))?
Wozu evaluiert [f [ ], f [ ]]?
Programmierung
Funktionale Programmierung
Haskell
Weiteres Beispiel:
f []y = []
f (x : xs) y = x : f xs y
g [x] = x : g [x]
Wozu evaluiert f [5] (g [3])?
Programmierung
Funktionale Programmierung
Haskell
Evaluierung von links nach rechts
f []x = []
g = []
h = 1 : h
Wozu evaluiert der Ausdruck f g h?
h läßt sich nicht evaluieren. Es ergäbe sich die unendliche
Liste [1, 1, 1, ...].
f g h läßt sich mit keiner Definition matchen.
Daher wird erst einmal g einmal evaluiert.
Daraus ergibt sich der Ausdruck f [ ] h, welcher jetzt zur
Definition von f paßt.
Jetzt erhalten wir die rechte Seite [ ] als Ergebnis.
Es ist wichtig, daß Hakell von
links nach rechts evaluiert.
Programmierung
Funktionale Programmierung
Haskell
Funktionales Morsedekodieren
Zum Vergleich mit Java dekodieren wir denselben Morsecode nun
mit Haskell.
Dies ist eine Aufgabe, die sich gut mit einem funktionalem
Programm lösen läßt.
Wir gehen ähnlich vor:
main = do
contents ← readFile "audio.txt"
(print . decode2 . decode1 . read) contents
Wir dekodieren in zwei Stufen:
decode1 = (cummulate 0) . threshold . (average 200) . absolute
decode2 = demorse . (crack "") . (foldl (++) "") . (map symbolize)
Programmierung
Funktionale Programmierung
Haskell
Absolute
Wir berechnen die Absolutwerte einer Liste. Am einfachsten
können wir dies mit map erledigen und einer Funktion, die den
Absolutwert eines Double berechnet.
absolute :: [Double]→[Double]
absolute = map (\x→if x > 0 then x else − x)
Ohne Funktionen höherer Ordnung würden wir alternativ schreiben:
absolute [ ] = [ ]
absolute (x : xs) = absx : absolute xs
where absx = if x > 0 then x else − x
Programmierung
Funktionale Programmierung
Haskell
Die Folgende Funktion berechnet die Durschnittswerte von allen
Unterlisten einer gegebenen Länge.
Zum Beispiel: average 2 [1, 2, 3, 4, 5, 6] soll das Ergebnis
[1.5, 2.5, 3.5, 4.5, 5.5] liefern.
average :: Double → [Double] → [Double]
average k l = map (\x→x/k) (listdiff (chop (k − 1) ll) (0 : ll))
where ll = prefixsum l 0
Wenn die Fensterlänge k ist, dann berechnen wir zuerst die
Präfixsummen p, von diesen eine neue Liste, von der nur die ersten
k − 1 Elemente fehlen und ziehen von ihr elementweise 0 : p ab.
Am Ende muß noch durch k geteilt werden.
Beispiel: p = [1, 3, 6, 10, 15, 21]
[3, 6, 10, 15, 21] minus [0, 1, 3, 6, 10, 15, 21]
ergibt [3, 5, 7, 9, 11].
Programmierung
Funktionale Programmierung
Haskell
Die Hilfsfunktionen prefixsum, chop und listdiff sind einfach:
prefixsum :: [Double]→Double→[Double]
prefixsum [ ] a = [ ]
prefixsum (x : xs) a = (x + a) : prefixsum xs (x + a)
chop :: Double→[Double]→[Double]
chop 0 l = l
chop k (x : xs) = chop (k − 1) xs
listdiff :: [Double] → [Double] → [Double]
listdiff [ ] l = [ ]
listdiff (x : xs) (y : ys) = x − y : (listdiff xs ys)
In prefixsum nutzen wir einen Akkumulator, um lineare Laufzeit zu
erhalten.
Programmierung
Funktionale Programmierung
Haskell
threshold :: [Double]→[Int]
threshold [ ] = [ ]
threshold (x : xs) = (if x > 0.2 then 1 else − 1) : (threshold xs)
Der Kürze wegen übersetzt threshold alle Werte in einer Liste zu
−1 oder 1, je nachdem ob sie größer als 0.2 sind.
In Java hatten wir einen Clustering-Algorithmus verwendet, um
diesen Schwellwert automatisch zu bestimmen. Darauf verzichten
wir jetzt.
Programmierung
Funktionale Programmierung
Haskell
cummulate :: Int→[Int]→[Int]
cummulate 0 (x : xs) = cummulate x xs
cummulate n [ ] = [n]
cummulate n (x : xs) =
if (n > 0) == (x > 0)
then cummulate (n + x) xs
else n : cummulate 0 (x : xs)
Die Funktion cummulate berechnet die Länge von Blöcken, die nur
aus −1 oder 1 bestehen.
Beispiel:
cummulate [1, 1, 1, −1, −1, 1, 1, −1, −1, −1, −1] = [3, −2, 2, −4]
Programmierung
Funktionale Programmierung
Haskell
symbolize :: Int→String
symbolize x
| x < − 20000 = " "
| x < 0 = ""
| x > 2000 = "-"
| otherwise = "."
In symbolize verwenden wir guarded commands, welche bisher
nicht vorkamen. Mit ihnen lassen sich gut viele verschiedene Fälle
behandeln.
Wir übersetzen kurze Signale in einen Punkte und lange in einen
Strich. Lange Pausen werden zu einem Leerzeichen übersetzt.
Programmierung
Funktionale Programmierung
Haskell
crack :: String →String →[String ]
crack a [ ] = [a]
crack a (x : xs) =
if x == ’ ’
then a : crack "" xs
else crack (a++[x]) xs
Diese Funktion bricht einen String in eine Liste von Teilstrings auf,
wobei die Teilstrings von Leerzeichen getrennt wurden.
Beispiel:
crack "" "-- . --- .-." = ["--", ".", "---", ".-."]
Programmierung
Funktionale Programmierung
Haskell
demorse :: [String ]→String
demorse = map (\s → zeichen s tab "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
demorse übersetzt eine Liste von Morsecodes in ihre jeweiligen
Klartextsymbole.
Die Hilfsfunktion zeichen s l1 l2 findet einen String s in der Liste
l1 auf Position p und gibt dann das pte Zeichen aus l2 aus.
zeichen :: String →[String ]→String →Char
zeichen [ ] = ’?’
zeichen z (x : xs) (y : ys) =
if z == x
then y
else zeichen z xs ys
tab = [".-", "-...", "-.-.", "-..", ".", "..-.", "--.",
"....", "..", ".---", "-.-", ".-..", "--", "-.",
"---", ".--.", "--.-", ".-.", "...", "-", "..-",
"...-", ".--", "-..-", "-.--", "--.."]
Überblick
11
Funktionale Programmierung
Allgemeines
Haskell
MapReduce
Neues Kapitel
1
Einführung in die objektorientierte Programmierung
2
Rekursion
3
Fehler finden und vermeiden
4
Objektorientiertes Design
5
Effiziente Programme
6
Nebenläufigkeit
7
Laufzeitfehler
8
Refactoring
9
Persistenz
10
Eingebettete Systeme
11
Funktionale Programmierung
12
Logische Programmierung
Überblick
12
Logische Programmierung
Prolog
Syntax und Semantik
Programmierung
Logische Programmierung
Prolog
Logische Programmierung
Eine Weise der logischen Programmierung:
Definition von Fakten
Definition von Regeln
Wir definieren eine Welt“ mittels Fakten und Regeln.
”
Dann stellen wir dem System Fragen über diese Welt.
Das System überlegt selbst, wie es diese Fragen beantworten kann.
Programmierung
Logische Programmierung
Prolog
Familienbeziehungen
Einige einfache Fakten und eine Schlußregel:
parent(peter , petik).
parent(peter , kenny ).
parent(dieter , peter ).
parent(dieter , martin).
parent(martin, martinek).
grand(X , Y ) :− parent(X , Z ), parent(Z , Y ).
Die letzte Regel besagt, daß wenn X Elternteil von Z und Z von Y
ist, dann ist X ein Großelternteil von Y .
:− ist ein stylisierter Pfeil nach links. Die linke Seite folgt“ von
”
der rechten.
Variablen werden groß geschrieben.
Überblick
12
Logische Programmierung
Prolog
Syntax und Semantik
Programmierung
Logische Programmierung
Syntax und Semantik
Syntax von Prolog
Atome sind
1
2
3
String aus Buchstaben, Ziffern und , angefangen mit
Kleinbuchstaben.
Beispiele: petik, xB7 , a HOPP
String aus Sonderzeichen.
Beispiele: ===> , .−−., :=:
String in einfachen Anführungszeichen.
Beispiele: ’Petik’, ’Hello, World’
Programmierung
Logische Programmierung
Syntax und Semantik
Syntax von Prolog
Variablen sind Strings aus Buchstaben, Ziffern und , angefangen
mit einem Großbuchstaben oder
Beispiele: X , X7 , X 7 , anton
Es gibt außerdem Zahlen.
Programmierung
Logische Programmierung
Syntax und Semantik
Strukturen
Strukturen sind Objekte, die aus mehreren Komponenten bestehen.
Beispiele:
student(karl, ranseier , 123456)
strecke(punkt(0, 0), punkt(5, 3))
Der Funktor einer Struktur (z.B. student) ist ein Atom, die
Komponenten sind Atome, Strukturen, oder Variablen.
Programmierung
Logische Programmierung
Syntax und Semantik
Wir können Strukturen als Bäume darstellen:
student(karl, ranseier , 123456)
student
ranseier
karl
123456
strecke(punkt(0, 0), punkt(5, 3))
strecke
punkt
0
0
punkt
5
3
Programmierung
Logische Programmierung
Syntax und Semantik
Unifikation oder Matching
Sind S und T zwei Terme, so ist S = T eine Anfrage, sie zu
unifizieren:
Sind S und T Konstanten (Atome oder Zahlen), dann
unifizieren sie genau dann, wenn sie identisch sind.
Ist S eine Variable, dann unifizieren S und T und S wird zu T
instantiiert: Ab jetzt wird S stets durch T ersetzt:
Ist T eine Variable dann ebenso.
Sind S und T Strukturen, dann unifizieren sie, falls die
Funktionen identisch sind und alle Komponenten unifizieren.
Programmierung
Logische Programmierung
Syntax und Semantik
Unifikation – Beispiele
X = adam
√
punkt(X , 7) = punkt(5, Y )
punkt(X , 7) = punkt(7, X )
√
√
punkt(X , 7) = punkt(5, X ) –
√
X = punkt(Y , 3)
strecke(X , Y ) = strecke(Z , punkt(3, 5))
strecke(X , Y ) = X ?
Welche Instantiierungen finden statt?
√
Programmierung
Logische Programmierung
Syntax und Semantik
Backtracking
Prolog versucht mit Backtracking, Anfragen mit seinen gespeicherten
Deklarationen zu unifizieren. Scheitert dies an einer Stelle, dann werden
die letzten Instantiierungen rückgängig gemacht und bei der nächsten
Deklaration weitergemacht.
punkt(3, 5).
punkt(1, 2).
punkt(2, 5).
nebeneinander (punkt(X1 , Y ), punkt(X2 , Y )).
Anfrage punkt(X , Y ), punkt(Y , Z )
Zuerst wird punkt(X , Y ) mit der ersten Zeile unifiziert. Dann wird
versucht punkt(Y , Z ) ebenfalls mit der ersten Zeile zu unifizieren, was
scheitert, gefolgt von den anderen Zeilen. Schließlich scheitert
punkt(Y , Z ) endgültig. Jetzt wird die Instantiierung von X , Y
zurückgenommen und punkt(X , Y ) mit der zweiten Zeile unifiziert.
Schließlich unifiziert punkt(Y , Z ) mit der letzten Zeile.
Programmierung
Logische Programmierung
Syntax und Semantik
Backtracking
punkt(3, 5).
punkt(1, 2).
punkt(2, 5).
nebeneinander (punkt(X1 , Y ), punkt(X2 , Y )).
Bei der Anfrage nebeneinander (A, B) sind die ersten drei
Deklarationen belanglos.
nebeneinander (A, B) wird mit der vierten Zeile erfolgreich
unifiziert.
Dabei wird A = punkt(X1 , Y ) und B = punkt(X2 , Y ) instantiiert.
Programmierung
Logische Programmierung
Syntax und Semantik
Backtracking
punkt(3, 5).
punkt(1, 2).
punkt(2, 5).
nebeneinander (punkt(X1 , Y ), punkt(X2 , Y )).
Die Anfrage
nebeneinander (punkt(A, B), punkt(C , D)), punkt(A, B), punkt(C , D)
liefert dagegen (A, B) = (3, 5) und (C , D) = (3, 5) als erste
Lösung.
nebeneinander (punkt(A, B), punkt(C , D)) wird mit der letzten
Zeile unifiziert, denn die Funktoren stimmen dort überein.
Was für Instantiierungen werden gemacht? Nur B = D.
Jetzt wird punkt(A, B) mit der ersten Zeile unifiziert und
A = 3, B = 5 instantiiert. Dann wird versucht punkt(C , 5) mit
einer Deklaration zu unifizieren.
Programmierung
Logische Programmierung
Syntax und Semantik
Listen
In Prolog wird [X |Y ] für eine Liste mit Kopf X und Schwanz Y
geschrieben.
Die leere Liste ist [ ].
Wir können die Abkürzung [1, 2, 3, 4, 5] benutzen. Beispiel:
removed(X , [X |Y ], Y ).
removed(X , [X1 |Y ], [X1 |Y ohne X ]) :−
X \ = X1 ,
removed(X , Y , Y ohne X ).
perm([ ], [ ]).
perm([X |Y ], Z ) :− member (X , Z ), removed(X , Z , Z1 ), perm(Y , Z1 ).
aufsteigend([ ]).
aufsteigend([ ]).
aufsteigend([X |[Y |Z ]]) :− X =< Y , aufsteigend([Y |Z ]).
sortiert(X , Y ) :− perm(X , Y ), aufsteigend(X ).
\ = ist ungleich, =< und >= sind in Prolog ≤ und ≥.
Programmierung
Logische Programmierung
Syntax und Semantik
Beispiel
Es gibt das bekannte Rätsel:
SEND + MORE = MONEY
Jeder Buchstabe muß durch eine Ziffer ersetzt werden, so daß die
Rechnung stimmt.
Verschiedene Buchstaben müssen dabei verschiedene Ziffern
erhalten.
Wie lösen wir solche Rätsel in Prolog?
Nehmen wir uns hier eine weniger bekannte Frage vor:
FH · RWTH = AACHEN
Programmierung
Logische Programmierung
Syntax und Semantik
Beispiel
Wir können das Rätsel lösen, indem wir verlangen, daß
[R, W , T , H, F , A, C , E , N, ] eine Permutation der Ziffern 0, . . . , 9
ist und die Rechnung stimmt.
Dafür verwenden wir eine Relation [num(N, L)], die aussagt, daß
die Zahl N aus den Ziffern in der Liste L besteht.
num(0, [ ]).
num(N, [X |XS]) :− num(N1 , XS), N is X + 10 ∗ N1 .
loesung (FH, RWTH) :−
perm([R, W , T , H, F , A, C , E , N, ], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
num(FH, [H, F ]),
num(RWTH, [H, T , W , R]),
num(AACHEN, [N, E , H, C , A, A]),
AACHEN is FH ∗ RWTH.
Programmierung
Logische Programmierung
Syntax und Semantik
Beispiel – Umdrehen einer Liste
Drehen wir eine Liste naiv um, ist die Laufzeit wieder quadratisch.
Die schnelle Art eine Liste umzudrehen, verwendet ähnlich wie in
Haskell einen Akkumulator.
Wir definieren eine Regel, die aussagt, daß ein Element
”
hinübergeschoben wurde“.
hinueber ([ ], L, L).
hinueber ([E |XS], YS, L) :− hinueber (XS, [E |YS], L).
umgedreht(A, B) :− hinueber (A, [ ], B).
Was ergiebt die Anfrage umgedreht(X , X )?
Programmierung
Logische Programmierung
Syntax und Semantik
Löschen und Einfügen in eine Liste
Löschen des ersten Elements einer Liste:
loesche erstes(X , [X |R], R).
loesche erstes(X , [Y |R], [Y |RR]) :− X \ = Y , loesche erstes(X , R, RR).
Löschen eines Elements:
loesche(X , [X |R], R).
loesche(X , [Y |R], [Y |RR]) :− loesche(X , R, RR).
Kann man so auch Elemente einfügen?
Programmierung
Logische Programmierung
Syntax und Semantik
Das Fährproblem
Ein Mann hat ein Schaf, einen Kohl und einen Wolf.
Er möchte sie auf die andere Flußseite bringen.
Er darf das Schaf und den Wolf nicht allein lassen.
Er darf auch das Schaf und den Kohl nicht allein lassen.
Im Boot ist nur Platz für den Mann und einen der drei Dinge.
Wie kann er das schaffen?
Programmierung
Logische Programmierung
Syntax und Semantik
Das Fährproblem
tausch([K , W , S, o], [K , W , S, w ]).
tausch([o, W , S, o], [w , W , S, w ]).
tausch([K , o, S, o], [K , w , S, w ]).
tausch([K , W , o, o], [K , W , w , w ]).
fahrt(X , Y ) :− tausch(X , Y ).
fahrt(X , Y ) :− tausch(Y , X ).
sicher (S) :− kohl sicher (S), schaf sicher (S).
kohl sicher ([K , , , K ]).
kohl sicher ([w , , o, ]).
kohl sicher ([o, , w , ]).
schaf sicher ([ , , S, S]).
schaf sicher ([ , w , o, ]).
schaf sicher ([ , o, w , ]).
erreichbar (0, [o, o, o, o], [ ]).
erreichbar (N, T , [S|L]) :− N > 0, fahrt(S, T ), sicher (T ),
N1 is N − 1, erreichbar (N1 , S, L).
Programmierung
Logische Programmierung
Syntax und Semantik
Das Acht-Damen-Problem
0Z0Z0ZQZ
Z0Z0Z0Z0
0Z0Z0L0Z
Z0L0Z0Z0
0Z0ZQZ0Z
ZQZ0Z0Z0
0Z0L0Z0Z
L0Z0Z0Z0
Löse das Problem rekursiv für die ersten k Spalten.
Dann versuche die k + 1te Spalte zu besetzen.
Programmierung
Logische Programmierung
Syntax und Semantik
Das Acht-Damen-Problem
sol(0, [ ]).
sol(N, [Y |L]) :−
N > 0, N1 is N − 1, member (Y , [1, 2, 3, 4, 5, 6, 7, 8]),
sol(N1 , L),
not same row (Y , L), not same diag1 (Y , L), not same diag2 (Y , L).
not same row (Y , [ ]).
not same row (Y , [Y1 |R]) :− Y \ = Y1 , not same row (Y , R).
not same diag1 (Y , [ ]).
not same diag1 (Y , [Y1 |R]) :− YY is Y − 1, YY \ = Y1 ,
not same diag1 (YY , R).
not same diag2 (Y , [ ]).
not same diag2 (Y , [Y1 |R]) :− YY is Y + 1, YY \ = Y1 ,
not same diag1 (YY , R).
Programmierung
Logische Programmierung
Syntax und Semantik
Hinzufügen von Fakten und Regeln
Durch assert (und asserta, assertz) können wir Fakten und Regeln
zur Wissensbasis hinzufügen.
Hier ein Beispiel zu Memoization:
:− dynamic fib/2.
fib(0, 0).
fib(1, 1).
fib(N, M) :−
N > 1,
N1 is N − 1, N2 is N − 2,
fib(N1 , M1 ),
fib(N2 , M2 ),
M is M1 + M2 ,
asserta(fib(N, M)).
Die erste Zeile erlaubt“ asserta.
Programmierung
Logische Programmierung
Syntax und Semantik
Ausgabe
Das Ziel write(X ) ist immer erfüllt.
Als Seiteneffekt wird X auf den Bildschirm ausgegeben.
Durch nl wird eine neue Zeile angefangen.
Türme von Hanoi als Beispiel:
hanoi(0, A, B, C ).
hanoi(N, A, B, C ) :−
N > 0,
N1 is N − 1,
hanoi(N1 , A, C , B),
write(’Nimm Scheibe von ’), write(A),
write(’ und setze sie auf ’), write(C ), nl,
hanoi(N1 , C , B, A).
Herunterladen