Sie sind auf Seite 1von 245

Schreibe Dein Programm!

Einfhrung in die Programmierung

Herbert Klaeren Michael Sperber

14. Mai 2014


Copyright 2007-2014 by Herbert Klaeren and Michael Sperber
Dieses Buch ist lizensiert unter der Creative-Commons-Lizenz Na-
mensnennung 4.0 International (CC BY 4.0), nachzulesen unter http:
//creativecommons.org/licenses/by/4.0/deed.de.

Dieses Buch wurde mit LATEX gesetzt, unter Verwendung der suftesi-Klasse von
Ivan Valbusa.
Inhaltsverzeichnis

1 Elemente des Programmierens 5


1.1 Handwerkszeug fr das Programmieren 5
1.2 Bausteine fr Programme 6
1.3 Rechnen ohne Zahlen 6
1.4 Namen und Definitionen 7
1.5 Information und Daten 7
1.6 Abstraktion 8
1.7 Kurzbeschreibung und Signatur 8
1.8 Testflle 10
1.9 Unsinnige Daten 12
1.10 Probleme und Teilprobleme 12
1.11 Das Substitutionsmodell 12

2 Fallunterscheidungen und Verzweigungen 13


2.1 Fallunterscheidungen 13
2.2 Boolesche Ausdrcke in Scheme 14
2.3 Programmieren mit Fallunterscheidungen 15
2.4 Konstruktionsanleitung fr Fallunterscheidungen 18
2.5 Verkrzte Tests 19
2.6 Binre Verzweigungen und syntaktischer Zucker 19
2.7 Signaturdefinitionen 21
2.8 Unsinnige Daten abfangen 24

3 Zusammengesetzte Daten 25
3.1 Computer konfigurieren 25
3.2 Record-Definitionen 29
3.3 Schablonen fr zusammengesetzte Daten 30
3.4 Grteltiere im Computer 31

4 Gemischte Daten 35
4.1 Gemischte Daten 35
4.2 Die Zucker-Ampel 39

5 Programmieren mit Listen 45


5.1 Listen reprsentieren 45
5.2 Mit Listen programmieren 47
5.3 Signaturkonstruktoren 51
5.4 Eingebaute Listen 54
4 Inhaltsverzeichnis

5.5 Parametrische Polymorphie 54


5.6 Prozeduren, die Listen produzieren 55

6 Induktive Beweise und Definitionen 61


6.1 Aussagen ber natrliche Zahlen 61
6.2 Induktive Beweise fhren 63
6.3 Struktur der natrlichen Zahlen 66
6.4 Endliche Folgen 67
6.5 Notation fr induktive Definitionen 70
6.6 Strukturelle Rekursion 71
6.7 Strukturelle Induktion 73

7 Prozeduren ber natrlichen Zahlen 79

8 Higher-Order-Programmierung 85
8.1 Higher-Order-Prozeduren auf Listen 85
8.2 Listen zusammenfalten 90
8.3 Anonyme Prozeduren 93
8.4 Prozedurfabriken 94
8.5 Der Schnfinkel-Isomorphismus 95

9 Zeitabhngige Modelle 99
9.1 Das Teachpack image2.ss 99
9.2 Zwischenergebnisse benennen 101
9.3 Modelle und Ansichten 102
9.4 Bewegung und Zustand 102
9.5 Andere Welten 103

10 Eigenschaften von Prozeduren 105


10.1 Eigenschaften von eingebauten Operationen 105
10.2 Eigenschaften von Prozeduren auf Listen 113
10.3 Eigenschaften von Prozeduren hherer Ordnung 120
10.4 Programme beweisen 121
10.5 Rekursive Programme beweisen 122
10.6 Invarianten 126

11 Fortgeschrittenes Programmieren mit Rekursion 131


11.1 Lastwagen optimal beladen 131

12 Programmieren mit Akkumulatoren 137


12.1 Zwischenergebnisse mitfhren 137
12.2 Schablonen fr Prozeduren mit Akkumulator 143
12.3 Kontext und Endrekursion 144
12.4 Das Phnomen der umgedrehten Liste 145

13 Bume 149
13.1 Binrbume 149
13.2 Suchbume 152
13.3 Eigenschaften der Suchbaum-Operationen 157
Inhaltsverzeichnis 1

14 Schrittweise Verfeinerung 163


14.1 Lschen in Suchbumen 163
14.2 Datenverfeinerung 169

15 Zuweisungen und Zustand 179

16 Der -Kalkl 181


16.1 Sprache und Reduktionssemantik 181
16.2 Normalformen 186
16.3 Der -Kalkl als Programmiersprache 187
16.4 Auswertungsstrategien 191
16.5 Die Auswertungsstrategie von Scheme 194

17 Die SECD-Maschine 197


17.1 Der angewandte -Kalkl 197
17.2 Die einfache SECD-Maschine 198
17.3 Quote und Symbole 202
17.4 Implementierung der SECD-Maschine 206
17.5 Die endrekursive SECD-Maschine 218
17.6 Der -Kalkl mit Zustand 220
17.7 Die SECDH-Maschine 222
17.8 Implementierung der SECDH-Maschine 223

1 Mathematische Grundlagen 233


1.1 Aussagenlogik 233
1.2 Mengen 234
1.3 Prdikatenlogik 237
1.4 Multimengen 237
1.5 Relationen und Abbildungen 238
1.6 Ordnungen 240
Vorwort

TBD
Schreibe Dein Programm! ist aus dem Vorgngerbuch Die Macht der
Abstraktion entstanden, das seinerseits aus dem Vorgngerbuch Vom
Problem zum Programm entstanden ist. Wir bemerkten nach der Verffent-
lichung vonDie Macht der Abstraktion, da wir das Buch einerseits einem
breiten Publikum einfach zugnglich machen wollten, andererseits kon-
tinuierlich Verbesserungen einarbeiten wollten. Beides war mit unserem
damaligen Verleger leider nicht zu machen. Entsprechend haben wir
uns entschieden, unsere Arbeit unter neuem Titel fortzufhren und
frei zugnglich zu machen. Es wird hoffentlich die letzte Titelnderung
bleiben.
Wir hatten zwar bereits viel Material aus Die Macht der Abstraktion bis
zur Unkenntlichkeit revidiert. Die TBD-Abschnitte in Schreibe Dein
Programm! kennzeichnen Stellen, deren Notwendigkeit bereits in Die
Macht der Abstraktion etabliert werde, die aber noch geschrieben werden
mssen.

Lehre mit diesem Buch

Das hier prsentierte Material entstammt einer Reihe von einfhrenden


Vorlesungen zur Informatik fr Haupt- und Nebenfachstudenten und
fr Geisteswissenschaftler sowie Erfahrungen in zahlreichen Fortbil-
dungen. Inhalte und Prsentation wurden dabei unter Beobachtung der
Studenten und ihres Lernerfolgs immer wieder verbessert. Der Stoff
dieses Buchs entspricht einer einsemestrigen Vorlesung Informatik I mit
vier Vorlesungsstunden und zwei bungsstunden.
Anhang 1 erlutert die im Buch verwendeten mathematischen Nota-
tionen und Termini.

Programmiersprache

Leider sind die heute in der Industrie populren Programmiersprachen


fr die Lehre nicht geeignet: ihre Abstraktionsmittel sind begrenzt, und
das Erlernen ihrer komplizierten Syntax kostet wertvolle Zeit und Kraft.
Aus diesem Grund verwendet der vorliegende Text eine Serie von
speziell fr die Lehre entwickelten Programmiersprachen, die auf Racket
und Scheme basieren. Diese sind ber die DrRacket-Entwicklungsumge-
bung Anfngern besonders gut zugnglich.
4 Vorwort

Software und Material zum Buch

Die Programmierbeispiele dieses Buchs bauen auf der Programmier-


umgebung DrRacket auf, die speziell fr die Programmierausbildung
entwickelt wurde. Insbesondere untersttzt DrRacket die Verwendung
sogenannter Sprachebenen, Varianten der Sprache, die speziell fr die
Ausbildung zugeschnitten wurden. Dieses Buch benutzt spezielle Spra-
chebenen, die Teil der sogenannten DMdA-Erweiterungen von DrRacket
sind.
DrRacket ist kostenlos im Internet auf der Seite http://www.racket-lang.
org/ erhltlich und luft auf Windows-, Mac- und Unix-/Linux-Rechnern.
Die DMdA-Erweiterungen sind von der Homepage zu Schreibe Dein
Programm! erhltlich:

http://www.deinprogramm.de/

Dort steht auch eine Installationsanleitung.


Auf der Homepage befindet sich weiteres Material zum Buch, insbe-
sondere Quelltext fr alle Programmbeispiele zum Herunterladen.

Danksagungen

Wir, die Autoren, haben bei der Erstellung dieses Buchs immens von
der Hilfe anderer profitiert. Robert Giegerich, Ulrich Gntzer, Peter
Thiemann, Martin Plmicke, Christoph Schmitz und Volker Klaeren
machten viele Verbesserungsvorschlge zum Vorgngerbuch Vom Pro-
blem zum Programm.
Martin Gasbichler hielt einen Teil der Vorlesungen der letzten Infor-
matik I, half bei der Entwicklung der DMdA-Erweiterungen und ist
fr eine groe Anzahl von Verbesserungen verantwortlich, die sich
in diesem Buch finden. Eric Knauel, Marcus Crestani, Sabine Sperber,
Jan-Georg Smaus und Mayte Fleischer brachten viele Verbesserungs-
vorschlge ein. Andreas Schilling, Torsten Grust und Michael Hanus
hielten Vorlesungen auf Basis dieses Buches und brachten ebenfalls
viele Verbesserungen ein. Besonderer Dank gebhrt den Tutoren und
Studenten unserer Vorlesung Informatik I, die eine Flle wertvoller Kritik
und exzellenter Verbesserungsvorschlge lieferten.
Wir sind auerdem dankbar fr die Arbeit unserer Kollegen, die
Pionierarbeit in der Entwicklung von Konzepten fr die Programmier-
ausbildung geliefert haben. Eine besondere Stellung nehmen Matthias
Felleisen, Robert Bruce Findler, Matthew Flatt und Shriram Krishna-
murthi und ihr Buch How to Design Programs [?] ein, das entscheidende
didaktische Impulse fr dieses Buch gegegen hat. Felleisens Arbeit im
Rahmen des PLT-Projekts hat uns stark beeinflut; das PLT-DrRacket-
System ist eine entscheidende Grundlage fr die Arbeit mit diesem
Buch.

Herbert Klaeren
Michael Sperber
Tbingen, April 2014
1 Elemente des Programmierens

TBD

1.1 Handwerkszeug fr das Programmieren

In diesem Kapitel wird zum ersten Mal die Programmierumgebung


DrRacket verwendet. (Bezugsquelle und weitere Hinweise dazu stehen
im Vorwort.) Zur Verwendung mit diesem Buch mssen in DrRacket
die DMdA-Sprachebenen aktiviert werden. Dies geschieht durch Aus-
wahl des Menpunkts Sprache Sprache auswhlen (bzw. Language
Choose language in der englischen Fassung), worauf ein Dialog zur
Auswahl von sogenannten Sprachebenen erscheint. Dort gibt es in der
Abteilung Lehrsprachen eine berschrift namens DeinProgramm, unter-
halb dessen mehrere Eintrge erscheinen, die speziell auf die Kapitel
dieses Buchs zugeschnitten sind.
Fr den ersten Teil des Buches ist die Ebene Die Macht der Abstraktion
- Anfnger zustndig. In Kapitel 5 wird auf Die Macht der Abstraktion
(ohne Anfnger), und in Kapitel 15 auf Die Macht der Abstraktion
mit Zuweisungen umgeschaltet. In Kapitel 17 kommt schlielich Die
Macht der Abstraktion - fortgeschritten zum Einsatz.
DrRacket bietet dem Programmierer ein zweigeteiltes Fenster:

1. In der oberen Hlfte des Fensters (dem Editor oder Definitionsfenster)


steht der Programmtext. Der Editor funktioniert hnlich wie ein
regulres Textverarbeitungsprogramm.
2. In der unteren Hlfte des Fensters (dem Interaktionsfenster) werden
die Ausgaben des Programms angezeigt. Auerdem kann der Pro-
grammierer hier Fragen an das Programm stellen, um einzelne
Programmteile gezielt auszuprobieren.
Im Interaktionsfenster berechnet DrRacket die Antworten sofort nach
Druck auf die Return-Taste und druckt diese aus. Der Inhalt des In-
teraktionsfensters kann allerdings nicht direkt abgespeichert werden.
Der Editor ist also fr den entstehenden Programmtext gedacht, das
Interaktionsfensters zum schnellen Ausprobieren.

TBD

TDB
Abbildung 1.1. Das Interaktionsfenster von DrRacket
6 Kapitel 1

1.2 Bausteine fr Programme

TBD

1.3 Rechnen ohne Zahlen

Ausdrcke und Werte gibt es in Computerprogrammen nicht nur in


Form von Zahlen. Zum Beispiel gibt es auch Text, wie in Abbildung 1.3
beschrieben. (Ksten wie Abbildung 1.3 werden in diesem Buch noch
oft dazu dienen, neue Sprachelemente einzufhren.)

Zeichenketten (auf Englisch Strings) reprsentieren Text. Literale fr


Zeichenketten haben folgende Form:

"z1 z2 . . . zn "
Dabei sind die zi beliebige einzelne Zeichen, auer " selbst. Beispiel:
"Mike was here!"

Das Anfhrungszeichen (") kann nicht ungeschtzt vorkommen, da


es das Ende der Zeichenkette markiert. Es wird als Zeichen innerhalb
einer Zeichenkette durch \" dargestellt:
"Herbert sagt, Mike wre \"doof\"!"

Abbildung 1.2. Zeichenketten

Mit Text kann DrRacket auch rechnen, und zwar mit der eingebauten
Prozedur string-append, die zwei Zeichenketten aneinanderhngt:
(string-append "Herbert" "Mike")
, "HerbertMike"
(string-append "Mike" " " "ist doof")
, "Mike ist doof"
Die eingebaute Prozedur string-length liefert die Anzahl der Buchsta-
ben in einer Zeichenkette:
(string-length "Herbert")
, 7
(string-length "Mike")
, 4
Die Prozeduren string->number und number->string konvertieren zwi-
schen Zahlen und den Zeichenketten, die diese darstellen:
(string->number "23")
, 23
(number->string 23)
, "23"
Programme knnen auch mit Bildern rechnen. Dazu wird eine Erwei-
terung zu DrRacket bentigt, ein sogenanntes Teachpack: Whlen Sie
dazu im Men Sprache den Punkt Teachpack hinzufgen und whlen
Sie image2.ss aus. Danach knnen Sie zum Beispiel ins Programm
schreiben:
Elemente des Programmierens 7

(square 40 "solid" "red")

,
(circle 40 "solid" "green")

,
(star-polygon 20 10 3 "solid" "blue")

,
Diese Bilder sind Werte wie Zahlen und Zeichenketten auch. Insbeson-
dere knnen Sie mit Definitionen an Namen gebunden werden:

(define s1 (square 40 "solid" "slateblue"))


(define c1 (circle 40 "solid" "slateblue"))
(define p1 (star-polygon 20 10 3 "solid" "cornflowerblue"))

Mit Bildern kann DrRacket ebenfalls rechnen:

(beside/align "bottom" s1 c1 p1)

,
Bilder und Animationen mit Bildern werden ausfhrlich in Kapitel 9
behandelt.

1.4 Namen und Definitionen

TBD

1.5 Information und Daten

Eine Definition wie

(define mehrwertsteuer 19)

suggeriert, da die Zahl 19 an dieser Stelle eine Bedeutung in der


realen Welt hat, zum Beispiel in einem Programm, das eine Registrier-
kasse steuert oder das bei der Steuererklrung hilft. Die Bedeutung
knnte folgende Aussage sein: Der Mehrwertsteuersatz betrgt 19%.
Dieser Satz reprsentiert Information, also ein Fakt ber die Welt oder
zumindest den Ausschnitt der Welt, in dem das Programm arbeiten soll.
Im Computerprogrammen wird Information in eine vereinfachte Form
gebracht, mit das Programm rechnen kann in diesem Fall die Zahl 19.
Diese vereinfachte Form heit Daten: Daten sind Reprsentationen fr
Informationen. Beim Programmieren ist eine unserer Hauptaufgaben
entsprechend, die richtigen Form fr die Daten zu whlen, um die fr
das Programm relevanten Informationen darzustellen die Informatio-
nen dann in Daten zu bersetzen.
Nicht immer ist offensichtlich, welche Information durch bestimmte
Daten reprsentiert werden. Das Datum 23 zum Beispiel knnte eine
Reihe von Informationen darstellen:
8 Kapitel 1

die Anzahl der Haare von Bruce Willis


die aktuelle Auentemperatur in C in Tbingen
die Auentemperatur vom 1.7.2000 in C in Tbingen
die Gre in m2 des Schlafzimmers
die Rckennummer von Michael Jordan
Damit andere unsere Programme lesen knnen, werden wir also immer
wieder klarstellen mssen, wie Information in Daten zu bersetzen ist
und umgekehrt.
Manche Programme knnen auch Informationen direkt verarbeiten,
meist dadurch, da sie diese erst in Daten bersetzen und dann die
Daten weiterverarbeiten. Der Teil, der diese bersetzung leistet, heit
Benutzerschnittstelle. Zunchst werden wir uns allerdings primr mit rein
datenverarbeitenden Programmen beschftigen; Benutzerschnittstellen
kommen spter.

1.6 Abstraktion

TBD

1.7 Kurzbeschreibung und Signatur

Angenommen, die Prozedurdefinition von parking-lot-cars wird an


jemanden weitergegeben, der dieses Buch nicht gelesen hat, aber die
Prozedur trotzdem einsetzen soll. Der potentielle Leser kann zwar
das Scheme-Programm prinzipiell verstehen, hat aber keinen weiteren
Hinweis darauf, wofr parking-lot-cars verwendet werden kann.

In Scheme kennzeichnet ein Semikolon ; einen Kommentar. Der Kom-


mentar erstreckt sich vom Semikolon bis zum Ende der Zeile und wird
vom Scheme-System ignoriert.

Abbildung 1.3. Kommentare

Das Problem ist, da die Definition von parking-lot-cars das End-


produkt des Denkprozesses ist, der in Kapitel ?? beschrieben wurde.
Der Denkproze selbst, der mit der Aufgabenstellung anfngt, ist nicht
Teil der Definition. Darum ist es hilfreich, wenn wichtige Aspekte des
Denkprozesses als Kommentare (siehe Abbildung 1.3) bei den Definitio-
nen stehen.
Ein erster sinnvoller Kommentar ist eine Kurzbeschreibung der Aufga-
benstellung:
; aus der Anzahl der Fahrzeuge und Rder die Anzahl der PKWs bestimmen

Fr die Kurzbeschreibung reicht in der Regel eine Zeile: Nehmen Sie


diese Einschrnkung als Gelegenheit, sich knapp, prgnant und przise
auszudrcken.
Als nchstes ist eine besondere Formulierung hilfreich, die sogenann-
te Signatur. Wer nur gelesen hat, dass die Prozedur parking-lot-cars
zwei Argumente vehicle-count und wheel-count hat, knnte ja auf den
Gedanken kommen, einen Aufruf der Form
Elemente des Programmierens 9

(parking-lot-cars "zweiundzwanzig" "achtunddreissig")

zu notieren. Das wird bei der Ausfhrung eine Fehlermeldung erzeugen,


weil die eingebauten Prozeduren /, - und * nur mit Zahlen in Form
von Ziffernfolgen umgehen knnen, aber nicht mit Zeichenketten, die
vielleicht auch Zahlen bezeichnen knnten. In der Tat akzeptieren fast
alle Prozeduren nur Argumente einer ganz bestimmten Sorte, in diesem
Fall Argumente der Sorte natrliche Zahl.
Hier eine Liste der wichtigsten eingebauten Sorten:

natrliche Zahlen natural


ganze Zahlen integer
rationale Zahlen rational
reelle Zahlen real
Zahlen allgemein (inkl. komplexe) number
Zeichenketten string
Bilder image

Eine Signatur ist eine Vorstufe fr die zu entwickelnde Prozedur und


fat einige wichtige Informationen zusammen:

1. den Namen der Prozedur,


2. Anzahl und Sorten der Argumente und
3. die Sorte des Rckgabewerts der Prozedur.

Die Prozedur parking-lot-cars akzeptiert zwei natrliche Zahlen und


liefert wieder eine natrliche Zahl. Deshalb sieht die Signatur von
parking-lot-cars so aus:

(: parking-lot-cars (natural natural -> natural))

Diese Signatur besagt:

Parking-lot-cars ist eine Prozedur (das sagt der Pfeil -> zusammen
mit den Klammern);
parking-lot-cars akzeptiert zwei Argumente (vor dem Pfeil stehen
zwei Wrter);
die beiden Argumente sind natrliche Zahlen (natural);
die Prozedur liefert wieder eine natrliche Zahl (das ist das natural
hinter dem Pfeil).

Die Signatur hnelt also der mathematischen Notation fr Funktionen,


die einen bestimmten Typ haben.
Aus der Signatur ergeben sich, wenn fr die beiden Argumente
sprechende Namen gefunden worden sind, die ersten beiden Zeilen der
folgenden Definition, das sogenannte Gerst:

(define parking-lot-cars
(lambda (vehicle-count wheel-count)
...))

Es bleibt, die passende Formel aus der mathematischen Theorie aus


Kapitel ?? einzusetzen. Die Definition von parking-lot-cars sollte voll-
stndig so aussehen:
10 Kapitel 1

; aus der Anzahl der Fahrzeuge und Rder die Anzahl der PKWs bestimmen
(: parking-lot-cars (natural natural -> natural))
(define parking-lot-cars
(lambda (vehicle-count wheel-count)
(/ (- m (* 2 n))
2)))

Signaturen knnen fr alle Arten von Werten deklariert werden, nicht


nur fr Prozeduren. Zum Beispiel so:
(: pi real)

Bei parking-lot-cars ist die Signatur noch nicht besonders umfangreich


oder kompliziert. Sptere Kapitel werden zeigen, da sich aus vielen
Signaturen ganz automatisch Schablonen ergeben, die dem Programmie-
rer einen Groteil der Denkarbeit bei der Entwicklung von Prozeduren
abnehmen.
Aus diesem Grund schreiben wir in diesem Buch die Kurzbeschrei-
bung und die Signatur in das Programm, bevor wir die Definition
entwickeln: Die nachtrgliche Entwicklung dieser Kommentare ist mh-
selig und langweilig. Auerdem sind die Kurzbeschreibung und die
Signatur ein hilfreicher Teil des Problemlsungsprozesses. Schon man-
cher Programmierer Anfnger und Profi ist an Aufgaben gescheitert,
die sich mit Hilfe systematischen Vorgehens anhand der Signatur leicht
htten lsen lassen.
Aus dem fernen Osten stammt der Begriff des Mantras als einem
Sinnspruch, den es sich lohnt, auswendig zu lernen. Hier das erste
Mantra:

Mantra 1 (Signatur vor Ausfhrung) Schreiben Sie eine Kurzbeschrei-


bung der Aufgabe und eine Signatur ins Programm, bevor Sie die
Prozedur selbst programmieren.
Ab jetzt werden sich die Programmbeispiele in diesem Buch natr-
lich an dieses Mantra halten. Kurzbeschreibung, Signatur, Testflle
(beschrieben im nchsten Abschnitt) Gerst und Schablone sind feste
Bestandteile einer Konstruktionsanleitung, die systematisch beschreibt,
wie eine Aufgabe schrittweise gelst werden kann. Dieses Buch wird
eine Reihe von Konstruktionsanleitungen vorstellen, die sich stets an
der Signatur einer Prozedur orientieren. Alle Mantras sind in Anhang ??
und die Konstruktionsanleitungen in Anhang ?? zusammengefat.

1.8 Testflle

Vertrauen ist gut aber Fehler passieren, auch bei sorgfltiger Program-
mierung. Angenommen, bei der Programmierung von parking-lot-cars
wre folgendes herausgekommen:
; aus der Anzahl der Fahrzeuge und Rder die Anzahl der PKWs bestimmen
(: parking-lot-cars (natural natural -> natural))
(define parking-lot-cars
(lambda (vehicle-count wheel-count)
(/ (- wheel-count (* 4 vehicle-count))
2)))
Elemente des Programmierens 11

Sehen Sie den Fehler auf den ersten Blick? Einfaches Ausprobieren ist
da vielleicht schneller:
(parking-lot-cars 1 4)
, 0
Bei der Entwicklung der Prozedur sollten also Testflle konstruiert
werden, die an ausgewhlten Beispielen berprfen, ob die gerade
programmierte Prozedur auch korrekt funktioniert. Testen ist eine un-
verzichtbare Ttigkeit des Programmierers.
Die Testflle werden am besten vor der Definition der Prozedur aufge-
stellt, denn wenn sie erst hinterher geschrieben werden, ist die Gefahr
gro, da unbewut das tatschliche Ergebnis eines Prozeduraufrufs
als das gewnschte eingegeben oder besonders kritische Beispiele weg-
gelassen werden. (In der industriellen Praxis ist sogar oft blich, da
jemand anderes als der Autor der Definitionen die Testflle schreibt.)
Es ist mhselig, bei der Programmentwicklung stndig Testflle in
die REPL einzutippen und durch einen Vergleich mit den erwarteten
Ergebnissen herauszubekommen, ob alles in Ordnung ist. In DrRacket
geht es deshalb auch einfacher. Testflle knnen zusammen mit den
erwarteten Ergebnissen wie folgt spezifiziert werden:
(check-expect (parking-lot-cars 1 4) 1)
(check-expect (parking-lot-cars 2 6) 1)
(check-expect (parking-lot-cars 10 28) 4)

Beim Druck auf den Start-Knopf berprft DrRacket, ob die tatsch-


lichen Ergebnisse der Ausdrcke mit den Soll-Werten bereinstimmen.
Fr fehlgeschlagene Testflle ffnet sich ein neues Fenster mit Informa-
tionen ber die Unterschiede zwischen erwarteten und tatschlichen
Ergebnissen; ansonsten gibt es eine kurze Meldung, dass die Testflle
erfolgreich waren. Fr die obere inkorrekte Version kommt zum Beispiel
folgendes heraus:
3 Tests gelaufen.
0 Tests bestanden.
2 Signaturverletzungen.

Check-Fehler:
Der tatschliche Wert 0 ist nicht der erwartete Wert 1.
in Zeile 4, Spalte 0
Der tatschliche Wert -1 ist nicht der erwartete Wert 1.
in Zeile 5, Spalte 0
Der tatschliche Wert -6 ist nicht der erwartete Wert 4.
in Zeile 6, Spalte 0

Signaturverletzungen:
bekam -1 in Zeile 5, Spalte 14 , Signatur in Zeile 2, Spalte 40
verantwortlich: Prozedur in Zeile 9, Spalte 2
bekam -6 in Zeile 6, Spalte 14 , Signatur in Zeile 2, Spalte 40
verantwortlich: Prozedur in Zeile 9, Spalte 2

Eine grozgige Verwendung von Testfllen kann viele Programmier-


fehler aufdecken und damit die Programmierung erleichtern und be-
schleunigen.
12 Kapitel 1

Mantra 2 (Testflle) Schreiben Sie fr jede Prozedur Testflle, bevor Sie


die Definition schreiben.

1.9 Unsinnige Daten

Die Testflle aus dem vorangegangenen Abschnitt sind alle sinnvoll


die Eingabedaten passen alle zu tatschlichen Parkplatzsituationen. Was
ist aber hiermit?
(parking-lot-cars 3 9)

Wie schon in Kapitel ?? (Seite ??) bereits angedeutet, lassen sich die Da-
ten 3 und 9 nicht als Information interpretieren: Es gibt keinen Parkplatz
mit 3 Fahrzeugen und 9 Rdern zumindest nicht mit den Einschrn-
kungen der Aufgabenstellung auf vollberderte PKWs und Motorrder.
Die Prozedur parking-lot-cars strt dies allerdings wenig: Sie liefert
munter die Ausgabe 1.5. Allerdings meldet DrRacket eine Signaturver-
letzung, wenn es (parking-lot-cars 3 9) auswertet, da das Ergebnis
keine natrliche Zahl ist wie in der Signatur angegeben.
Das Programm sollte natrlich abseits der Signaturverletzung un-
sinnige Daten soweit mglich und praktikabel zurckweisen. Fr die
Eingabe (parking-lot-cars 3 16) htte es nmlich keine Signaturverlet-
zung gegeben, sondern es wre eine zunchst unschuldig aussehende
5 herausgekommen. Da htte es zuerst noch der Beobachtung bedurft,
dass unmglich 5 von 3 Fahrzeugen PKWs sein knnen. Noch feh-
len uns die Mittel, solche unsinnigen Eingaben zurckzuweisen; in
Abschnitt 2.8 werden wir dies nachholen.

1.10 Probleme und Teilprobleme

TBD

1.11 Das Substitutionsmodell

TBD

Aufgaben

TBD
2 Fallunterscheidungen und
Verzweigungen

Computerprogramme mssen bei manchen Daten, die sie verarbeiten,


zwischen verschiedenen Mglichkeiten differenzieren: Ist die Wasser-
temperatur warm genug zum Baden? Welche von fnf Tupperschsseln
ist fr eine bestimmte Menge Kartoffelsalat gro genug? Welches ist
die richtige Abzweigung nach Dortmund? Solche Entscheidungen sind
daran festgemacht, da ein Wert zu einer von mehreren verschiedenen
Kategorien gehren kann es handelt sich dann um eine sogenannte
Fallunterscheidung; mathematische Funktionen und Scheme-Prozeduren
operieren auf Daten mit Fallunterscheidung durch Verzweigungen. Um
diese geht es in diesem Kapitel.

2.1 Fallunterscheidungen

Zu den Flensburg-Punkten, die es bei Versten gegen die Straen-


verkehrsordnung gibt, hat eine Seite im Internet folgendes zu sagen:

0 bis 3 Punkte Keine Sanktionen


4 bis 8 Punkte Bei freiwilliger Teilnahme an Aufbauseminaren: 4 Punkte
Abzug
8 bis 13 Punkte Verwarnung und Hinweis auf freiwilliges Aufbausemi-
nar
9 bis 13 Punkte Bei freiwilliger Teilnahme an Aufbauseminaren: 2 Punk-
te Abzug
14 bis 17 Punkte Teilnahme an Aufbauseminar wird angeordnet
14 bis 17 Punkte Bei freiwilliger Teilnahme an verkehrspsychologischer
Beratung: 2 Punkte Abzug
Ab 18 Punkte Fhrerschein wird entzogen

Wir knnen zu dieser Aufstellung eine Reihe von Fragestellungen bear-


beiten, zum Beispiel welche Sanktionen durch eine bestimmte Punkte-
zahl verpflichtend werden oder wieviele Punkte ein Autofahrer nach
einer bestimmten Manahme noch auf dem Konto hat. In jedem Fall
teilt die Aufstellung mgliche Punktezahlen in bestimmte Kategorien
ein, je nach Manahme. Folgende Manahmen gibt es:

nichts
Aufbauseminar
verkehrspsychologische Beratung
14 Kapitel 2

Fhrerscheinentzug
Dies ist eine Aufzhlung.
Wir fangen mit der Fragestellung an, welche Zwangsmanahme
fr eine bestimmte Punktezahl angeordnet wird. Zwangsmanahmen
gibt es nur drei: keine, Aufbauseminar und Fhrerscheinentzug, da
die verkehrspsychologische Beratung rein freiwillig ist. Entsprechend
zerfllt die Punkteskala in drei Teile: 0 13, 14 17 und ab 18. Die
Punktezahl gehrt also zu einer von drei Kategorien.
Wenn die Menge, aus der ein Wert kommt, in eine feste Anzahl von
Kategorien aufgeteilt wird und bei einem Wert nur die Kategorie zhlt,
ist diese Menge durch eine Fallunterscheidung definiert. Aufzhlungen
sind damit auch Fallunterscheidungen.
Eine Funktion, die aus einem Punktestand die Zwangsmanahme
ermittelt, sieht folgendermaen aus:

nichts
falls p 13
def
m( p) = Aufbauseminar falls p 14, p 17

Fhrerscheinentzug falls p 18

Die Notation mit der groen geschweiften Klammer heit Verzweigung


(engl. conditional); ein Ausdruck wie p 13, der wahr oder falsch sein
kann, heit Bedingung oder Test.

2.2 Boolesche Ausdrcke in Scheme

Tests gibt es auch in Scheme; sie sind dort Ausdrcke. Hier ein Beispiel:
(<= -5 0)
, #t
(<= -5 0) ist die Scheme-Schreibweise fr 5 0. Als Frage gestellt
oder Aussage aufgefat, ist 5 0 wahr. #t steht fr true oder
wahr. <= ist eine eingebaute Prozedur, welche auf kleiner gleich
testet. (Ebenso gibt es auch = fr =, < fr <, > fr > und >= fr .)
Ein Test kann auch negativ ausfallen:
(<= 5 0)
, #f
#fsteht fr false oder falsch. Wahr und falsch heien zusam-
men boolesche Werte oder auch Wahrheitswerte.1
Analog zu = fr Zahlen knnen Zeichenketten mit string=? vergli-
chen werden:
(string=? "Mike" "Mike")
, #t
(string=? "Herbert" "Mike")
, #f

#t und #f sind wie Zahlen Literale, knnen also auch in Programmen


stehen:
1 Die booleschen Werte sind benannt nach George Boole (18151864), der zuerst einen
algebraischen Ansatz fr die Behandlung von Logik mit den Werten wahr und falsch
formulierte.
Fallunterscheidungen und Verzweigungen 15

#t
, #t
#f
, #f

Auch mit booleschen Werten kann DrRacket rechnen. Ein Ausdruck der
Form
(and e1 e2 . . . e n )
ergibt immer dann #t, wenn alle ei #t ergeben, sonst #f. Bei zwei
Operanden e1 und e2 ergibt (and e1 e2 ) immer dann #t, wenn e1 und e2
#t ergeben:

(and #t #t)
, #t
(and #f #t)
, #f
(and #t #f)
, #f
(and #f #f)
, #f
Entsprechend gibt es Ausdrcke der Form
(or e1 e2 . . . e n )
die immer dann #t ergeben, wenn einer der ei #t ergibt, sonst #f. Bei
zwei Operanden e1 und e2 ergibt (or e1 e2 ) immer dann #t, wenn e1
oder e2 #t ergeben:
(or #t #t)
, #t
(or #f #t)
, #t
(or #t #f)
, #t
(or #f #f)
, #f
Des weiteren gibt es noch eine eingebaute Prozedur not, die einen
booleschen Wert umdreht, sich also folgendermaen verhlt:
(not #f)
, #t
(not #t)
, #f

2.3 Programmieren mit Fallunterscheidungen

Zurck zu den Punkten in Flensburg: Zunchst schreiben wir eine Pro-


zedur, die zu einem gegebenen Punktestand die entsprechende Zwangs-
manahme ausrechnet. Fast alles, was zur Datenanalyse gehrt, haben
wir schon am Anfang des Kapitels gemacht: der Punktestand ist eine
natrliche Zahl, eine Zwangsmanahme ist nichts, ein Aufbauseminar
oder der Fhrerscheinentzug. Diese Informationen mssen wir noch in
16 Kapitel 2

Daten umwandeln dazu benutzen wir einfach die entsprechenden Zei-


chenketten "nichts", "Aufbauseminar" und "Fhrerscheinentzug" und
halten das Ergebnis in einem Kommentar fest:
; Eine Zwangsmanahme ist:
; - "nichts"
; - "Aufbauseminar"
; - "Fhrerscheinentzug"

Die Kurzbeschreibung der Prozedur knnte so aussehen:


; Zwangsmanahme bei Flensburg-Punktestand errechnen

Eine passende Signatur ist diese hier:


(: points-must-do (natural -> (one-of "nichts"
"Aufbauseminar"
"Fhrerscheinentzug")))

Die Konstruktion one-of bei Signaturen ist neu: In der obigen Signa-
tur bedeutet es, da der Aggregatzustand einer der in der one-of-
Signatur angegegebenen Werte ist, also eine der Zeichenketten "nichts",
"Aufbauseminar" und "Fhrerscheinentzug".
Hier sind zwei mgliche Testflle:
(check-expect (points-must-do 14) "Aufbauseminar")
(check-expect (points-must-do 18) "Fhrerscheinentzug")

Es folgt das Gerst der Prozedur:


(define points-must-do
(lambda (p)
...))

Auf jeden Fall mu das p irgendwo im Rumpf vorkommen:


(define points-must-do
(lambda (p)
... p ...))

Jetzt brauchen wir, wie bei der mathematischen Funktion m aus Ab-
schnitt 2.1, eine Verzweigung, nur eben in Scheme. Abbildung 2.1
beschreibt das dafr zustndige cond. Es ist also von vorneherein klar,
da eine cond-Form im Rumpf von points-must-do auftauchen mu:
(define points-must-do
(lambda (p)
(cond ... p ...)))

Bei der Konstruktion der cond-Formen ist entscheidend, wieviele Zwei-


ge sie hat. Dabei gibt es eine einfache Faustregel da die Eingabe von
points-must-do die Punktzahl in drei Kategorien zerfllt, braucht
die cond-Form auch drei Zweige:
(define points-must-do
(lambda (p)
(cond
(... ...)
(... ...)
(... ...))))
Fallunterscheidungen und Verzweigungen 17

In Scheme werden Verzweigungen mit der Spezialform cond dargestellt.


Ein cond-Ausdruck hat die folgende Form:

(cond
(t1 a1 )
(t2 a2 )
...
( t n 1 a n 1 )
(else an )))

Dabei sind die ti und die ai ihrerseits Ausdrcke. Der cond-Ausdruck


wertet nacheinander alle Tests ti aus; sobald ein Test tk #t ergibt, wird
der cond-Ausdruck durch das entsprechende ak ersetzt. Wenn alle Tests
fehlschlagen, wird durch an ersetzt. Die Paarungen (ti ai ) heien Zweige
des cond-Ausdruckes, und der Zweig mit else (auf deutsch sonst)
heit else-Zweig. Der else-Zweig kann auch fehlen dann sollte aber
immer einer der Tests #t ergeben. Wenn doch einmal bei allen ti #f
herauskommen sollte, bricht DrRacket das Programm ab und gibt eine
Fehlermeldung aus.

Abbildung 2.1. Verzweigung

Wir bentigen jetzt fr jeden cond-Zweig einen Test, der die entspre-
chende Kategorie bei den Punkten identifiziert. Dazu mssen wir nur
die entsprechenden Bedingungen aus der mathematischen Fassung
nach Scheme bersetzen. Heraus kommt folgendes:
(define points-must-do
(lambda (p)
(cond
((<= p 13) ...)
((and (>= p 14) (<= p 17)) ...)
((>= p 18) ...))))

Der letzte Schritt ist einfach wir fgen fr jeden Zweig die zum Test
passende Manahme ein:
(define points-must-do
(lambda (p)
(cond
((<= p 13) "nichts")
((and (>= p 14) (<= p 17)) "Aufbauseminar")
((>= p 18) "Fhrerscheinentzug"))))

Fertig knnte man meinen. Wenn Sie das Programm in der REPL
laufen lassen, meldet DrRacket zwei bestandene Tests. Allerdings fllt
Ihnen vielleicht auf, da das Programm im Definitionsfenster nach dem
Lauf so aussieht:
(define points-must-do
(lambda (p)
(cond
((<= p 13) "nichts" )
18 Kapitel 2

((and (>= p 14) (<= p 17)) "Aufbauseminar")


((>= p 18) "Fhrerscheinentzug"))))

Das "nichts" ist farbig unterlegt, weil DrRacket diesen Ausdruck noch
nie ausgewertet hat. Das ist nicht gut, weil es heit, da der entspre-
chende Zweig durch die Tests noch nicht strapaziert wurde er ist
also mglicherweise fehlerhaft. Anders gesagt: Die Abdeckung des Pro-
gramms durch die Tests ist unvollstndig. Wenn wir einen Testfall fr
den ersten Zweig ergnzen, verschwindet die farbige Unterlegung:
(check-expect (points-must-do 0) "nichts")

Trotzdem ist der bestehende Satz an Tests noch suboptimal wer sagt
schlielich, da das Programm zum Beispiel bei 13 Punkten, also genau
an der Grenze zwischen dem ersten und zweiten Zweig, das richtige
tut. Wir sollten fr diese Eckflle auch Testflle bereitstellen sowie einen
Testfall, der sicherstellt, da auch bei Punktzahlen oberhalb von 18
immer noch der Fhrerschein entzogen wird:
(check-expect (points-must-do 13) "nichts")
(check-expect (points-must-do 17) "Aufbauseminar")
(check-expect (points-must-do 100) "Fhrerscheinentzug")

Mantra 3 (Abdeckung und Eckflle) Sorgen Sie fr vollstndige Abdeckung


Ihres Programms durch die Testflle! Testen Sie mglichst alle Eckflle!

2.4 Konstruktionsanleitung fr Fallunterscheidungen

Bei der Konstruktion der Prozedur points-must-do haben wir ein be-
stimmtes Schema angewendet. Dieses Schema geht zunchst von fol-
gender Frage aus:
Wieviele Kategorien gibt es bei der Fallunterscheidung?

Ist die Frage beantwortet durch eine Zahl n knnen wir bereits
etwas Code in den Rumpf schreiben, nmlich eine Verzweigung mit n
Zweigen:
(define p
(lambda (...)
(cond
(... ...)
... (n Zweige)
(... ...))))

Solch ein Rumpf mit Lcken (die Ellipsen ... stehen fr noch zu
ergnzende Programmteile) ist eine Schablone. Wir, die Autoren, empfeh-
len Ihnen, die Schablone bereits hinzuschreiben, wenn Sie die Anzahl
der Kategorien bereits kennen, noch bevor Sie weiter ber die Pro-
blemstellung nachdenken. Das hilft oft, etwaige Denkblockaden zu
lsen.
Die Schablone folgt in diesem Fall aus der Struktur der Daten, also
dem Ergebnis der Datenanalyse. Es gibt noch andere Arten von Daten,
jede mit ihrer eigenen Schablone. Diese werden wir im Rest des Buchs
entwickeln. Fr alle folgenden Konstruktionsanleitungen gilt deshalb
folgendes Mantra:
Fallunterscheidungen und Verzweigungen 19

Mantra 4 (Schablone) Benutzen Sie ausgehend von einer Datenanalyse


die passende Schablone!
Was die Fallunterscheidung betrifft, knnen wir die Schablone aber
noch weiterentwickeln, indem wir Tests fr die einzelnen Flle der
Fallunterscheidung ergnzen.
Die Schablone fr Fallunterscheidungen ist noch einmal als Konstruk-
tionsanleitung ?? in Anhang ?? zusammenfat. (Konstruktionsanlei-
tung ?? beschreibt die Konstruktion von Prozeduren im allgemeinen.)

2.5 Verkrzte Tests

Natrlich knnten wir die Prozedur auch leicht abkrzen:


(define points-must-do
(lambda (p)
(cond
((<= p 13) "nichts")
((<= p 17) "Aufbauseminar")
(else "Fhrerscheinentzug"))))

Wenn die Auswertung den Test im zweiten Zweig erreicht, steht schon
fest, da die Punktezahl 14 ist, da der Test (<= p 13) fehlgeschlagen
ist. Diese Bedingung knnten wir also weglassen. Ebenso der letzte Test,
der dadurch, da (<= p 17) #f ergeben hat, immer #t ergibt. Allerdings
sind die Zweige damit von ihrer Reihenfolge abhngig: Wenn wir
zum Beispiel die ersten beiden Zweige vertauschten, funktioniert die
Prozedur nicht mehr richtig. In Fllen wie diesen, wo vollstndige
Tests einfach zu formulieren sind, empfiehlt es sich, dies auch zu tun.

Mantra 5 (Vollstndige Tests) Schreiben Sie wenn mglich bei Verzwei-


gungen vollstndige Tests, so da die Verzweigung unabhngig von
der Reihenfolge der Zweige ist.

2.6 Binre Verzweigungen und syntaktischer Zucker

Bei manchen Fallunterscheidungen definiert sich die letzte Kategorie


dadurch, da ein Wert in keine der anderen Kategorien gehrt. Dann
ist die Benutzung eines else-Zweigs im cond sinnvoll. FIXME: Beispiel
wre schn, dann vielleicht frher. Manchmal gibt es dabei nur zwei
Kategorien, wie zum Beispiel beim Absolutbetrag. Hier die Definition
dazu in mathematischer Schreibweise:

def x falls x 0
|x| =
x andernfalls
Die dazu passende Scheme-Prozedur unter Verwendung von cond sieht
so aus:
; Absolutbetrag einer Zahl berechnen
(: absolute (number -> number))
(define absolute
(lambda (x)
(cond
((>= x 0) x)
(else (- x)))))
20 Kapitel 2

FIXME: besseres Beispiel


Dieser Spezialfall mit nur zwei Kategorien, genannt binre Verzwei-
gung kommt in der Praxis hufig vor. In Scheme gibt es dafr eine
eigene Spezialform, genannt if, die hier krzer ausfllt als cond:
(define absolute
(lambda (x)
(if (>= x 0)
x
(- x))))

Eine if-Form hat folgende Form:


(if t k a)
Dabei ist t der Test und k und a sind die beiden Zweige: die Konsequente
k und die Alternative a. Abhngig vom Ausgang des Tests ist der Wert
der Verzweigung entweder der Wert der Konsequente oder der Wert
der Alternative.
Tatschlich ist if die primitivere Form als cond: jede cond-Form
kann in eine quivalente if-Form bersetzt werden, und zwar nach
folgendem Schema:
(cond (t1 a1 ) (t2 a2 ) . . . (tn1 an1 ) (else an ))
7 (if t1 a1 (if t2 a2 ... (if tn1 an1 an )...))
Die geschachtelte if-Form auf der rechten Seite der bersetzung wertet,
genau wie die cond-Form, nacheinander alle Tests aus, bis einer #t liefert.
Die rechte Seite des cond-Zweigs ist dann gerade die Konsequente des
ifs. Erst wenn alle Tests fehlschlagen ist die Alternative des letzten
if-Ausdrucks dran, nmlich an aus dem else-Zweig.
Da sich mit Hilfe dieser bersetzung jede cond-Form durch geschach-
telte if-Formen ersetzen lt, ist cond streng genommen gar nicht
notwendig. Cond ist deswegen eine sogenannte abgeleitete Form. Da cond
und andere abgeleitete Formen trotzdem praktisch und angenehm zu
verwenden sind und damit dem Programmierer die Arbeit versen,
heien abgeleitete Formen auch syntaktischer Zucker.
Um die Funktionsweise von Verzweigungen genau zu beschreiben,
dient folgende zustzliche Regel fr das Substitutionsmodell aus Ab-
schnitt 1.11:
binre Verzweigungen Bei der Auswertung einer Verzweigung wird zu-
nchst der Wert des Tests festgestellt. Ist dieser Wert #t, so ist der
Wert der Verzweigung der Wert der Konsequente. Ist er #f, so ist
der Wert der Verzweigung der Wert der Alternative. Ist der Wert
des Tests kein boolescher Wert ist, ist das Programm fehlerhaft.
Auch and und or sind eigentlich syntaktischer Zucker: Es ist immer
mglich, einen and-Ausdruck in ifs zu bersetzen. Es gelten folgende
bersetzungsregeln:
(and) 7 #t
(and e1 e2 . . .) 7 (if e1 (and e2 . . .) #f)
Ein and-Ausdruck mit mehreren Operanden wird so schrittweise in eine
Kaskade von if-Ausdrcken bersetzt:
Fallunterscheidungen und Verzweigungen 21

(and a b c)
7 (if a (and b c) #f)
7 (if a (if b (and c) #f) #f)
7 (if a (if b (if c (and) #f) #f) #f)
7 (if a (if b (if c #t #f) #f) #f)
Ebenso lassen sich or-Ausdrcke immer in if-Ausdrcke bersetzen,
und zwar mit folgender bersetzung:

(or) 7 #f
(or e1 e2 . . .) 7 (if e1 #t (or e2 . . .))

Beispiel:

(or a b c)
7 (if a #t (or b c))
7 (if a #t (if b #t (or c)))
7 (if a #t (if b #t (if c #t (or))))
7 (if a #t (if b #t (if c #t #f)))

2.7 Signaturdefinitionen

Nehmen wir uns zu bungszwecken noch eine weitere Aufgabe vor:


Nehmen wir an, jemand nimmt bei einem bestimmten Punktestand in
Flensburg an einer freiwilligen Manahme teil was ist der Punktestand
nach der Manahme? Die bekannten Gren sind:

Punktestand vor der Manahme (natrliche Zahl)


freiwillige Manahme (siehe Abschnitt 2.1)

Die unbekannte Gre ist der Punktestand nach der Manahme.


Die Kurzbeschreibung knnte so lauten:

; Punktestand in Flensburg senken

Die Signatur folgt aus der Datenanalyse:

(: improve-points (natural (one-of "nichts"


"Aufbauseminar"
"verkehrspsychologische Beratung"
"Fhrerscheinentzug")
-> natural))

Der one-of-Teil der Signatur macht sich da ganz schn breit, zumal
er sich weitgehend deckt mit dem entsprechenden Teil der Signatur
von points-must-do auf Seite 16. Entsprechend sollten wir genauso wie
bei anderen Werten der Signatur fr Flensburg-Manahmen einen
Namen geben. Das geht mit einer fast ganz normalen Definition:

(define action
(signature
(one-of "nichts"
"Aufbauseminar"
"verkehrspsychologische Beratung"
"Fhrerscheinentzug")))
22 Kapitel 2

Das Wrtchen signature ist aus technischen Grnden ntig.2 Faustre-


gel: Signaturen auerhalb von Formen (: ...) mssen immer in ein
(signature ...) eingeschachtelt werden..
Mit dieser Definition gewappnet knnen wir die Signatur abkrzen:

(: improve-points (natural action -> natural))

Entsprechend Mantra 3 versuchen wir, durch mehr Tests als noch bei
points-must-do bessere Abdeckung zu erzielen:

(check-expect (improve-points 3 "Aufbauseminar") 3)


(check-expect (improve-points 4 "nichts") 4)
(check-expect (improve-points 4 "Aufbauseminar") 0)
(check-expect (improve-points 8 "Aufbauseminar") 4)
(check-expect (improve-points 9 "Aufbauseminar") 7)
(check-expect (improve-points 13 "Aufbauseminar") 11)
(check-expect (improve-points 14 "verkehrspsychologische Beratung") 12)
(check-expect (improve-points 17 "verkehrspsychologische Beratung") 15)
(check-expect (improve-points 18 "Aufbauseminar") 18)
(check-expect (improve-points 18 "verkehrspsychologische Beratung") 18)

Hier das Gerst:

(define improve-points
(lambda (p a)
...))

Bei der Konstruktion der Schablone mssen wir uns entscheiden, an


welchem Parameter wir uns orientieren, p oder a. Die Entscheidung ist
willkrlich wir entscheiden uns erst einmal fr p. (Ausgehend von a
kommt eine andere aber genauso gute Lsung heraus das sei Ihnen
in Aufgabe ?? als Fingerbung empfohlen.) Bei p gibt es in Bezug auf
diese Aufgabe fnf Kategorien:

03 Punkte Da bringt keine Manahme etwas.


48 Punkte Da bringt ein Aufbauseminar 4 Punkte Abzug.
913 Punkte Da bringt ein Aufbauseminar 2 Punkte Abzug.
1417 Punkte Da bringt eine verkehrspsychologische Beratung 2 Punkte
Abzug.
ber 18 Punkte Auch hier hilft keine Manahme.

Wir brauchen also ein cond mit fnf Zweigen:

(define improve-points
(lambda (p a)
(cond
(... ...)
(... ...)
(... ...)
(... ...)
(... ...))))

2 Es sorgt unter anderem dafr, da Signaturdefinitionen in beliebiger Reihenfolge ge-


schrieben werden und die Links in den Fehlermeldungen von DrRacket auf die richtige
Stelle zeigen.
Fallunterscheidungen und Verzweigungen 23

Jetzt mssen wir Tests erfinden, die den Kategorien entsprechen:


(define improve-points
(lambda (p a)
(cond
((<= p 3) ...)
((and (>= p 4) (<= p 8)) ...)
((and (>= p 9) (<= p 13)) ...)
((and (>= p 14) (<= p 17)) ...)
((>= p 18) ...))))

Wir fangen mal mit den einfachsten Fllen an unten und oben in der
Punkteskala, wo sich nichts bewegt:
(define improve-points
(lambda (p a)
(cond
((<= p 3) p)
((and (>= p 4) (<= p 8)) ...)
((and (>= p 9) (<= p 13)) ...)
((and (>= p 14) (<= p 17)) ...)
((>= p 18) p))))

Im zweiten Zweig zwischen vier und acht Punkten zhlt nur ein
Aufbauseminar, alle anderen Manahmen bringen nichts. Darum ist
hier eine binre Verzweigung angemessen:
(define improve-points
(lambda (p a)
(cond
...
((and (>= p 4) (<= p 8))
(if (string=? a "Aufbauseminar")
(- p 4)
p))
...)))

Entsprechend funktionieren auch der dritte und der vierte Zweig:


(define improve-points
(lambda (p a)
(cond
((<= p 3) p)
((and (>= p 4) (<= p 8))
(if (string=? a "Aufbauseminar")
(- p 4)
p))
((and (>= p 9) (<= p 13))
(if (string=? a "Aufbauseminar")
(- p 2)
p))
((and (>= p 14) (<= p 17))
(if (string=? a "verkehrspsychologische Beratung")
(- p 2)
p))
((>= p 18) p))))
24 Kapitel 2

Fertig! Es gibt trotzdem noch einen Wermutstropfen: Die Abdeckung ist


trotz der vielen Tests immer noch nicht vollstndig siehe Aufgabe ??

2.8 Unsinnige Daten abfangen

Noch einmal zurck zum Parkplatzproblem, das wir auf Seite ?? pro-
grammiert hatten. In Abschnitt 1.9 auf Seite 12 hatten wir bereits be-
merkt, da die Prozedur parking-lot-cars auch fr unsinnige Daten
frhlich ebenso unsinnige Ergebnisse ermittelt.
Auf Seite ?? wurde bereits eine Bedingung fr sinnvolle Daten formu-
liert: Wenn n die Anzahl der Fahrzeuge und m die Anzahl der Rder
ist, dann mu m gerade sein sowie 2n m 4n gelten. Wir knnen
das in einer binren Verzweigung zum Ausdruck bringen:
(define parking-lot-cars
(lambda (vehicle-count wheel-count)
(if (and (even? wheel-count)
(<= (* 2 vehicle-count) wheel-count)
(<= wheel-count (* 4 vehicle-count)))
(/ (- wheel-count (* 2 vehicle-count))
2)
...)))

Die eingebaute Prozedur even? akzeptiert eine ganze Zahl und liefert
#t, falls die Zahl gerade ist und #f, falls sie ungerade ist solche und
viele andere ntzliche Prozeduren finden Sie in der Dokumentation
im Hilfezentrum unter Sprachebenen und Material zu Die Macht der
Abstraktion im Abschnitt Primitive Operationen.
Nur was tun im Fehlerfall? Dazu gibt eine eingebaute Prozedur
violation, die eine Fehlermeldung als Zeichenkette akzeptiert und,
wenn sie aufgerufen wird, das Programm abbricht und die Fehlermel-
dung ausdruckt. Parking-lot-cars sieht dann vollstndig so aus:
(define parking-lot-cars
(lambda (vehicle-count wheel-count)
(if (and (even? wheel-count)
(<= (* 2 vehicle-count) wheel-count)
(<= wheel-count (* 4 vehicle-count)))
(/ (- wheel-count (* 2 vehicle-count))
2)
(violation "unsinnige Daten"))))

Natrlich sollten wir auch den Fehlerfall testen das geht nicht mit
check-expect, das ja erwartet, da ein Testausdruck einen ordnungs-
gemen Wert liefert. Fr Fehlerflle gibt es check-error, das Testflle
erzeugt, die dann bestanden sind, wenn die Auswertung einen Fehler
liefert:
(check-error (parking-lot-cars 10 10)) ; zu wenige Rder
(check-error (parking-lot-cars 3 9)) ; ungerade Rderzahl
(check-error (parking-lot-cars 2 10)) ; zu viele Rder

Aufgaben

TBD
3 Zusammengesetzte Daten

TBD
Mit anderen Worten: mehrere Dinge werden zu einem zusammenge-
setzt. Eine andere Betrachtungsweise ist, da ein einzelnes Ding durch
mehrere Eigenschaften charakterisiert ist.
In Scheme lassen sich solche zusammengesetzte Daten durch Records
darstellen. Ein Record ist wie ein Behlter mit mehreren Fchern, in
denen die Bestandteile der Daten untergebracht sind.

3.1 Computer konfigurieren

Viele Computerhndler erlauben Ihre Kunden, bestimmte Komponen-


ten eines neues Computers selbst auszuwhlen, zum Beispiel den Pro-
zessor, die Festplatte oder die Gre des RAM-Hauptspeichers:

Eine mgliche Beschreibung dieser Situation ist:


Ein Computer besteht aus:

Prozessor
RAM
Festplatte

Natrlich besteht ein Computer auch noch aus anderen Teilen, aber
diese Teile sind (zumindest in diesem Beispiel) immer gleich, sie knnen
vom Kunden nicht ausgewhlt werden. In einer Bestellung mu der
26 Kapitel 3

Kunde also nur diese drei Bestandteile angeben. Wir nehmen an, da
es beim Prozessor nur auf den Namen (Athlon, Xeon, Cell, . . . )
ankommt, beim RAM nur auf die Gre in Gigabyte, und auch bei der
Festplatte nur auf die Gre in Gigabyte. Eine vereinfachte Darstellung
knnte so aussehen:
Feld Komponente
Prozessor "Cell"
Computer:
RAM 2
Festplatte 250

Diese Tabelle steht demnach fr einen Computer mit Cell-Prozessor, 2


Gigabyte RAM und einer 250-Gigabyte-Festplatte.
Die Begriffe Feld und Komponente sind dabei Termini das Feld
ist die Allgemeinbezeichnung fr ein Bestandteil, das alle Computer
haben. Die Komponente ist das konkrete Bestandteil eines einzelnen
Computers.
Die Darstellung fr solche zusammengesetzte Daten, die aus mehreren
Komponenten (in diesem FallCell, 2 und 250) bestehen, heit Record.
Alle Records fr Computer gehren zu einer gemeinsamen Menge,
dem Record-Typ fr Computer. (Weiter hinten in diesem Kapitel wird
beschrieben, wie ein Programm eigene Record-Typen definieren kann.)
Der Record-Typ fr Computer sieht feste Felder (Prozessor, RAM
und Festplatte) vor, welche die Komponenten aufnehmen. Fr jedes
Feld des Record-Typs Computer besitzt also jeder einzelne Computer
jeweils eine Komponente, in diesem Fall eine fr das Prozessor-, eine
fr das RAM- und eine fr das Festplatten-Feld.
Der Computerhersteller stellt einen echten Computer her, indem
er zunchst den Prozessor, den RAM und die Festplatte fertigstellt
und diese dann zum Computer zusammensetzt. Umgekehrt nehmen
manche Bastler aus dem Computer die Einzelteile wieder heraus, zum
Beispiel, um sie in einem anderen Computer zu verbauen.
In der DrRacket-Sprachebene Die Macht der Abstraktion - Anfnger
sind Computer schon eingebaut. Ein Computer mit Cell-Prozessor,
2 Gigabyte RAM und 250 Gigabyte Festplatte wird folgendermaen
hergestellt:

(make-computer "Cell" 2 250)


, #<record:computer "Cell" 2 250>
Die Prozedur make-computer hat folgende Signatur:

(: make-computer (string rational rational -> computer))

Sie macht also aus einer Zeichenkette und zwei Zahlen einen Wert
der eingebauten Sorte computer der Computer-Records. Die DrRacket-
REPL druckt Record-Werte mit der Schreibweise #<record:... ...> aus,
damit Sorte und Komponenten sichtbar werden.
Computer sind Werte wie andere auch und lassen sich zum Beispiel
an Variablen binden:

(define gamer (make-computer "Cell" 4 1000)) ; Cell, 4 Gbyte RAM, 1000 Gbyte Festplat
gamer
, #<record:computer "Cell" 4 1000>
Zusammengesetzte Daten 27

(define workstation (make-computer "Xeon" 2 500)) ; Xeon, 2 Gbyte RAM, 500 Gbyte Festplatte
workstation
, #<record:computer "Xeon" 2 500>
Da die Prozedur make-computer einen Computer konstruiert, heit sie
auch Konstruktor. Fr das Zerlegen von Computern sind die Prozeduren
computer-processor, computer-ram und computer-hard-drive zustndig:

(computer-processor gamer)
, "Cell"
(computer-ram gamer)
, 4
(computer-hard-drive gamer)
, 1000

Diese drei Prozeduren extrahieren die Bestandteile aus einem Computer


und heien Selektoren. Sie haben folgende Signaturen:
(: computer-processor (computer -> string))
(: computer-ram (computer -> rational))
(: computer-hard-drive (computer -> rational))

Mit Hilfe des Konstruktors und der Selektoren kann der Programmierer
weitergehende Prozeduren definieren. Fr den Anfang knnte das eine
Prozedur sein, die den Gesamtspeicher eines Computers berechnet,
also Hauptspeicher und Festplattenspeicher zusammen. Eine solche
Prozedur mte Kurzbeschreibung und Signatur wie folgt haben:
; Gesamtspeicher berechnen
(: total-memory (computer -> rational))

Hier sind unsere Erwartungen an total-memory, als Testflle formuliert:


(check-expect (total-memory workstation) 502)
(check-expect (total-memory gamer) 1004)

Das Gerst mte folgendermaen sein:


(define total-memory
(lambda (c)
...))

Da in den Gesamtspeicher des Computer sowohl der Hauptspeicher als


auch die Festplatte eingehen, steht schon fest, da die entsprechenden
Selektoraufrufe im Rumpf der Prozedur vorkommen mssen:
(define total-memory
(lambda (c)
... (computer-ram c) ...
... (computer-hard-drive c) ...))

Das Gesamtspeicher ergibt sich aus Addition der beiden Komponenten:


(define total-memory
(lambda (c)
(+ (computer-ram c)
(computer-hard-drive c))))
28 Kapitel 3

Fertig!
Total-memory ist ein Beispiel fr eine Prozedur, die einen Record ak-
zeptiert. Umgekehrt gibt es auch Prozeduren, die Records produzieren.
Angenommen, unser Computerhndler bietet neben der Einzelkonfigu-
ration von Prozessor, Hauptspeicher und Festplatte einige Standardmo-
delle an sagen wir, ein Billigmodell, ein Modell fr Profis (was immer
ein Profi sein mag) und ein Modell fr Computerspieler. Je nach-
dem, welches der Modelle der Kunde auswhlt, mu die entsprechende
Konfiguration zusammengesetzt werden. Fr die Standardkonfigurati-
on gibt es drei feste Mglichkeiten, es handelt sich hier also um eine
Aufzhlung.
Eine Prozedur, die zu einer Standardkonfiguration den passenden
Computer fertigt, knnte folgende Kurzbeschreibung und Signatur
haben:
; Standard-Computer zusammenstellen
(: standard-computer ((one-of "cheap" "professional" "game") -> computer))

Die Testflle sollten alle drei Standardkonfigurationen abdecken:


(check-expect (standard-computer "cheap") (make-computer "Sempron" 2 500))
(check-expect (standard-computer "professional") (make-computer "Xeon" 4 1000))
(check-expect (standard-computer "game") (make-computer "Quad" 4 750))

Hier ist das Gerst:


(define standard-computer
(lambda (k)
...))

Da es sich beim Argument um eine Fallunterscheidung eine Aufzh-


lung mit drei Alternativen handelt, knnen wir die dazu passende
Schablone eine Verzweigung mit drei Zweigen zum Einsatz bringen:
(define standard-computer
(lambda (k)
(cond
(... ...)
(... ...)
(... ...))))

Bei den Tests der Zweige mssen wir k mit den Elementen der Aufzh-
lung vergleichen. Da es sich um Zeichenketten handelt, nehmen wir
dazu string=?:
(define standard-computer
(lambda (k)
(cond
((string=? k "cheap") ...)
((string=? k "professional") ...)
((string=? k "game") ...))))

In jedem Zweig mssen wir nun dafr sorgen, da der entsprechende


Computer hergestellt wird. Fr das Herstellen von Computer-Records
ist der Konstruktor make-computer zustndig. Dementsprechend mssen
wir in jedem Zweig einen Aufruf an make-computer plazieren:
Zusammengesetzte Daten 29

(define standard-computer
(lambda (k)
(cond
((string=? k "cheap")
(make-computer ... ... ...))
((string=? k "professional")
(make-computer ... ... ...))
((string=? k "game")
(make-computer ... ... ...)))))

Jetzt mssen wir nur noch die Argumente fr die Aufrufe von make-computer
zur Verfgung stellen. Fr jeden Aufruf sind das, wie gehabt, der Pro-
zessor, die Gre des Hauptspeichers und die Gre der Festplatte.
Die entsprechenden Angaben knnen wir zum Beispiel den Testfllen
entnehmen, und es kommt folgendes dabei heraus:

(define standard-computer
(lambda (k)
(cond
((string=? k "cheap")
(make-computer "Sempron" 2 500))
((string=? k "professional")
(make-computer "Xeon" 4 1000))
((string=? k "game")
(make-computer "Quad" 4 750)))))

Fertig!

3.2 Record-Definitionen

Natrlich sind zusammengesetzte Daten in Scheme nicht auf Computer-


Konfigurationen beschrnkt der Programmierer kann neue Arten
zusammengesetzter Daten selbst definieren. Voraussetzung fr die
Definition einer neuen Art zusammengesetzter Daten ist eine klare Vor-
stellung davon, was die Komponenten sind. Dabei hilft eine informelle
Beschreibung wie diese hier:

; Eine kartesische Koordinate in der Ebene besteht aus einer X- und Y-Koordinate.

Eine solche Datendefinition lt sich direkt in eine Record-Definition in


Scheme bersetzen. Dies ist eine Form mit dem syntaktischen Schlssel-
wort define-record-procedures. Eine Record-Definition definiert einen
neuen Record-Typ und dabei automatisch auch u. a. den Konstruktor
und die Selektoren nur ihre Namen mssen angegeben werden.
TBD Eine define-record-procedures-Form hat folgende allgemeine
Gestalt:

(define-record-procedures t
c p
(s1 . . . sn ))

Diese Form definiert einen Record-Typ mit n Feldern. Dabei sind t, c, p,


s1 . . . sn allesamt Variablen, fr die define-record-procedures Definitio-
nen anlegt:
30 Kapitel 3

t ist der Name des Record-Typs.


c ist der Name des Konstruktors, den define-record-procedures an-
legt.
p ist der Name des Prdikats, das define-record-procedures anlegt.
s1 , . . . , sn sind die Namen der Selektoren fr die Felder des Record-
Typen.
Beim Entwurf einer Record-Definition hilft es, mit der Datendefinition
anzufangen, die ausfhrlich beschreibt, was fr Komponenten die Daten
haben. Fr Computer sieht diese Datendefinition folgendermaen aus:
; Ein Computer besteht aus:
; - Prozessor (string)
; - Hauptspeicher-Kapazitt in Gbyte (rational)
; - Festplatten-Kapazitt in Gbyte (rational)

Hier ist die Datendefinition fr kartesische Koordinaten:


; Eine kartesische Koordinate besteht aus:
; - X-Anteil (real)
; - Y-Anteil (real)

Die Datendefinition muss genau soviele Komponenten aufweisen, wie


die zusammengesetzten Daten Bestandteile haben. Wenn es nicht ab-
solut glaskar ist, sollten in Klammern die Signaturen der jeweiligen
Komponenten angegeben werden wie in diesen Beispielen.
Aus der Datendefinition ergibt sich direkt die Record-Definition. Ins-
besondere gehrt zu jeder Komponente ein Selektor. Die Namen fr
den Konstruktor, das Prdikat und die Selektoren knnen frei gewhlt
werden, sollten aber meist einer einheitlichen Konvention folgen, um
anderen das Lesen des Programms zu erleichern. Die gngige Kon-
vention ist, da der Konstruktor mit make- anfngt (make-cartesian),
der Name des Prdikats auf ein Fragezeichen endet (cartesian?), und
die Selektoren mit dem Namen des Record-Typs beginnen und auf die
Namen der Felder enden (cartesian-x, cartesian-y).
TBD

3.3 Schablonen fr zusammengesetzte Daten

Zusammengesetzte Daten knnen Sie an Formulierungen wie ein


X besteht aus . . . , ein X ist charakterisiert durch . . . oder ein X
hat folgende Eigenschaften: . . . erkennen. Diese Formulierung bil-
det dann ordentlich aufgeschrieben und ggf. um die Signaturen fr
die Komponenten ergnzt das Herzstck der Datendefinition. Diese
Datendefinition knnen Sie dann direkt in die dazugehrige Record-
Definition bersetzen. Diese muss genauso viele Felder haben wie die
Datendefinition Komponenten beschreibt.
Diese Methode bildet Konstruktionsanleitung ?? in Anhang ??.

3.3.1 Zusammengesetzte Daten als Eingabe

Die Definitionen von total-price folgen dem selben Muster. Dieses


Muster ergibt Schablonen fr Prozeduren, die Records als Argumente
akzeptieren und lt sich auch auf andere Record-Typen folgenderma-
en in eine Konstruktionsanleitung bertragen:
Zusammengesetzte Daten 31

1. Stellen Sie fest, von welchen Komponenten des Records das Ergebnis
der Prozeduren abhngt.
2. Fr jede dieser Komponenten, schreiben Sie (s c) in die Schablone,
wobei s der Selektor der Komponente und c der Name des Record-
Parameters ist.
3. Vervollstndigen Sie die Schablone, indem Sie einen Ausdruck kon-
struieren, in dem die Selektor-Anwendungen vorkommen.

Konstruktionsanleitung ?? in Anhang ?? fat diese Schritte noch einmal


zusammen.

3.3.2 Zusammengesetzte Daten als Ausgabe

Prozeduren, die zusammengesetzte Daten als Ausgabe haben, mssen


einen entsprechenden Record konstruieren, mithin den Konstruktor
aufrufen. Die Schablone wird also folgendermaen konstruiert:
Wenn die Prozedur zusammengesetzte Daten als Ausgabe hat, schreiben Sie einen
Aufruf des passenden Record-Konstruktors in den Rumpf, zunchst mit einer
Ellipse fr jedes Feld des Records.

Im nchsten Schritt ersetzen Sie dann die Ellipsen durch Ausdrcke,


welche die entsprechenden Komponenten berechnen.
Konstruktionsanleitung ?? in Anhang ?? fat diese Schritte noch
einmal zusammen.

3.4 Grteltiere im Computer

Nachdem wir aus den Beispielen die Schablonen fr zusammengesetzte


Daten entwickelt haben, demonstrieren wir diese in diesem Abschnitt
noch einmal an einem frischen Beispiel:
In Texas gibt es viele Grteltiere, die insbesondere die Highways ber-
queren und dabei leider oft berfahren werden am Straenrand sind
entsprechend viele Grteltiere zu sehen. Auerdem fttern freundliche
Autofahrer gelegentlich die Grteltiere. Mit diesen beiden Aspekte wol-
len wir uns beschftigen: Was passiert, wenn ein Grteltier berfahren
wird? Was passiert, wenn ein Grteltier gefttert wird? Entsprechend
interessiert uns, ob ein Grteltier am Leben ist und welches Gewicht es
hat. Das knnen wir direkt in eine Datendefinition bersetzen:

; Ein Grteltier hat folgende Eigenschaften:


; - Gewicht (in g)
; - lebendig oder tot

Wiederum handelt es sich sichtlich um zusammengesetzte Daten. Dies-


mal trifft die Phrase besteht aus natrlich nicht zu. Stattdessen geht
es um die Eigenschaften, von denen ein Grteltiert viele hat. Von diesen
vielen interessanten Eigenschaften sind aber viele bei allen Grteltieren
gleich (Sugetier, zwei Augen, vier Fe etc.) und darum nicht Teil
der Datendefinition. Fr unsere Aufgaben sind auerdem nur zwei
Eigenschaften von Belang, weshalb die Datendefinition auch nur diese
auflistet.
Aus der Datendefinition knnen wir direkt eine passende Record-
Definition machen:
32 Kapitel 3

(define-record-procedures dillo
make-dillo dillo?
(dillo-weight dillo-alive?))

(Dillo steht kurz fr Armadillo, englisch fr Grteltier.)


Fr das Feld alive? knnten wir unterschiedliche Reprsentationen
whlen: Eine Aufzhlung wre mglich; die Autoren haben sich fr
einen booleschen Wert entschieden, der die Frage Lebt das Grteltier?
beantwortet. Hier sind die Signaturen fr die Record-Prozeduren:
(: make-dillo (natural boolean -> dillo))
(: dillo? (any -> boolean))
(: dillo-weight (dillo -> natural))
(: dillo-alive? (dillo -> boolean))

Dabei bedeutet any, dass an dieser Argumentstelle beliebige Daten


auftreten drfen. Riesengrteltiere werden um die 60kg schwer. Hier
sind einige Exemplare:
(define d1 (make-dillo 55000 #t)) ; 55kg, lebendig
(define d2 (make-dillo 58000 #f)) ; 58kg, tot
(define d3 (make-dillo 60000 #t)) ; 60kg, lebendig
(define d4 (make-dillo 63000 #f)) ; 63kg, tot

Fangen wir mit dem unangenehmen Teil an, dem berfahren, das aus
einem lebenden Grteltier ein totes macht. Hier Kurzbeschreibung und
Signatur:
; Grteltier berfahren
(: run-over-dillo (dillo -> dillo))

Aus dem Beispiel d1 knnen wir den ersten Testfall machen:


(check-expect (run-over-dillo d1) (make-dillo 55000 #f))

Wir sollten aber auch bercksichtigen, was run-over-dillo mit toten


Grteltieren anstellt: Diese bleiben auch nach dem berfahren tot:
(check-expect (run-over-dillo d2) d2)

Hier das Gerst der Prozedur:


(define run-over-dillo
(lambda (d)
...))

Run-over-dillo hat zusammengesetzte Daten sowohl als Eingabe als


auch als Ausgabe. Entsprechend kommen die Schablonen fr beide
Situationen zum Einsatz. Zunchst die Schablone fr zusammengesetzte
Daten als Eingabe; wir schreiben die Aufrufe der Selektoren auf:
(define run-over-dillo
(lambda (d)
... (dillo-weight d) ...
... (dillo-alive? d) ...))

Dazu kommt die Schablone fr zusammengesetzte Daten als Ausgabe,


also der Aufruf des Konstruktors:
Zusammengesetzte Daten 33

(define run-over-dillo
(lambda (d)
(make-dillo ... ...)
... (dillo-weight d) ...
... (dillo-alive? d) ...))

Wir mssen beim Aufruf des Konstruktors make-dillo angeben, welches


Gewicht das frisch berfahrene Grteltier haben soll und ob es noch
am Leben ist. Da das berfahren das Gewicht nicht ndert, bernimmt
der Ausdruck fr das Gewicht das Gewicht des Eingabe-Grteltiers aus
der Schablone:
(define run-over-dillo
(lambda (d)
(make-dillo (dillo-weight d) ...)
... (dillo-alive? d) ...))

Das Grteltier ist nach dem berfahren auf jeden Fall tot. Da es keine
Rolle spielt, ob das Grteltier vorher lebendig war oder nicht, knnen
wir den Selektoraufruf (dillo-alive? d) verwerfen:
(define run-over-dillo
(lambda (d)
(make-dillo (dillo-weight d)
#f)))

Fertig!
Nchste Aufgabe: Grteltier fttern. Die Standard-Futter-Portion ist
dabei 500g, und das Grteltier nimmt durch die Ftterung um das
entsprechende Gewicht zu. Hier Kurzbeschreibung und Signatur:
; Grteltier mit 500g Futter fttern
(: feed-dillo (dillo -> dillo))

Hier der erste, naheliegende Testfall:


(check-expect (feed-dillo d1) (make-dillo 55500 #t))

Auch bei feed-dillo ist relevant, was es mit toten Grteltieren macht:
Tote Grteltiere fressen nicht, entsprechend nehmen sie auch nicht zu,
wenn man ihnen Futter anbietet:
(check-expect (feed-dillo d2) d2)

Hier das Gerst der Prozedur:


(define feed-dillo
(lambda (d)
...))

Feed-dillohat die gleiche Signatur wie run-over-dillo; entsprechend


benutzen wir die gleiche Schablone:
(define feed-dillo
(lambda (d)
(make-dillo ... ...)
... (dillo-weight d) ...
... (dillo-alive? d) ...))
34 Kapitel 3

Beim zweiten Testfall haben wir gesehen, da, was feed-dillo betrifft,
die Grteltiere in zwei verschiedene Gruppen fallen: Feed-dillo verhlt
sich bei lebenden Grteltieren anders als bei toten: eine Fallunterschei-
dung. Entsprechend brauchen wir eine Verzweigung im Rumpf. Da
sich der Fall Grteltier tot dadurch definiert, da der Fall Grteltier
lebendig nicht eintritt, ist die binre Verzweigung angemessen:
(define feed-dillo
(lambda (d)
(if (dillo-alive? d)
...
...)
(make-dillo ... ...)
... (dillo-weight d) ...

Nun mssen wir noch die beiden Zweige ergnzen. Am einfachsten


ist die Alternative Grteltier tot, dann nmlich kommt das gleiche
Grteltier aus der Prozedur, das hineingegangen ist:

(define feed-dillo
(lambda (d)
(if (dillo-alive? d)
...
d)
(make-dillo ... ...)
... (dillo-weight d) ...

Im ersten Zweig mssen wir schlielich einen neuen Grteltier-Wert be-


rechnen, der die Zunahme bercksichtigt. Dabei werden der Konstruktur-
Aufruf und der zweite Selektor-Aufruf aus der Schablone verbraucht:

(define feed-dillo
(lambda (d)
(if (dillo-alive? d)
(make-dillo (+ (dillo-weight d) 500)
#t)
d)))

Fertig!

Aufgaben

TBD
4 Gemischte Daten

Neben den zusammengesetzten Daten verarbeiten Programme hufig


auch gemischte Daten, die verschiedene Formen annehmen knnen, aber
grundlegende Gemeinsamkeiten ausweisen:

Ein Tier kann ein Grteltier oder ein Papagei sein.


Eine Koordinate kann eine kartesische Koordinate oder eine Polarko-
ordinate sein.
Ein Essen kann ein Frhstck, Mittagessen oder Abendessen sein.

Obwohl die Daten verschiedenartig sind, untersttzen sie doch gemein-


same Operationen: Das Gewicht eines Tiers kann sowohl fr Grteltiere
als auch Papageien berechnet werden, der Abstand vom Ursprung
kann fr beide Koordinatendarstellungen berechnet werden, die Menge
Vitamin A kann fr jede Art Essen bestimmt werden, etc.

4.1 Gemischte Daten

In der Einleitung war die Rede von Papageien: die benutzen wir, um
gemischte Daten einzufhren. Vorher mssen wir jedoch Papageien
mit den bekannten Mitteln definieren; wir versuchen es, kurz und
schmerzlos zu machen:
Wir interessieren uns vor allem fr sprechende Papageien. Genau
wie bei Grteltieren interessiert uns das Gewicht, aber wir nehmen an,
da Papageien in der Regel nicht auf texanischen Highways berfahren
werden, da sie immer lebendig sind. Hier die Datendefinition:

; Ein Papagei hat folgende Eigenschaften:


; - Gewicht in Gramm (natural)
; - Satz, den er sagt (string)

Hier die dazu passende Record-Definition:

(define-record-procedures parrot
make-parrot parrot?
(parrot-weight parrot-sentence))

. . . und die passenden Signaturen:

(: make-parrot (natural string -> parrot))


(: parrot? (any -> boolean))
(: parrot-weight (parrot -> natural))
(: parrot-sentence (parrot -> string))
36 Kapitel 4

Hier zwei Beispiele fr Papageien:

(define p1 (make-parrot 10000 "Der Grtner wars.")) ; 10kg, Miss Marple


(define p2 (make-parrot 5000 "Ich liebe Dich.")) ; 5kg, Romantiker

Wir knnen einen Papagei hnlich wie ein Grteltier fttern nur
die Portion ist kleiner, wir nehmen 50 g an. Kurzbeschreibung und
Signatur:

; Papagei mit 50 g Futter fttern


(: feed-parrot (parrot -> parrot))

Testflle:

(check-expect (feed-parrot p1) (make-parrot 10050 "Der Grtner wars."))


(check-expect (feed-parrot p2) (make-parrot 5050 "Ich liebe Dich."))

Gerst:

(define feed-parrot
(lambda (p)
...))

Schablone:

(define feed-parrot
(lambda (p)
(make-parrot ... ...)
... (parrot-weight p) ...
... (parrot-sentence p) ...))

. . . und schlielich der vollstndige Rumpf:

(define feed-parrot
(lambda (p)
(make-parrot (+ (parrot-weight p) 50)
(parrot-sentence p))))

Fertig! Sie knnen sich vielleicht vorstellen, da Papageien und Gr-


teltiere sich in einem Programm begegnen, also gemischt vorkommen.
Papageien und Grteltiere gehren zum gemeinsamen Oberbegriff
Tier, den wir im Rahmen eines solchen Programms folgendermaen
beschreiben knnten:
Ein Tier ist eins der folgenden:

ein Grteltier
ein Papagei

Die Formulierung eins der folgenden ist der klare Hinweis fr eine neue
Form der Organisation von Daten: gemischte Daten. Entsprechend ist es
durchaus sinnvoll, nach dem Gewicht eines Tiers zu fragen oder ein
Tier zu fttern, was wir im folgenden auch vorhaben.
Die Beschreibung des Begriffs Tier ist bereits als Datendefinition
geeignet, und mu fr Inklusion im Programm nur als Kommentar
umformatiert werden:
Gemischte Daten 37

; Ein Tier ist eins der folgenden:


; - Grteltier
; - Papagei

Fr Grteltiere und Papageien sind durch die jeweiligen Record-Definitionen


bereits Signaturen definiert. Auch fr die Sorte der Tiere werden wir ei-
ne Signatur brauchen: Schlielich braucht die Prozedur, die das Gewicht
ausrechnen soll, eine Signatur wie die folgende:
(: animal-weight (animal -> natural))

Wir brauchen also eine Definition fr die Signatur animal. Diese sieht
folgendermaen aus:
(define animal
(signature
(mixed dillo parrot)))

Das signature kennen wir von den Fallunterscheidungen aus Ab-


schnitt 2.7. Das mixed ist neu und steht fr gemischte Daten. Sie
knnen die obige Definition lesen als Tiere sind gemischt aus Grtel-
tieren und Papageien; das klingt aber auf Deutsch hlzern, weshalb
wir fr die Datendefinition bei der Formulierung eins der folgenden
bleiben. Mit der Definition steht die Signatur animal zur Verfgung.
Wir haben schon mit der Signatur fr animal-weight vorgegriffen. Hier
ist sie noch einmal zusammen mit einer Kurzbeschreibung:
; Gewicht eines Tiers feststellen
(: animal-weight (animal -> natural))

Diese Prozedur sollte entsprechend fr Grteltiere und Papageien funk-


tionieren, wir brauchen also Testflle fr beide:
(check-expect (animal-weight d1) 55000)
(check-expect (animal-weight d2) 58000)
(check-expect (animal-weight p1) 10000)
(check-expect (animal-weight p2) 5000)

Das Gerst sieht so aus:


(define animal-weight
(lambda (a)
...))

Im Rumpf mssen wir zwischen Grteltieren und Papageien unter-


scheiden: Schlielich mu animal-weight die Prozedur dillo-weight fr
Grteltiere, fr Papageien aber die Prozedur parrot-weight aufrufen.
Wir brauchen also wie bei Fallunterscheidungen eine Verzweigung,
und zwar mit einem Zweig fr Grteltiere und einem Zweig fr Papa-
geien:

(define animal-weight
(lambda (a)
(cond
(... ...)
(... ...))))
38 Kapitel 4

Wir brauchen als nchstes einen Test, der Grteltiere identifiziert. Jetzt
endlich kommen die auf Seite ?? eingefhrten Prdikate dillo? und
parrot? ins Spiel, die durch die Record-Definitionen definiert wurden:

(define animal-weight
(lambda (a)
(cond
((dillo? a) ...)
((parrot? a) ...))))

Im ersten Zweig dem Zweig fr Grteltiere kommt nun dillo-weight


zum Einsatz, im zweiten Zweig fr Papageien ist parrot-weight
zustndig:

(define animal-weight
(lambda (a)
(cond
((dillo? a) (dillo-weight a))
((parrot? a) (parrot-weight a)))))

Fertig!
Aus diesem Beispiel ergibt sich direkt eine Konstruktionsanleitung
fr gemischte Daten. Zunchst zur Daten- und Signaturdefinition:

Gemischte Daten liegen vor, wenn Sie eine Datensorte durch eine
Formulierung der Form ein X ist eins der folgenden beschreiben
knnen: Diese Formulierung ist die Datendefinition.
Stellen Sie fest, wieviele unterschiedliche Flle die Sorte fr die ge-
mischten Daten hat.
Schreiben Sie eine Signaturdefinition der folgenden Form unter die
Datendefinition:

(define S
(signature
(mixed S1 S2 . . . Sn )))

Dabei ist S die Signatur fr die neue Datensorte; S1 bis Sn sind


die Signaturen fr die Signaturen, aus denen die neue Datensorte
zusammengemischt ist.

Eine Schablone fr eine Prozedur und deren Testflle, die gemischte


Daten akzeptiert, knnen Sie folgendermaen konzentrieren:

Schreiben Sie Tests fr jeden der Flle.


Schreiben Sie eine cond-Verzweigung als Rumpf in die Schablone, die
genau n Zweige hat also genau soviele Zweige, wie es Flle gibt.
Schreiben Sie fr jeden Zweig einen Test, der den entsprechenden
Fall identifiziert.
Vervollstndigen Sie die Zweige, indem Sie eine Datenanalyse fr
jeden einzelnen Fall vornehmen und entsprechende Hilfsprozedu-
ren oder Konstruktionsanleitungen benutzen. Die bersichtlichsten
Programme entstehen meist, wenn fr jeden Fall separate Hilfsproze-
duren definiert sind.
Gemischte Daten 39

Konstruktionsanleitung ?? fr gemischte Daten in Anhang ?? fat dies


noch einmal zusammen.
Eine Konstruktionsanleitung oder Schablone fr gemischte Daten als
Ausgabe ist unntig Sie benutzen einfach die Schablone des entspre-
chenden Falls.
Beachten Sie den Unterschied zwischen one-of und mixed, die leicht
zu verwechseln sind: One-of steht fr einer der folgenden Werte,
whrend mixed fr gehrend zu einer der folgenden Signaturen gehrt.

4.2 Die Zucker-Ampel

In diesem Abschnitt nehmen wir uns noch ein weiteres Beispiel fr


gemischte Daten vor, diesmal von vorneherein unter Benutzung der
Konstruktionsanleitung aus dem vorigen Abschnitt.
Einige Lnder der Europischen Union planen zum Zeitpunkt der
Drucklegung dieses Buches, den Gehalt bestimmter Inhaltsstoffe von
Lebensmitteln vereinfacht durch eine sogenannte Ampel darzustellen.
Bei Zucker zum Beispiel sieht die Ampel so aus, bezogen auf 100 g
eines Lebensmittels:
grn niedriger Gehalt weniger als 5 g
gelb mittlerer Gehalt zwischen 5 g und 12,5 g
rot hoher Gehalt mehr als 12,5 g

Uns geht es nicht darum, ob solch eine Kennzeichnung sinnvoll ist oder
nicht, oder ob Zucker gesund oder ungesund ist. Auf jeden Fall sind
trotz der Bemhungen der Europischen Union die Bezeichnungen un-
einheitlich. Technisch gesehen ist die Ampel natrlich redundant, wenn
der Zuckergehalt in Gramm angegeben ist. Manchmal ist allerdings
der Zuckergehalt auch separat fr Fruktose und Glukose angegeben.
Ein Computerprogramm knnte aber den Umgang erleichtern, indem
es jede Angabe auf einer Lebensmittelpackung Zucker in Gramm
insgesamt, Fruktose und Glukose separat sowie die Ampel in die
einheitliche Ampel-Form bringt.
Zunchst die Datenanalyse:
Den Zuckergehalt in Gramm insgesamt kann eine (rationale) Zahl
reprsentieren.
Die Zuckerangabe mit Fruktose und Glukose separat ist zusammen-
gesetzt, mit zwei Komponenten. Hier Daten- und Record-Definition
dazu sowie die Signaturen fr die Record-Prozeduren:

; Zuckeranteile bestehen aus:


; - Fruktose-Anteil (in g)
; - Glukose-Anteil (in g)
(define-record-procedures sugars
make-sugars sugars?
(sugars-fructose sugars-glucose))
(: make-sugars (rational rational -> sugars))
(: sugars? (any -> boolean))
(: sugars-fructose (sugars -> rational))
(: sugars-glucose (sugars -> rational))

Hier einige Beispiele:


40 Kapitel 4

(define s1 (make-sugars 1 1)) ; 1 g Fruktose, 1 g Glukose


(define s2 (make-sugars 2 3)) ; 2 g Fruktose, 3 g Glukose
(define s3 (make-sugars 5 5)) ; 5 g Fruktose, 5 g Glukose
(define s4 (make-sugars 10 2.5)) ; 10 g Fruktose, 2.5 g Glukose
(define s5 (make-sugars 10 3)) ; 10 g Fruktose, 3 g Glukose
(define s6 (make-sugars 15 10)) ; 15 g Fruktose, 10 g Glukose

Bei der Ampel selbst handelt es sich um eine einfache Fallunterschei-


dung:
; Eine Ampelbezeichnung ist eins der folgenden:
; - rot
; - gelb
; - grn
(define traffic-light
(signature
(one-of "rot" "gelb" "grn")))

Die Angabe ber den Zuckergehalt kann jede der drei oben genannten
Formen annehmen:
; Eins Zuckergehalt ist eins der folgenden:
; - Gewicht in Gramm (rational)
; - Zuckeranteile (sugars)
; - Ampelbezeichnung (traffic-light)
(define sugar-content
(signature
(mixed rational
sugars
traffic-light)))

Das Beispiel zeigt, da die Flle einer Definition fr gemischte Daten


nicht allesamt Records sein mssen. Es ist allerdings wichtig, da die
Flle disjunkt sind, also jeder Wert eindeutig einem der Flle zugeordnet
werden kann: Sonst wre es nicht mglich, eine sinnvolle Verzweigung
zu schreiben, welche die Flle unterscheidet.
Nun zu unserer Prozedur zur Ermittlung der Ampelbezeichnung fr
den Zuckergehalt. Hier Kurzbeschreibung und Signatur:
; Ampelbezeichnung fr Zuckeranteil ermitteln
(: sugar-traffic-light (sugar-content -> traffic-light))

Wir brauchen ziemlich viele Testflle, um alle Flle von Zuckeranteilen


abzudecken sowie die Eckflle der Tabelle von oben.
(check-expect (sugar-traffic-light 2) "grn")
(check-expect (sugar-traffic-light 5) "gelb")
(check-expect (sugar-traffic-light 10) "gelb")
(check-expect (sugar-traffic-light 12.5) "gelb")
(check-expect (sugar-traffic-light 20) "rot")

(check-expect (sugar-traffic-light s1) "grn")


(check-expect (sugar-traffic-light s2) "gelb")
(check-expect (sugar-traffic-light s3) "gelb")
(check-expect (sugar-traffic-light s4) "gelb")
Gemischte Daten 41

(check-expect (sugar-traffic-light s5) "rot")


(check-expect (sugar-traffic-light s6) "rot")

(check-expect (sugar-traffic-light "grn") "grn")


(check-expect (sugar-traffic-light "gelb") "gelb")
(check-expect (sugar-traffic-light "rot") "rot")

Als nchstes ist, wie immer, das Gerst dran:


(define sugar-traffic-light
(lambda (f)
...))

Als nchstes wenden wir die Schablone fr Prozeduren an, die gemisch-
te Daten akzeptieren. Wir brauchen eine Verzweigung mit sovielen
Zweigen wie sugar-content Flle hat, also drei:
(define sugar-traffic-light
(lambda (f)
(cond
(... ...)
(... ...)
(... ...))))

Als nchstes brauchen wir Tests fr die drei Flle. Fr den zweiten
Fall ist das einfach, da es sich um sugars-Records handelt: da gibt es
das Prdikat sugars?. Beim ersten Fall handelt es sich aber um eine
rationale Zahl, beim dritten um eine Zeichenkette beides eingebaute
Datensorten. Fr diese gibt es die eingebauten Prdikate rational? und
string? Abbildung 4.1 zhlt noch mehr eingebaute Prdikate auf.

Folgende Prdikate sind eingebaut:


number? testet, ob ein Wert eine Zahl ist.
real? testet, ob ein Wert eine reelle Zahl ist.
rational? testet, ob ein Wert eine rationale Zahl ist.
natural? testet, ob ein Wert eine natrliche Zahl ist.
string? testet, ob ein Wert eine Zeichenkette ist.
boolean? testet, ob ein Wert ein boolescher Wert ist.

Abbildung 4.1. Eingebaute Prdikate

Mit dieser Information gewappnet knnen wir die Tests ergnzen:


(define sugar-traffic-light
(lambda (f)
(cond
((rational? f) ...)
((sugars? f) ...)
((string? f) ...))))

Im ersten Zweig handelt es sich nicht nur um eine rationale Zahl,


sondern auch um eine Fallunterscheidung mit drei Fllen entsprechend
der Tabelle vom Anfang:
42 Kapitel 4

(define sugar-traffic-light
(lambda (f)
(cond
((rational? f)
(cond
(... ...)
(... ...)
(... ...)))
((sugars? f) ...)
((string? f) ...))))

Als nchstes ergnzen wir Tests entsprechend der Tabelle:


(define sugar-traffic-light
(lambda (f)
(cond
((rational? f)
(cond
((< f 5) ...)
((and (>= f 5) (<= f 12.5)) ...)
((> f 12.5) ...)))
((sugars? f) ...)
((string? f) ...))))

Schlielich mssen wir noch die Antworten eintragen:


(define sugar-traffic-light
(lambda (f)
(cond
((rational? f)
(cond
((< f 5) "grn")
((and (>= f 5) (<= f 12.5)) "gelb")
((> f 12.5) "rot")))
((sugars? f) ...)
((string? f) ...))))

Hier ist jetzt ein cond in einem anderen cond eingeschachtelt. Da so


etwas schnell unbersichtlich wird, lohnt es sich, wie auf Seite 38
empfohlen, diesen Zweig in eine separate Hilfsprozedur auszulagern.
Hier sind Kurzbeschreibung und Signatur:
; Zuckeranteil in g in Ampel umwandeln
(: sugar-weight->traffic-light (rational -> traffic-light))

Die Testflle lassen sich aus den Testfllen fr sugar-traffic-light


durch einfaches Kopieren und Umbenennen gewinnen:
(check-expect (sugar-weight->traffic-light 2) "grn")
(check-expect (sugar-weight->traffic-light 5) "gelb")
(check-expect (sugar-weight->traffic-light 10) "gelb")
(check-expect (sugar-weight->traffic-light 12.5) "gelb")
(check-expect (sugar-weight->traffic-light 20) "rot")

Gerst:
Gemischte Daten 43

(define sugar-weight->traffic-light
(lambda (w)
...))

Den Rumpf haben wir ja schon geschrieben, wir mssen ihn nur noch
hierher bewegen und f in w umbenennen. Das DrRacket-System bie-
tet dazu nach einem Klick auf Syntaxprfung die Mglichkeit, mit
einem Rechtsklick auf den Parameter f ein Men aufzuklappen, das
unter anderem die Auswahl f umbenennen anbietet. DrRacket sorgt
dann dafr, dass alle zugehrigen Vorkommen von f in gleicher Weise
umbenannt werden.

(define sugar-weight->traffic-light
(lambda (w)
(cond
((< w 5) "grn")
((and (>= w 5) (<= w 12.5)) "gelb")
((> w 12.5) "rot"))))

Zurck zu sugar-traffic-light: Dort benutzen wir zunchst die neu


definierte Hilfsprozedur:

(define sugar-traffic-light
(lambda (f)
(cond
((rational? f)
(sugar-weight->traffic-light f))
((sugars? f) ...)
((string? f) ...))))

Beim nchsten Zweig geht es um den Fall sugars zusammengesetzte


Daten. Wir knnen also die entsprechende Schablone anwenden:

(define sugar-traffic-light
(lambda (f)
(cond
((rational? f)
(sugar-weight->traffic-light f))
((sugars? f) ... (sugars-fructose f) ...
... (sugars-glucose f) ...)
((string? f) ...))))

Wir mssen Fruktose- und den Glukose-Anteil addieren und die Summe
dann ebenfalls entsprechend der Tabelle vom Anfang in eine Ampel-
farbe umwandeln. Aber halt! Genau fr das Umwandeln der Zahl
aus der Tabelle in eine Ampelfarbe haben wir ja gerade die Hilfsproze-
dur sugar-weight->traffic-light geschrieben, und diese knnen wir
erneut zum Einsatz bringen:

(define sugar-traffic-light
(lambda (f)
(cond
((rational? f)
(sugar-weight->traffic-light f))
44 Kapitel 4

((sugars? f)
(sugar-weight->traffic-light (+ (sugars-fructose f)
(sugars-glucose f))))
((string? f) ...))))

Bleibt der letzte Fall der ist zum Glck trivial, da es sich schon um
eine Farbe handelt, Die mu sugar-traffic-light nur zurckgeben:
(define sugar-traffic-light
(lambda (f)
(cond
((rational? f)
(sugar-weight->traffic-light f))
((sugars? f)
(sugar-weight->traffic-light (+ (sugars-fructose f)
(sugars-glucose f))))
((string? f) f))))

Fertig!

Aufgaben

Aufgabe 4.1 Schreibe Daten- und Record-Definitionen fr geometrische


Figuren, wobei eine geometrische Figur ein Quadrat, ein Kreis oder ein
Rechteck sein kann. Schreibe eine Prozedur, die fr eine geometrische
Figur (ein Quadrat, einen Kreis oder ein Rechteck) den Flcheninhalt
berechnet.
TBD
5 Programmieren mit Listen

Die bisher betrachteten Daten hatten alle immer eine feste Gre
die Anzahl der Komponenten zusammengesetzter ist fest, ebenso wie
die Anzahl der Flle bei Fallunterscheidungen oder gemischten Daten.
Das reicht nicht fr alle Anwendungen: Die Bcher im Regal, die
Wagen eines Zuges, die Fotos im Album sind allesamt in ihrer Anzahl
variabel. Fr die Reprsentation solcher Informationen wird also eine
neue Art der Datendefinition bentigt, welche die schon bekannten
zusammengesetzten und gemischten Daten ergnzt: der Selbstbezug.
Selbstbezge knnen benutzt werden, um solche Daten variabler Gre
abzubilden, insbesondere in sogenannten Listen.

5.1 Listen reprsentieren

Hier sind einige Listen aus dem tglichen Leben:

Brot Herbert 1 Phidipides Dauerwelle Pumps


Butter Mike 2 Diabetes Dreadlocks
Kse 3 Bursitis Irokese
4 Woody Vokuhila
5 Hepatitis
6 Zeus
Doris
Lorenzo
Keine dieser Listen ist auf ihre jeweilige Lnge festgelegt: Zur Liste mit
Brot knnten beispielsweise noch Gurken hinzukommen. Damit
ist es nicht mglich, diese Listen jeweils durch die bereits bekannten
zusammengesetzten Daten und Records zu reprsentieren, da bei ihnen
die Liste der Komponenten schon in der Datendefinition festgelegt ist.
Hier kommen Listen im Sinne der Programmierung ins Spiel, die es
erlauben, zusammengesetzte Daten variabler Lnge zu reprsentieren.
Die einfachste Sorte Liste ist bereits eingebaut:
empty
, #<empty-list>
Dies ist die leere Liste. Um Listen herzustellen, die nicht leer sind, dienen
Paare, eine besonders allgemein verwendbare Sorte zusammengesetzter
Daten. Hier sind Daten- und Record-Definition fr Paare:
; Ein Paar besteht aus:
; - einem beliebigen Element
46 Kapitel 5

; - einer Liste
(define-record-procedures pair
make-pair pair?
(first rest))

Fr eine Signatur fr den Konstruktor ist es noch zu frh, da es noch


keine Signatur fr den Begriff Liste aus der Datendefinition gibt.
Diese folgt in wenigen Augenblicken.
Ein Paar kann nun benutzt werden, um die letzte Liste aus der obigen
Tabelle zu reprsentieren. Pumps wird als Zeichenkette zum ersten
Element des Paars. Die rest-Komponente des Paars mu eine Liste
sein dabei ist zu beachten, da insbesondere die leere Liste eine Liste
ist, dort also verwendet werden kann:

(make-pair "Pumps" empty)


, #<record:pair "Pumps" #<empty-list>>

Jetzt kommt der entscheidende Punkt: Paare sind auch Listen, Listen
sind also gemischte Daten:

; Eine Liste ist eins der folgenden:


; - die leere Liste
; - ein Paar
(define a-list
(signature
(mixed empty-list
pair)))

Die Signatur empty-list ist bereits eingebaut und pat auf die lee-
re Liste empty (und nichts sonst). (Der Name list ist in den DMdA-
Lehrsprachen bereits vergeben, darum heit die Signatur a-list.) Jetzt
ist es mglich, eine Signatur fr make-pair sowie die anderen Prozedu-
ren der Record-Definition von pair zu vergeben:

(: make-pair (%a a-list -> pair))


(: pair? (any -> boolean))
(: first (pair -> %a))
(: rest (pair -> a-list))

Hinter der Bezeichnung %a verbirgt sich eine Signaturvariable: Sie steht


fr eine beliebige Sorte, die aber in den zwei Zeilen, wo sie vorkommt,
die gleiche Sorte darstellen soll. Damit ist %a etwas anderes als any,
welches erlauben wrde, dass die Rckgabesorte von first verschieden
wre von der Sorte, welche der Konstruktor make-pair zuvor verwendet
hat.
So weit, so gut: Bisher gab die leere Liste und eine Liste mit einem
Element. Wie kommen mehr Elemente in eine Liste? Indem make-pair
noch einmal aufgerufen wird:

(make-pair "Mike" (make-pair "Herbert" (make-pair "Marcus" empty)))


,#<record:pair "Mike" #<record:pair "Herbert" #<record:pair "Marcus" #<empty-list>>>

Zu einer Liste mit zwei Elementen gehren also zwei Paare. Entspre-
chend fr drei Elemente:
Programmieren mit Listen 47

Abbildung 5.1. Liste mit Paaren reprsentiert

Abbildung 5.2. Liste mit Paaren reprsentiert, als Pfeildiagramm

(make-pair "Brot" (make-pair "Butter" (make-pair "Kse" empty)))


,#<record:pair "Brot" #<record:pair "Butter" #<record:pair "Kse" #<empty-list>>>>

Diesmal sind drei Paare im Spiel. Die Liste wird jeweils terminiert
durch die leere Liste. Abbildung 5.1 zeigt, wie die Listen aussehen, wenn
sie wie andere zusammengesetzte Daten als Tabellen dargestellt sind
das steht fr die leere Liste. Das sieht aus wie ineinandergeschachtelte
russische Puppen und entspricht damit nicht der gngigen Intuition,
wie Listen aufgebaut sind, aber es funktioniert. Fr viel mehr als drei
Elemente funktioniert die Darstellungsweise allerdings nicht: Darum
bevorzugen wir ab hier sogenannte Zeigerdiagramme, bei denen alle
Paare gleich gro dargestellt sind und ein Pfeil zeigt, da ein Paar
die rest-Komponente eines anderen Paares bildet. Abbildung 5.2 zeigt
das Pfeildiagramm, das Abbildung 5.1 entspricht; es pat besser zur
gngigen Intuition von Listen.

5.2 Mit Listen programmieren

Als erstes Beispiel fr das Programmieren mit Listen schreiben wir eine
Prozedur, die eine Liste von Zahlen akzeptiert und deren Summe liefert.
Dazu werden erst einmal einige Beispiele solcher Listen bentigt:
; Liste mit den Zahlen 1 2 3
(define n1 (make-pair 1 (make-pair 2 (make-pair 3 empty))))
; Liste mit den Zahlen e und pi
(define n2 (make-pair 2.7183 (make-pair 3.14159 empty)))
; Liste mit den Zahlen 2 3 5 7
(define n3 (make-pair 2 (make-pair 3 (make-pair 5 (make-pair 7 empty)))))

Hier Kurzbeschreibung und Vertrag:


; Summe der Elemente einer Liste von Zahlen berechnen
(: list-sum (a-list -> number))

Fr die Testflle halten die Beispiellisten her:


48 Kapitel 5

(check-expect (list-sum n1) 6)


(check-within (list-sum n2) 5.85989 0.001)
(check-expect (list-sum n3) 17)

Nicht vergessen: In n2 sind Zahlen mit Dezimalpunkt. Deshalb mssen


wir check-within verwenden. Diese Prozedur hat drei Parameter: Die
ersten beiden geben ein reales und ein erwartetes Ergebnis an und der
dritte sagt, um wieviel das reale Ergebnis in der positiven oder negati-
ven Richtung vom erwarteten Ergebnis abweichen darf. Das Gerst der
Prozedur sieht so aus:
(define list-sum
(lambda (lis)
...))

(Es empfiehlt sich, als Bezeichner fr Listen nicht einfach nur ein l zu
verwenden, da es in vielen Schriftarten der Ziffer 1 hnlich sieht.)
Zur Erinnerung: Listen sind zunchst gemischte Daten die Definiti-
on von a-list benutzt mixed mit zwei Fllen. Entsprechend kommt die
Konstruktionsanleitung fr gemischte Daten zur Anwendung:
(define list-sum
(lambda (lis)
(cond
(... ...)
(... ...))))

Als nchstes sollten wir die Tests ergnzen, die auf die leere Liste
bzw. Paare testen. Fr empty hat dieses Buch Ihnen den Test bisher
vorenthalten: Das eingebaute Prdikat empty? liefert #t fr die leere
Liste und #f fr jeden anderen Wert. Fr Paare ist das Prdikat pair?
bereits von der Record-Definition definiert:
(define list-sum
(lambda (lis)
(cond
((empty? lis) ...)
((pair? lis) ...))))

Beim zweiten Zweig des cond handelt es sich um zusammengesetzte


Daten. Ergo ergnzen wir entsprechend der Konstruktionsanleitung fr
zusammengesetzte Daten Selektor-Aufrufe in die Schablone:

(define list-sum
(lambda (lis)
(cond
((empty? lis) ...)
((pair? lis)
... (first lis) ... (rest lis) ...))))

Jetzt knnen wir ans Ausfllen der beiden Zweige gehen: Die erste
Ellipse mu die Frage beantworten, was die Summe der leeren Liste
sein soll. Diese Frage ist bei nahezu allen Prozeduren relevant, die Listen
akzeptieren, es empfiehlt sich darum grundstzlich, einen Testfall fr
die leere Liste zu formulieren:
Programmieren mit Listen 49

(check-expect (list-sum empty) ?)


Die Antwort ist nicht ganz offensichtlich, wir knnen sie aber durch
folgende berlegung gewinnen: Betrachten wir Listen aus Einsen, dann
entspricht deren Summe immer der Lnge der Liste. Wenn die Liste
leer ist hat sie die Lnge 0, entsprechend ist also auch ihre Summe 0:
(check-expect (list-sum empty) 0)
...
(define list-sum
(lambda (lis)
(cond
((empty? lis) 0)
((pair? lis)
... (first lis) ... (rest lis) ...))))

Bleibt der zweite Zweig mit den Paaren. Hier hilft es, die beiden
Selektor-Aufrufe (first lis) und (rest lis) auf die Daten zurckzu-
beziehen. (first lis) liefert fr die drei Beispiellisten folgende Werte:
(first n1)
, 1
(first n2)
, 2.7183
(first n3)
, 2

Das ist jeweils das erste Element kein Wunder, da der Selektor first
heit. Nun fr rest:
(rest n1)
, #<record:pair 2 #<record:pair 3 #<empty-list>>>
(rest n2)
, #<record:pair 3.14159 #<empty-list>>
(rest n3)
, #<record:pair 3 #<record:pair 5 #<record:pair 7 #<empty-list>>>>
Dies sind jeweils Listen mit allen Elementen auer dem ersten, also quasi
den Rest daher der Name rest fr den Selektor.
Zurck zur Schablone: Was knnen wir mit dem ersten Element der
Liste und dem Rest der Liste anfangen? Hier kommt ein besonderer
Trick zum Zug da der Rest der Liste wieder eine Liste ist, knnen wir
von der Summe des Rests sprechen. Wenn diese Summe bekannt ist
also die Summe aller Elemente auer des ersten, dann knnten wir (und
das Programm auch) die Gesamtsumme ermitteln, indem wir auf diese
Summe das noch fehlende erste Element addieren. Die Summe aller
Elemente auer des ersten knnen wir so aufschreiben:
(list-sum (rest lis))

Entsprechend die Summe des ersten Elements und der Summe des
Rests:
(+ (first lis) (list-sum (rest lis)))

Das knnen wir in die Schablone von oben einsetzen:


50 Kapitel 5

(define list-sum
(lambda (lis)
(cond
((empty? lis) 0)
((pair? lis)
(+ (first lis) (list-sum (rest lis)))))))

Dieses Programm ist nicht nur vollstndig, es funktioniert auch tat-


schlich: Alles was wir gemacht haben war, die Kurzbeschreibung von
list-sum ernst zu nehmen, die behauptet, da die Prozedur die Summe
einer beliebigen Liste berechnet: (rest lis) ist eine Liste, also kann
list-sum auch deren Summe berechnen.
Um den Selbstaufruf von list-sum noch besser zu verstehen, be-
trachten wir noch einmal die Datendefinition fr Listen zusammen mit
der Datendefinition fr Paare:
Die Datendefinition enthlt einen Verweis auf sich selbst, genannt
Selbstbezug. Dieser Selbstbezug ist beim Rest der Selbstaufruf, der
in list-sum auf den Rest erfolgt, folgt also der Datendefinition. Ein
Selbstaufruf heit auch rekursiver Aufruf und ist damit fester Bestandteil
der Schablone fr Prozeduren, die Listen akzeptieren:
(: proc (a-list -> ...))

(define proc
(lambda (lis)
(cond
((empty? lis) ...)
((pair? lis)
... (first lis)
... (proc (rest lis)) ...))))

Jetzt wo wir die Schablone haben, gehen wir noch ein weiteres Beispiel
durch, bei dem wir sie von vornherein anwenden: Gefragt ist eine
Prozedur, die eine Liste von Zahlen akzeptiert und feststellt, ob alle
Listenelemente positiv sind. Die Prozedur beantwortet eine Ja/Nein-
Frage, die damit auch die Kurzbeschreibung bildet:
; sind alle Zahlen aus einer Liste positiv?

Hier ist der dazu passende Vertrag:


(: all-positive? (a-list -> boolean))

Fr Testflle stehen die leere Liste sowie die drei Beispiellisten n1, n2
und n3 aus dem vorherigen Beispiel zur Verfgung:
(check-expect (all-positive? empty) #t)
(check-expect (all-positive? n1) #t)
(check-expect (all-positive? n2) #t)
(check-expect (all-positive? n3) #t)

Der empty-Testfall ist vielleicht etwas verwirrend: In der Tat sind alle
Elemente der leeren Liste positiv. Eine andere Art dies zu sagen wre,
da kein Element der leeren Liste nicht positiv ist.
Diese Testflle reichen sichtlich nicht aus, da alle #t ergeben sollen:
Diese wrden auch von folgender Prozedur erfllt:
Programmieren mit Listen 51

(define all-positive?
(lambda (lis)
#t))

Also werden noch Beispiele mit nicht-positiven Elementen bentigt


insbesondere eins mit dem Grenzfall 0:

(check-expect (all-positive? (make-pair -5 empty)) #f)


(check-expect (all-positive? (make-pair 0 empty)) #f)
(check-expect (all-positive? (make-pair 1 (make-pair -2 empty))) #f)

Das Gerst ist wie folgt:

(define all-positive?
(lambda (lis)
...))

Wir knnen nun die Schablone fr Listen direkt benutzen:

(define all-positive?
(lambda (lis)
(cond
((empty? lis) ...)
((pair? lis)
... (first lis)
... (all-positive? (rest lis)) ...))))

Das Ergebnis im ersten Zweig wird durch den ersten Testfall diktiert: #t.
Wie schon zuvor machen wir uns die Bedeutung der Ausdrcke im pair-
Zweig klar: (first lis) ist das erste Element der Liste. (all-positive?
(rest lis)) besagt, ob alle Elemente des Rests von lis positiv sind. Es
sind nur dann alle Elemente von lis positiv, wenn (first lis) positiv
ist und (all-positive? (rest lis)) als Ergebnis #t liefert. Damit ist
klar, wie die beiden Ausdrcke kombiniert werden mssen:

(define all-positive?
(lambda (lis)
(cond
((empty? lis) #t)
((pair? lis)
(and (> (first lis) 0)
(all-positive? (rest lis)))))))

Konstruktionsanleitung ?? auf Seite ?? fat die Schablone fr Prozedu-


ren, die Listen akzeptieren, noch einmal zusammen.

5.3 Signaturkonstruktoren

Die Beispiellisten vom Anfang dieses Kapitels sind allesamt homogen:


Alle Elemente der Liste sind jeweils von derselben Sorte. Es gibt eine
Liste von Essenszutaten, eine von Eigennamen, eine von Zahlen, eine
von dramatischen Figuren, eine von Frisuren und eine von Schuhen. Die
Datendefinition fr a-list von Seite 5.1 ist da allerdings nicht festgelegt:
es heit ausdrcklich, da jedes Paar ein beliebiges Element enthlt. So
ist beispielsweise auch folgende Liste zulssig:
52 Kapitel 5

(: ml1 a-list)
(define ml1 (make-pair 5 (make-pair "Herbert" empty)))

In den meisten Fllen gehren jedoch alle Elemente einer Liste zu der-
selben Sorte beziehungsweise haben dieselbe Signatur: Schn wre es,
wenn die Signatur fr die Liste dokumentieren knne, welche Signa-
tur die Elemente haben. Wir knnten bei der Definition von pair die
Signatur der Elemente angeben, also pair zum Beispiel auf Elemente
der Signatur number fest abonnieren:
(define-record-procedures pair
make-pair pair?
(first rest))
(: make-pair (number list-of-numbers -> pair))
(: pair? (any -> boolean))
(: first (pair -> number))
(: rest (pair -> list-of-numbers))

Entsprechend wrden wir die daraus resultierende Signatur fr Listen


nicht mehr a-list sondern list-of-numbers nennen:
(define list-of-numbers
(signature
(mixed empty-list
pair)))

Das funktioniert zwar, hat aber den Nachteil, da wir dann fr jede
neue Elementsorte eine neue Definition von Listen schreiben mssen:
list-of-strings, list-of-shoes etc. Das wre nicht nur mhsam, son-
dern wrde auch viel Code ohne Sinn und Verstand mehrmals kopieren.
Besser wre es natrlich, die Macht der Abstraktion zum Tragen zu brin-
gen und ber die Elementsignatur zu abstrahieren. Dazu brauchen wir
allerdings eine etwas aufgebohrte Form von define-record-procedures,
genannt define-record-procedures-parametric. Abbildung 5.3 beschreibt
die Funktionsweise.

Die define-record-procedures-parametric-Form hat folgende allgemei-


ne Gestalt:

(define-record-procedures-parametric t sc
c p
(s1 . . . sn ))

Dabei sind t, c, p, s1 , . . . , sn genau wie bei define-record-procedures


(siehe Seite 29). Zustzlich wird unter dem Namen sc ein Signaturkon-
struktor definiert. Dieser ist eine Prozedur mit genau so vielen Argumen-
ten wie es Felder in dem Record gibt. Er konstruiert aus den Signaturen
der Felder die Signatur von Records des Typs t.
Anmerkung: Die Signaturen der Felder werden jeweils erst bei Aufrufen
der Selektoren berprft.

Abbildung 5.3. define-record-procedures-parametric

Mit define-record-procedures-parametric knnen wir die Record-


Definition von pair erweitern:
Programmieren mit Listen 53

(define-record-procedures-parametric pair pair-of


make-pair pair?
(first rest))

Pair-of ist der Signaturkonstruktor. (Das angehngte -of ist, hnlich


wie das make- bei den Konstruktoren, das ? beim Prdikat und den
Feld-Namen bei den Selektoren reine Konvention.)
Konstruktor, Prdikat und Selektoren fr das neue pair haben die
folgenden Signaturen:
(: make-pair (%a %b -> (pair-of %a %b)))
(: pair? (any -> boolean))
(: first ((pair-of %a %b) -> %a))
(: rest ((pair-of %a %b) -> %b))

Hier sind %a und %b wiederum Signaturvariablen, die fr beliebige Si-


gnaturen stehen. Die Tatsache, dass hier zwei verschiedene Signaturva-
riablen verwendet wurden bringt die zustzliche Information fr den
menschlichen Leser zum Ausdruck, da die Argumente von pair-of zu
potentiell unterschiedlichen Signaturen gehren.
Zu lesen sind die obigen Signaturdeklarationen so:
Make-pair akzeptiert zwei Argumente beliebiger Signaturen %a und
%b und liefert als Resultat einen Record, dessen erstes Feld von der
Signatur %a und dessen zweites Feld von der Signatur %b ist.
Das Resultat von first hat die Signatur des ersten Feldes seines
Arguments.
Das Resultat von rest hat die Signatur des zweiten Feldes seines
Arguments.
Diese Signaturen sind allerdings noch unbefriedigend, da sie nicht
zum Ausdruck bringen, da das zweite Argument von make-pair immer
eine Liste sein mu beziehungsweise da rest immer eine Liste liefert.
Diese Problematik stellen wir noch einen Moment hintenan, werden
aber spter zu ihr zurckkehren zunchst einmal zu den Signaturen
fr Listen.
Mit Hilfe von pair-of knnen wir versuchen, einen Ersatz fr a-list
zu definieren, der spezifiziert, auf welche Signatur die Elemente der
Liste passen. Das knnte so aussehen:
(define list-of-numbers
(signature
(mixed empty-list
(pair-of number list-of-numbers))))

Entsprechend fr Zeichenketten:
(define list-of-strings
(signature
(mixed empty-list
(pair-of string list-of-strings))))

Wie schon oben angemerkt, wre es unbefriedigend, fr jede Ele-


mentsignatur eine eigene Listensignatur definieren zu mssen, im-
mer auf die gleiche Weise. Aber glcklicherweise unterscheiden sich
list-of-numbers und list-of-strings nur an einer einzigen Stelle, und
ber die knnen wir abstrahieren:
54 Kapitel 5

(define list-of
(lambda (x)
(signature
(mixed empty-list
(pair-of x (list-of x))))))

Fertig! Nun knnen wir Signaturen fr Listen von Zahlen als (list-of
number), Listen von Zeichenketten als (list-of string) und Listen von
beliebigen Werten als (list-of any) schreiben. Auerdem knnen wir
diese Definition jetzt verwenden, um bessere Signaturen fr make-pair,
first und rest anzugeben:

(: make-pair (%a (list-of %a) -> (pair-of %a (list-of %a))))


(: first ((pair-of %a (list-of %a)) -> %a))
(: rest ((pair-of %a (list-of %a)) -> (list-of %a)))

5.4 Eingebaute Listen

Da Listen in diesem Buch noch oft vorkommen und es umstndlich


ist, jedesmal die Definitionen fr pair und list-of an den Anfang von
Programmen zu setzen, ist es an dieser Stelle Zeit, eine neue Spra-
chebene in DrRacket auszuwhlen, nmlich Die Macht der Abstraktion
(ohne Anfnger). Diese enthlt make-pair, pair?, first und rest als ein-
gebaute Prozeduren sowie den Signaturkonstruktor list-of1 und eine
eingebaute Prozedur list zur Erzeugung beliebiger Listen.
Auerdem werden nichtleere Listen ab dieser Sprachebene in der
REPL anders ausgedruckt, nmlich, der besseren bersicht halber, als
#<list ...>, wobei die Listenelemente zwischen den spitzen Klammern
aufgereiht sind. Beispiele:

(list 1)
, #<list 1>
(list 1 2)
, #<list 1 2>
(list 1 2 3)
, #<list 1 2 3>

5.5 Parametrische Polymorphie

In diesem Abschnitt programmieren wir eine Prozedur, welche die


Lnge einer Liste ermittelt. Das ist eine einfache Fingerbung mit einer
interessanten Eigenschaft. Hier sind Kurzbeschreibung und Signatur:

; Lnge einer Liste berechnen


(: list-length ((list-of %a) -> natural))

Dieser Prozedur ist egal, welche Signatur die Elemente der Liste erfllen,
weil das Konzept der Lnge einer Liste unabhngig davon ist, was die
Elemente sind. Dementsprechend steht dort nur die Signaturvariable
1 In Standard-Scheme heit der Konstruktor fr die eingebauten Paare cons, die Selektoren
car und cdr (gesprochen kar und kudder; dies waren die Namen von Anweisungen
auf einer Maschine, auf der ein Vorlufer von Scheme lief) und das Prdikat fr die leere
Liste null?.
Programmieren mit Listen 55

Die eingebaute Prozedur list erlaubt es, Listen aus ihren Elementen
ohne Verwendung von make-pair zu erzeugen. Sie akzeptiert eine belie-
bige Anzahl von Argumenten, macht daraus eine Liste und gibt diese
zurck:
(list 1 2 3)
, #<list 1 2 3>
(list "Axl" "Slash" "Izzy")
, #<list "Axl" "Slash" "Izzy">

Abbildung 5.4. list

%a.Solche Prozeduren, die Argumente akzeptieren, deren Signaturen


Signaturvariablen enthalten, heien polymorph oder auch parametrisch
polymorph (weil die Signaturvariable eine Art Parameter abgibt), und
das dazugehrige Konzept heit parametrische Polymorphie: ein groes
Wort, das hier fr eine kleine Sache steht. Interessantere Beispiele fr
parametrische Polymorphie wird es in Kapitel 8 geben.
Weiter mit list-length hier ist das Gerst:
(define list-length
(lambda (lis)
...))

Die Schablone ist wie gehabt:


(define list-length
(lambda (lis)
(cond
((empty? lis) ...)
((pair? lis)
... (first lis) ...
... (list-length (rest lis)) ...))))

Es ist list-length egal, was der Wert von (first lis) ist. Die Lnge
der Liste ist unabhngig davon, was fr Werte sich darin befinden:
entscheidend ist nur, wieviele es sind. (Dieser Umstand ist gerade ver-
antwortlich fr die parametrische Polymorphie.) Damit knnen wir
(first lis) aus der Schablone streichen und diese dann zum vollstn-
digen Rumpf ergnzen:
(define list-length
(lambda (lis)
(cond
((empty? lis) 0)
((pair? lis)
(+ 1
(list-length (rest lis)))))))

5.6 Prozeduren, die Listen produzieren

In den vorherigen Abschnitten haben wir ausschlielich Prozeduren


programmiert, die Listen akzeptieren. In diesem Abschnitt schreiben wir
56 Kapitel 5

Prozeduren, die Listen produzieren. Das geht mit Techniken, die wir
bereits vorgestellt haben. Wir machen die Sache interessanter, indem
wir in einem ersten Beispiel Listen von zusammengesetzten Daten
betrachten und in einem zweiten Beispiel zwei Listen verarbeiten.

5.6.1 Grteltiere berfahren

Auf Seite 32 haben wir die Prozedur run-over-dillo geschrieben, die


fr das berfahren von Grteltieren zustndig ist. In diesem Abschnitt
schreiben die Prozedur, die das gleich massenweise erledigt, beispiels-
weise fr alle Grteltiere auf einem Highway. Dazu bernehmen wir
Daten- und Record-Definition von Grteltieren aus Abschnitt 3.4 sowie
die Prozedurdefinition von run-over-dillo. Grteltiere knnen wir in
Listen stecken, ebenso wie Zahlen, Zeichenketten oder boolesche Werte.
Hier ist ein Beispiel:

; Grteltiere auf Highway 75


(define dl75 (list d1 d2 d3 d4))

(D1, d2, d3 und d4 sind die Beispielgrteltiere aus Abschnitt 3.4.)


Diese Liste hat die Signatur (list-of dillo). Wenn wir eine Prozedur
schreiben wollen, die alle Grteltiere aus einer Liste berfhrt, mte
diese also folgende Kurzbeschreibung und Signatur haben:

; Grteltiere berfahren
(: run-over-dillos ((list-of dillo) -> (list-of dillo)))

Als Testfall kann obige Beispielliste herhalten:

(check-expect (run-over-dillos dl75)


(list (make-dillo 55000 #f)
d2
(make-dillo 60000 #f)
d4))

Zur Erinnerung: d2 und d4 sind bereits tot, dementsprechend sind sie


berfahren wie zuvor.
Hier ist das Gerst:

(define run-over-dillos
(lambda (dls)
...))

Die Prozedur akzeptiert eine Liste als Eingabe, wir knnen also, wie
schon so oft, die entsprechende Schablone zum Einsatz bringen:

(define run-over-dillos
(lambda (dls)
(cond
((empty? dls) ...)
((pair? dls)
... (first dls) ...
... (run-over-dillos (rest dls)) ...))))
Programmieren mit Listen 57

Im ersten Zweig ist die Sache klar: Geht eine leere Liste rein, kommt
auch eine leere Liste raus. Im zweiten Zweig knnen wir uns erst einmal
um das erste Grteltier kmmern. Wir haben ja bereits eine Prozedur,
die ein einzelnes Grteltier berfhrt; diese knnen wir auf das erste
Element der Liste anwenden:
(define run-over-dillos
(lambda (dls)
(cond
((empty? dls) empty)
((pair? dls)
... (run-over-dillo (first dls)) ...
... (run-over-dillos (rest dls)) ...))))

Lesen wir noch einmal die beiden Ausdrcke, die im zweiten Zweig
stehen:
(run-over-dillo (first dls)) ist das erste Grteltier der Liste, ber-
fahren.
(run-over-dillos (rest dls)) ist eine Liste der restlichen Grteltiere,
berfahren.
Gefragt ist eine Liste aller Grteltiere, berfahren: Wir mssen also nur
die Resultate der beiden Ausdrucke mit make-pair kombinieren:
(define run-over-dillos
(lambda (dls)
(cond
((empty? dls) empty)
((pair? dls)
(make-pair (run-over-dillo (first dls))
(run-over-dillos (rest dls)))))))

Fertig!
Dieses Beispiel zeigt, da wir fr Prozeduren, die Listen produzieren,
keine neue Technik brauchen: Wenn eine Prozedure eine leere Liste
produzieren soll, benutzen wir an der entsprechenden Stelle empty,
und bei nichtleeren Listen benutzen wir make-pair, bringen also die
Schablone fr Prozeduren zum Einsatz, die zusammengesetzte Daten
produzieren.

5.6.2 Zwei Listen aneinanderhngen

In unserem nchsten Beispiel ist eine Prozedur concatenate gefragt, die


zwei Listen aneinanderhngt:
(concatenate (list 1 2 3) (list 4 5 6))
, #<list 1 2 3 4 5 6>
Kurzbeschreibung, Signatur und Gerst sehen folgendermaen aus:
; zwei Listen aneinanderhngen
(: concatenate ((list-of %a) (list-of %a) -> (list-of %a)))
(define concatenate
(lambda (lis-1 lis-2)
...))
58 Kapitel 5

Die Konstruktionsanleitung aus Abschnitt 5.1 ist eigentlich nur fr


Prozeduren gedacht, die eine einzelne Liste akzeptieren. Welche von
beiden ist das l aus der Anleitung? Im Zweifelsfall knnen wir beide
Alternativen auszuprobieren. Wir fangen, um die Sache spannender zu
machen, mit lis-2 an:

(define concatenate
(lambda (lis-1 lis-2)
(cond
((empty? lis-2) ...)
((pair? lis-2)
... (first lis-2)
... (concatenate lis-1 (rest lis-2)) ...))))

Der erste Zweig des cond ist noch einfach: Wenn lis-2 leer ist, mu
lis-1 herauskommen. Jedoch wre fr das obige Beispiel der Wert von
(concatenate lis-1 (rest lis-2)) die folgende Liste:

#<list 1 2 3 5 6>

Bei dieser Liste fehlt das Element 4 in der Mitte, und es ist nicht
ersichtlich, wie unsere Prozedur sie passend ergnzen knnte. Diese
Mglichkeit fhrt also in eine Sackgasse. Wir versuchen deshalb, die
Schablone auf lis-1 statt auf lis-2 anzuwenden:

(define concatenate
(lambda (lis-1 lis-2)
(cond
((empty? lis-1) ...)
((pair? lis-1)
... (first lis-1)
... (concatenate (rest lis-1) lis-2) ...))))

Die erste Ellipse ist einfach zu ersetzen: Ist die erste Liste leer, ist das
Ergebnis die zweite Liste lis-2. Fr den zweiten Fall sollten wir uns
noch einmal ins Gedchtnis rufen, was fr einen Wert (concatenate
(rest lis-1) lis-2) liefert: das Ergebnis dieses Aufrufs ist eine Li-
ste, die aus (rest lis-1) und lis-2 zusammengesetzt wurde. Auf das
obige Beispiel bertragen, mit lis-1 = #<list 1 2 3> und lis-2 =
#<list 4 5 6>, ist (rest lis-1) = #<list 2 3>. Der Wert von (concatenate
(rest lis-1) lis-2) wre also:

#<list 2 3 4 5 6>

Es fehlt das erste Element von lis-1, (first lis-1), das vorn an das
Ergebnis angehngt werden mu. Das geht mit make-pair:

(define concatenate
(lambda (lis-1 lis-2)
(cond
((empty? lis-1) lis-2)
((pair? lis-1)
(make-pair (first lis-1)
(concatenate (rest lis-1) lis-2))))))
Programmieren mit Listen 59

Dieses Beispiel zeigt ein weiteres Schablonenelement, das noch fter


vorkommen wird: Wie bei anderen zusammengesetzten Daten mssen
Prozeduren, die Listen konstruieren sollen, irgendwo ein make-pair
enthalten.
List-length und concatenate sind gute Programmierbungen. Da
viele Programme diese Operationen bentigen, sind sie in Scheme
bereits unter den Namen length und append eingebaut.

Anmerkungen

Listen sind, was Datenstrukturen betrifft, eine Art Alleskleber: Sie tau-
gen auch fr die Reprsentation von Tabellen, Mengen und vielen
anderen zusammengesetzten Daten. Fr Listen gibt es eine riesige
Anzahl praktischer Prozeduren, von denen die Prozeduren in diesem
Kapitel nur die Spitze des Eisberges sind. Da Listen in Scheme fest
eingebaut sind, knnen sie als universelles Kommunikationsmittel zwi-
schen Programmen dienen, weil sich Prozeduren auf Listen aus einem
Programm auch in einem anderen verwenden lassen. Dies unterscheidet
Scheme von vielen anderen Programmiersprachen, in denen Listen vom
Programmierer selbst definiert werden mssen oder nur eine unterge-
ordnete Rolle spielen.
Viele andere Programmiersprachen bauen auf Felder oder Arrays
als fundamentale Datenstrukturen fr die Reprsentation von Folgen.
Diese gibt es in Scheme auch (unter dem Namen Vektor), finden jedoch
nur selten Verwendung: Oft lt sich eine bessere Lsung mit Listen
oder anderen Reprsentationen finden.

Aufgaben

FIXME: viel mehr Aufgaben

Aufgabe 5.1 Schreiben Sie Ausdrcke fr Listen, welche die Beispielli-


sten vom Anfang von Abschnitt 5.1 reprsentieren.

Aufgabe 5.2 Schreibe eine Prozedur backlog-list, welche die gleichen


Eingaben wie load-list akzeptiert, aber die nach dem Beladen eines
Lastwagens im Lager zurckbleibenden Artikel zurckgibt. Die Proze-
dur soll load-list als Hilfsprozedur verwenden.
6 Induktive Beweise und Definitionen

Die Mathematik beschftigt sich zentral mit Beweisen von formalen Aus-
sagen. Diese sind auch in der Informatik wichtig, um die Korrektheit
von Programmen sicherzustellen. Dabei geht es hufig um Aussagen
der Form fr alle x X, wobei X eine unendliche Menge ist zum
Beispiel die Menge der natrlichen Zahlen oder die Menge der Listen.
In der Informatik haben viele der in der Praxis vorkommenden Men-
gen insbesondere alle gemischten Daten mit Selbstbezug wie die
Listen eine besondere Eigenschaft: Sie sind induktiv definiert. Die in-
duktive Definition erlaubt, eine besondere Beweistechnik anzuwenden,
die Induktion. Um induktiv definierte Mengen, Funktionen darauf und
Beweise darber geht es in diesem Kapitel.

6.1 Aussagen ber natrliche Zahlen

In der Mathematik gibt es viele kuriose Aussagen ber natrliche


Zahlen. Legendenstatus hat z.B. die Gausche Summenformel:
n
n
n N : i= 2
( n + 1)
i =0

Wie lassen sich solche Aussagen beweisen? Gau Argument war, da,
wenn er die Summe ausschreibt:

1 + 2 + . . . + ( n 1) + n

. . . die beiden ueren Summanden 1 und n zusammen n + 1 ergeben,


die beiden nchstinneren Summanden 2 und n 1 ebenfalls n + 1
undsoweiter bis zur Mitte effektiv also halb so oft n + 1 auf sich
selbst addiert wird wie die Reihe selbst lang ist. Es ist also einfach
nachzuvollziehen, wie Gau auf die Formel kam und warum sie korrekt
ist.
Was ist aber mit folgender Reihe?
n
i2
i =0

Der Blick auf die ausgeschriebene Form hilft nicht direkt weiter:

1 + 4 + 9 + 16 + . . . + (n 1)2 + n2

Allerdings lohnt es sich, einen Blick auf die ersten paar Glieder der
Reihe zu werfen, und diese tabellarisch ber die Gausche Summen zu
62 Kapitel 6

setzen:
n 0 1 2 3 4 5 6 ...
in=0 i2 0 1 5 14 30 55 91 ...
in=0 i 0 1 3 6 10 15 21 ...
Wer die Paare der Summen der beiden Reihen lang genug anstarrt, sieht
vielleicht, da sich alle als Brche auf den Nenner 3 krzen lassen:

n 0 1 2 3 4 5 6 ...
in=0 i2 ? 3 5 7 9 11 13
...
in=0 i 3 3 3 3 3 3 3

Einzige Ausnahme ist der Bruch fr 0: dort wird durch 0 geteilt, es ist
also unklar, welcher Bruch an dieser Stelle in der Tabelle stehen sollte.
Ansonsten suggeriert die Tabelle folgende Formel:

in=0 i2 2n + 1
n =
i =0 i 3

Die Gleichung kann mit in=0 i = n(n + 1)/2 multipliziert werden, um


eine Antwort fr die ursprngliche Frage zu ergeben:
n
n(n + 1)(2n + 1)
i2 = 6
(6.1)
i =0

Schne Formel aber stimmt sie auch fr alle n N? (Fr den unklaren
Fall 0 stimmt sie.) Wer mag, kann sie noch fr weitere n 7, 8, . . . aus-
probieren, und tatschlich zeigen sich zunchst keine Gegenbeispiele.
Aber das ist langweilig und wrde immer noch nicht reichen, um die
Behauptung fr alle n N zu beweisen.
Wenn die Behauptung fr alle n N stimmt, also insbesondere auch
fr ein bestimmtes n N, dann sollte sie auch fr n + 1 gelten, das
wre dann die folgende Gleichung, bei der gegenber der Gleichung
oben fr n jeweils n + 1 eingesetzt wurde:
n +1
(n + 1)((n + 1) + 1)(2(n + 1) + 1)
i2 = 6
i =0

Das lt sich etwas vereinfachen:


n +1
(n + 1)(n + 2)(2n + 3)
i2 = 6
(6.2)
i =0

Bei der Reihe in=+01 i2 lt sich der letze Summand ausgliedern und die
Gleichung damit folgendermaen schreiben:
n
(n + 1)(n + 2)(2n + 3)
( i 2 ) + ( n + 1)2 =
i =0
6

Damit bietet sich die Chance, die jeweiligen Seiten von Gleichung 6.1
von den Seiten von Gleichung 6.2 abzuziehen:
n n
(n + 1)(n + 2)(2n + 3) n(n + 1)(2n + 1)
( i 2 ) + ( n + 1)2 i 2 =
i =0 i =0
6 6
Induktive Beweise und Definitionen 63

Es bleibt:
(n + 1)(n + 2)(2n + 3) n(n + 1)(2n + 1)
( n + 1)2 =
6 6
Wenn diese Gleichung stimmt, dann stimmt auch Gleichung 6.2. Das
lt sich ausrechnen:
(n + 1)(n + 2)(2n + 3) n(n + 1)(2n + 1) (n + 1)(n + 2)(2n + 3) n(n + 1)(2n + 1)
=
6 6 6
(n + 1)((n + 2)(2n + 3) n(2n + 1))
=
6
(n + 1)(2n + 3n + 4n + 6 2n2 n)
2
=
6
(n + 1)(6n + 6)
=
6
6( n + 1)2
=
6
= ( n + 1)2
Tatsache! Aber was wurde jetzt eigentlich gezeigt? Es ist leicht, bei den
vielen Schritten von oben den Faden zu verlieren. Hier noch einmal die
Zusammenfassung:
Es schien so, als ob folgende Gleichung stimmen wrde:
n
n(n + 1)(2n + 1)
i2 = 6
i =0

Es ist so, da folgende Gleichung stimmt:

(n + 1)(n + 2)(2n + 3) n(n + 1)(2n + 1)


( n + 1)2 =
6 6
Wenn also die Gleichung stimmte, die so scheint, dann folgte daraus
diese Gleichung:
n +1
(n + 1)(n + 2)(2n + 3)
i2 = 6
i =0

Das ist aber die gleiche Behauptung, nur fr n + 1 statt n mit anderen
Worten folgt aus der Behauptung fr n die Behauptung fr n + 1. Da
oben durch Ausrechnen bereits gezeigt wurde, da die Behauptung
fr 1, . . . , 6 gilt, gilt sie auch fr 7. Da sie fr 7 gilt, gilt sie auch fr
8. Undsoweiter fr alle natrlichen Zahlen. Es nicht ntig, sie einzeln
auszuprobieren. Die Vermutung von oben ist also bewiesen.

6.2 Induktive Beweise fhren

Das im vorigen Abschnitt verwendete Beweisprinzip fr Beweise von


Stzen der Form fr alle n N . . . heit vollstndige Induktion.1 Diese
funktioniert bei Aufgaben, bei denen eine Behauptung fr alle n N
zu beweisen ist. Fr den Beweis werden folgende Zutaten bentigt:
1 Nicht zu verwechseln mit der philosophischen Induktion. Die vollstndige Induktion ist
zwar verwandt, philosophisch gesehen aber eher eine deduktive Technik.
64 Kapitel 6

1. ein Beweis dafr, da die Behauptung fr n = 0 stimmt und


2. ein Beweis dafr, da, wenn die Behauptung fr ein beliebiges n gilt,
sie auch fr n + 1 gilt.
Die erste Zutat (der Induktionsanfang) lt sich meist durch Einsetzen
beweisen, fr die zweite Zutat (den Induktionsschlu) ist in der Regel
Algebra ntig. Da die Behauptung nach Zutat Nr. 1 fr 0 gilt, mu
sie nach Zutat Nr. 2 auch fr 1 gelten, und deshalb auch fr 2, . . .
und damit fr alle natrlichen Zahlen. (Es mu nicht unbedingt bei
0 losgehen, sondern kann auch bei einer beliebigen Zahl a losgehen
dann gilt aber die Behauptung nur fr alle natrlichen Zahlen ab a.)
Wenn die Behauptung erst einmal formuliert ist, sind induktive
Beweise oft einfach zu fhren, da sie meist dem o.g. Schema folgen.
Das wichtigste dabei ist, das Schema auch tatschlich einzuhalten.
Darum empfiehlt es sich, folgende Anleitung zu befolgen eine Art
Konstruktionsanleitung fr Beweise mit vollstndiger Induktion:
1. Formulieren Sie die zu beweisende Behauptung als Behauptung der
Form Fr alle n N gilt . . . , falls das noch nicht geschehen ist.
2. Schreiben Sie die berschrift n = 0:. Schreiben Sie die Behauptung
noch einmal ab, wobei Sie das fr alle n N weglassen und fr n
die 0 einsetzen.
3. Beweisen Sie die abgeschriebene Behauptung. (Das ist in der Regel
sehr einfach.)
4. Schreiben Sie das Wort Induktionsvoraussetzung: Schreiben Sie
darunter die Behauptung noch einmal ab, wobei Sie das fr alle
n N weglassen lassen Sie das n da, wo es ist.
5. Schreiben Sie Induktionsschlu (zu zeigen):. Schreiben Sie darunter
die Behauptung noch einmal ab, wobei Sie fr n stattdessen (n + 1)
einsetzen. (Wieder lassen Sie das fr alle n N weg.)
6. Beweisen Sie den Induktionsschlu unter Verwendung der Indukti-
onsvoraussetzung.
Wenn die Behauptung eine Gleichung der Form A = B ist, dann lt
sich hufig die Induktionsvoraussetzung entweder direkt oder nach
einigen Umformungen in den Induktionsschlu einsetzen.
Wir gehen die Anleitung anhand des obigen Beispiels durch. Die Be-
hauptung ist:
n
n(n + 1)(2n + 1)
i2 = 6
i =0
Die Behauptung ist durch Raten entstanden es gibt leider keine
Patentanleitung, solche Gleichungen zu finden: Hier sind Experimen-
tierfreude und Geduld gefragt. Steht die Behauptung erst einmal fest,
ist sie allerdings recht einfach zu beweisen:
1. Ausgeschrieben hat die Behauptung bereits die richtige Form:
n
n(n + 1)(2n + 1)
n N : i2 = 6
i =0
2. n = 0:
0
0(0 + 1)(2 0 + 1)
i2 = 6
i =0
Induktive Beweise und Definitionen 65

3. Beide Seiten der Gleichung sind 0, sie ist also bewiesen.


4. Induktionsvoraussetzung:
n
n(n + 1)(2n + 1)
i2 = 6
i =0

5. Induktionsschlu (zu zeigen):


n +1
(n + 1)((n + 1) + 1)(2(n + 1) + 1)
i2 = 6
i =0

6. Die Summe auf der linken Seite lt sich aufteilen:


n
(n + 1)((n + 1) + 1)(2(n + 1) + 1)
( i 2 ) + ( n + 1)2 =
i =0
6

Damit knnen wir die Induktionsvoraussetzung einsetzen, in der


in=0 i2 auf der linken Seite steht dieser Kniff funktioniert fast
immer bei Induktionsbeweisen fr Gleichungen ber Summen oder
Produkte. Es entsteht folgende Gleichung:

n(n + 1)(2n + 1) (n + 1)((n + 1) + 1)(2(n + 1) + 1)


( ) + ( n + 1)2 =
6 6
Die linke Seite knnen wir folgendermaen vereinfachen:

n(n + 1)(2n + 1) n(n + 1)(2n + 1) + 6(n + 1)2


+ ( n + 1)2 =
6 6
(n + 1)(n(2n + 1) + 6(n + 1))
=
6
(n + 1)(2n2 + n + 6n + 6)
=
6
(n + 1)(2n2 + 7n + 6)
=
6
Die rechte Seite knnen wir folgendermaen vereinfachen:

(n + 1)((n + 1) + 1)(2(n + 1) + 1) (n + 1)(n + 2)(2(n + 1) + 1)


=
6 6
(n + 1)(n + 2)(2n + 2 + 1)
=
6
(n + 1)(n + 2)(2n + 3)
=
6
2
(n + 1)(2n + 4n + 3n + 6)
=
6
(n + 1)(2n2 + 4n + 3n + 6)
=
6
(n + 1)(2n2 + 7n + 6)
=
6
Damit ist bei der linken und der rechten Seite jeweils das gleiche
herausgekommen und die Behauptung ist bewiesen.
66 Kapitel 6

6.3 Struktur der natrlichen Zahlen

Die vollstndige Induktion aus dem vorigen Abschnitt ist nur fr die
Menge der natrlichen Zahlen geeignet. Sie funktioniert aber nicht fr
alle Mengen: Fr die reellen Zahlen R z.B. erreicht die Konstruktion
bei 0 anfangen und dann immer um 1 hochzhlen einfach nicht alle
Elemente der Menge.Die natrlichen Zahlen sind also etwas besonderes.
Das liegt daran, da sich eine Art Bastelanleitung fr alle Elemente von
N angeben lt:
0 ist eine natrliche Zahl.
Fr eine bekannte natrliche Zahl n ist auch n + 1 eine natrliche
Zahl.
Diese Anleitung erreicht jede natrliche Zahl jede Zahl kann in der
Form 0 + 1 + . . . + 1 geschrieben werden. Fr diese Art Bastelanleitung
fr Mengen ist charakteristisch, da es ein oder mehrere Basiselemente
gibt (in diesem Fall die 0) und dann ein oder mehrere Regeln, die aus
beliebigeren kleineren Elementen grere Elemente konstruieren,
in diesem Fall die Regel, die besagt, da fr jede natrliche Zahl auch
ihr Nachfolger eine natrliche Zahl ist. Umgekehrt lt sich die Menge
der natrlichen Zahlen N folgendermaen definieren:

Definition 6.1 (natrliche Zahlen) Die Menge der natrlichen Zahlen


N ist folgendermaen definiert:
1. 0 N
2. Fr n N ist auch n + 1 N.
3. Die obigen Regeln definieren alle n N.
Eine solche Definition heit auch induktive Definition, die eine induktive
Menge definiert. Die letzte Klausel ist bei induktiven Definition immer
dabei ohne sie knnte z.B. die Menge der reellen Zahlen als N durch-
gehen, weil die Klauseln davor keinen Anspruch auf Vollstndigkeit
erheben knnten. Diese Klausel heit induktiver Abschlu.
Wer genau hinschaut, sieht, da die induktive Definition genau die
gleiche Struktur hat wie die Definition der Induktion von Seite 63:
Es gibt eine Klausel fr die 0 und eine Klausel fr den Schritt von n
nach n + 1. Die induktive Definition sagt, da es zwei verschiedene
Sorten von natrlichen Zahlen gibt, nmlich das Basiselement 0 aus der
ersten Regel der Definition und die (positiven) Zahlen n + 1 aus der
zweiten Klausel. Anders gesagt sind die natrlichen Zahlen so etwas
wie gemischte Daten.
Entsprechend mu eine Behauptung ber die natrlichen Zahlen fr
beide Sorten bewiesen werden: Einerseits fr das Basiselement 0 und
andererseits fr die positiven Zahlen, die keine Basiselemente sind. Die
Klauseln fr die Basiselemente heien Induktionsverankerungen. Da die
zweite Klausel einen Selbstbezug enthlt es mu schon eine natrliche
Zahl da sein, um eine weitere zu finden mu dort ein Induktionsschlu
bewiesen werden. Der Begriff des Selbstbezugs entspricht dem, der bei
Listen in Kapitel 5 eingefhrt wurde.
Die natrlichen Zahlen haben nur ein Basiselement und es gibt nur
eine Klausel mit Selbstbezug: Das ist nicht bei allen induktive Defi-
nitionen so allgemein kann eine induktive Definition beliebig viele
Induktive Beweise und Definitionen 67

Basiselemente (mindestens eins) und beliebig viele Klauseln mit Selbst-


bezug haben. Dafr wird es in diesem Buch noch mehrere Beispiele
geben.

6.4 Endliche Folgen

Die natrlichen Zahlen sind nicht die einzige induktiv definierte Menge.
Ein weiteres Beispiel sind die endlichen Folgen, die den Listen entspre-
chen:

Definition 6.2 Sei M eine beliebige Menge. Die Menge M der endlichen
Folgen ber M ist folgendermaen definiert:
1. Es gibt eine leere Folge2 e M .
2. Wenn f M eine Folge ist und m M, so ist m f M , also auch
eine Folge.
3. Die obigen Regeln definieren alle f M .
Eine Folge entsteht also aus einer bestehenden Folge dadurch, da
vorn noch ein Element angehngt wird. Folgen ber M = { a, b, c} sind
deshalb etwa

e, ae, be, ce, aae, abe, ace, . . . , abce, . . . cbbae, . . . (nicht alphabetisch sortiert)

Da das e bei nichtleeren Folgen immer dazugehrt, wird es oft nicht


mitnotiert.
Die Definition der endlichen Folgen ist analog zur Definition 6.1: Die
erste Klausel ist der Basisfall, die zweite Klausel enthlt einen Selbstbezug
und die dritte Klausel bildet den induktiven Abschlu.

6.4.1 Funktionen auf endlichen Folgen

Dieser Abschnitt demonstriert, wie mathematische Funktionen auf


endlichen Folgen formuliert werden. Als Beispiel ist eine Funktion
s : R R gefragt, die fr eine Folge deren Summe ausrechnet, also
z.B.:
s (1 2 3 e ) = 6
s(2 3 5 7e) = 17
s(17 23 42 5 e) = 87

Die Funktion entspricht also der Prozedur list-sum aus Abschnitt 5.2.
Da es zwei verschiedene Sorten endlicher Folgen gibt die leere Folge
und die nichtleeren Folgen liegt es nahe, die entsprechende Funkti-
on mit einer Verzweigung zu schreiben, die zwischen den beiden Sorten
unterscheidet:
(
def ? falls f = e
s( f ) =
? falls f = m f 0 , m M, f 0 M

2 Auf den ersten Blick scheint das Konzept einer leeren Folge, die keine Elemente enthlt,
merkwrdig. Die Informatik hat aber die Erfahrung gemacht, dass allzu oft Program-
me abstrzen, weil sie irgendwo nichts vorfinden, wo sie doch mindestens etwas
erwarteten. Auf den zweiten Blick ist die leere Folge aber das Analog zu der 0 bei den
natrlichen Zahlen.
68 Kapitel 6

Es fehlen nur noch die Teile der Definition, wo Fragezeichen stehen:


Die Summe der leeren Folge ist 0: Wenn es sich um eine andere Zahl
m 6= 0 handeln wrde, liee sich schlielich die Summe einer beliebigen
Folge durch das Anhngen der leeren Folge um m verndern. Die erste
Lcke ist also schon geschlossen:
(
def 0 falls f = e
s( f ) =
? falls f = m f 0 , m M, f 0 M

Im zweiten Fall handelt es sich bei f um ein zusammengesetztes Objekt


mit den Bestandteilen m und f 0 . Deshalb knnen m und f 0 fr die
Konstruktion des Funktionswerts herangezogen werden:
(
def 0 falls f = e
s( f ) =
. . . m . . . f . . . falls f = m f 0 , m M, f 0 M
0

Soweit sind die bekannten Techniken fr die Konstruktion von Funktio-


nen auf gemischten und zusammengesetzten Daten zur Anwendung
gekommen, lediglich bertragen von Scheme-Prozeduren auf mathe-
matische Funktionen.
Fr den nchsten Schritt pat genau wie beim Programmieren ein
rekursiver Aufruf zum Selbstbezug. Die Funktionsdefinition verdichtet
sich folgendermaen:
(
def 0 falls f = e
s( f ) =
. . . m . . . s( f 0 ) . . . falls f = m f 0 , m M, f 0 M

Hier ist s( f 0 ) die Summe aller Folgenelemente in f 0 . Gefragt ist die


Summe aller Folgenelemente von f = m f 0 . Es fehlt also zur Summe nur
noch m selbst:
(
def 0 falls f = e
s( f ) =
m + s( f ) falls f = m f 0 , m M, f 0 M
0

Mit Papier und Bleistift lt sich schnell nachvollziehen, da die Defini-


tion korrekt arbeitet:
s(17 23 42 5 e) = 17 + s(23 42 5 e)
= 17 + 23 + s(42 5 e)
= 17 + 23 + 42 + s(5 e)
= 17 + 23 + 42 + 5 + s(e)
= 17 + 23 + 42 + 5 + 0
= 87

Die Definition von s ruft sich also an der Stelle selbst auf, an der die
induktive Definition der endlichen Folgen den Selbstbezug f 0 M
eine Folge enthlt.
Mathematisch geneigte Leser werden die Definition von s mit Skep-
sis betrachten, taucht doch s sowohl auf der linken als auch auf der
rechten Seite auf es sieht so aus, als sei s durch sich selbst definiert.
Tatschlich ist dies jedoch kein Problem, da:
Induktive Beweise und Definitionen 69

sich s( f ) stets selbst auf einer krzeren Folge f 0 aufruft, und


schlielich bei der leeren Folge landet, bei der die Verzweigung greift
und keinen weiteren rekursiven Aufruf mehr vornimmt.
Solange eine rekursive Funktion dem Schema von s folgt und damit der
Struktur der Folgen selbst, sind diese beiden Bedingungen automatisch
erfllt.

6.4.2 Folgeninduktion

Das Gegenstck zur vollstndigen Induktion heit bei den Folgen Fol-
geninduktion. Der Schlu von n auf n + 1 wird bei der Folgeninduktion
zu je einem Schlu von f auf m f fr alle Folgen f und alle m M . Oft
kommt es dabei auf das Folgenelement m gar nicht an.
Zum Beweis der Behauptung, da eine bestimmte Behauptung fr
alle f M gilt, gengt es, die folgenden Beweise zu fhren:
1. Die Behauptung gilt fr f = e (Induktionsanfang)
2. Wenn die Behauptung fr eine Folge f gilt, so gilt sie auch fr alle
Folgen m f wobei m M. (Induktionsschlu).
Die Folgeninduktion funktioniert, weil sie der Struktur der Definition
der endlichen Folgen 6.2 genauso folgt wie die vollstndige Induktion
der Struktur der natrlichen Zahlen. Das Lemma lt sich auch mit
Hilfe der vollstndigen Induktion beweisen siehe Aufgabe 6.7.
Entsprechend der vollstndigen Induktion gibt es auch fr die Fol-
geninduktion eine Anleitung:
1. Formulieren Sie die zu beweisende Behauptung als Behauptung der
Form Fr alle f M gilt . . . , falls das noch nicht geschehen ist.
2. Schreiben Sie die berschrift f = e:. Schreiben Sie die Behauptung
noch einmal ab, wobei Sie das fr alle f M weglassen und fr f
das e einsetzen.
3. Beweisen Sie die abgeschriebene Behauptung. (Das ist in der Regel
sehr einfach.)
4. Schreiben Sie das Wort Induktionsvoraussetzung: Schreiben Sie
darunter die Behauptung noch einmal ab, wobei Sie das fr alle
f M weglassen lassen Sie das f da, wo es ist.
5. Schreiben Sie Induktionsschlu (zu zeigen):. Schreiben Sie darunter
die Behauptung noch einmal ab, wobei Sie fr f stattdessen m f
einsetzen. (Wieder lassen Sie das fr alle f M weg.)
6. Beweisen Sie den Induktionsschlu unter Verwendung der Indukti-
onsvoraussetzung. Denken Sie daran, die Behauptung fr alle m M
zu beweisen das ist meist aber nicht immer trivial.
Wenn die Behauptung eine Gleichung der Form A = B ist, dann lt
sich hufig die Induktionsvoraussetzung entweder direkt oder nach
einigen Umformungen in den Induktionsschlu einsetzen.
Fr ein sinnvolles Beispiel dient die Funktion cat, die Folgen aneinan-
derhngt:
(
def f2 falls f 1 = e
cat( f 1 , f 2 ) = 0
m cat( f 1 , f 2 ) falls f 1 = m f 10 , m M, f 10 M
70 Kapitel 6

Es soll bewiesen werden, da cat assoziativ ist, d.h. fr alle u, v, w M


gilt
cat(u, cat(v, w)) = cat(cat(u, v), w).

1. Die Form der Behauptung ist noch problematisch, da in ihr drei


Folgen auftauchen und keine davon heit f . Im schlimmsten Fall
mssen Sie raten, ber welche Folge die Induktion geht und gegebe-
nenfalls alle Mglichkeiten durchprobieren. Wir entscheiden uns fr
die erste Folge u und benennen Sie in f um, damit der Beweis auf
die Anleitung pat:

f M : v, w M : cat( f , cat(v, w)) = cat(cat( f , v), w)

2. f = e: Hier gilt

cat(e, cat(v, w)) = cat(v, w)


cat(cat(e, v), w) = cat(v, w)

nach der definierenden Gleichung.


3. Induktionsvoraussetzung:

cat( f , cat(v, w)) = cat(cat( f , v), w)

4. Induktionsschlu (zu zeigen):

cat(m f , cat(v, w)) = cat(cat(m f , v), w)

5. Wir benutzen die Definition von cat, um die linke Seite weiter auszu-
rechnen:
cat(m f , cat(v, w)) = mcat( f , cat(v, w))
Hier steht aber cat( f , cat(v, w)) und das steht auch auf der linken
Seite der Induktionsvoraussetzung. Wir knnen also einsetzen:

= mcat(cat( f , v), w)

Ebenso knnen wir die rechte Seite ausrechnen:

cat(cat(m f , v), w) = cat(mcat( f , v), w)


= mcat(cat( f , v), w)

Das ist aber das gleiche, das auch bei der linken Seite herausgekom-
men ist der Beweis ist fertig.

6.5 Notation fr induktive Definitionen

Induktive Mengen kommen in der Informatik enorm hufig vor so


hufig, da sich eine Kurzschreibweise fr ihre induktiven Definitionen
eingebrgert hat, die sogenannte kontextfreie Grammatik. In den meisten
Informatik-Bchern geht mit dem Begriff eine langwierige mathemati-
sche Definition einher, die fr dieses Buch nicht in aller Ausfhrlichkeit
bentigt wird. Hier dient die kontextfreie Grammatik (ab hier einfach
nur Grammatik) informell als Abkrzung fr eine lnger ausgeschrie-
bene induktive Definition. Hier die Grammatik fr die natrlichen
Zahlen:
Induktive Beweise und Definitionen 71

hNi 0 | hNi + 1

In der Grammatik steht hNi fr eine natrliche Zahl. Zu lesen ist die
Grammatik so: Eine natrliche Zahl ist entweder die 0 oder hat die Form
n + 1 wobei n wiederum eine natrliche Zahl ist. Der obligatorische
induktive Abschlu (Die obigen Regeln definieren alle n N.) wird
stillschweigend vorausgesetzt und darum weggelassen.
Das Zeichen kann also als ist oder hat die Form gelesen
werden, das Zeichen | als oder. Die Notation h X i definiert eine ent-
sprechende Menge X. Die Grammatik ist also eine Art mathematische
Schreibweise fr zusammengesetzte Daten, gemischte Daten (|) und
Selbstbezge, nur eben fr mathematische Objekte.
Fr die Menge der Folgen ber einer Menge M gengt also folgende
Notation:
h M i e | h Mi h M i
In beiden Definitionen ist jeweils der Selbstbezug klar zu sehen: Bei
den natrlichen Zahlen taucht hNi in einer Klausel auf, bei den Folgen
h M i. Alle anderen Klauseln also die ohne Selbstbezug beschreiben
Basisflle.

6.6 Strukturelle Rekursion

In Abschnitt 6.4 ist zu sehen, da die Definition der beiden Beispiel-


funktionen auf Folgen (s und cat) jeweils der induktiven Definition der
Folgen folgt die Definition beider Funktionen hat die Form:
(
def . . . falls f = e
F ( f , . . .) =
. . . F ( f ) . . . falls f = m f 0 , m M, f 0 M
0

Das ist kein Zufall: Die rekursive Funktionsdefinition gehrt zur induk-
tiven Mengendefinition wie Pech zu Schwefel. Zwei Grundregeln legen
diese Form fest:
1. Fr jede Klausel gibt es eine Verzweigung mit einem Zweig der
induktiven Definition.
2. Bei Selbstbezgen steht im entsprechenden Zweig ein Selbstaufruf.
Auch bei den natrlichen Zahlen lassen sich viele Operationen rekursiv
aufschreiben. Zum Beispiel die Potenz:
(
n def 1 falls n = 0
b = 0
bbn falls n = n0 + 1, n0 N

Die Notation n = n0 + 1 folgt zwar der induktiven Definition, ist hier


aber gleichbedeutend mit n > 0 und n0 = n 1. Deshalb werden
induktive Definitionen auf natrlichen Zahlen meist so geschrieben:
(
n def 1 falls n = 0
b =
bbn1 falls n N, n > 0

Die Korrespondenz zwischen induktiven Definitionen und den rekursi-


ven Funktionen lt sich auch allgemein formulieren. Angenommen,
die Menge X ist durch einer Grammatik definiert, die n Klauseln hat:
72 Kapitel 6

hXi C1 | ...| Cn
Eine Funktion auf dieser Menge braucht dann genau wie bei gemisch-
ten Daten eine Verzweigung mit n Zweigen, eine fr jede Klausel:

R1 falls x = F1

def
F(x) = . . .

Rn falls x = Fn

Die Bedingung x = Fi ergibt sich jeweils aus der entsprechenden Klau-


sel. Wenn dort Bezge zu anderen Mengen oder Selbstbezge stehen,
so werden diese durch Variablen ersetzt und entsprechende Mengenzu-
gehrigkeiten. Angenommen, die Klausel Ci hat beispielsweise diese
Form:
[ hAi & hBi ]

Dann mte Fi entsprechend so aussehen:

x = [ a & b ], a A, b B

Eine Klausel Ci mit Selbstbezug she beispielsweise so aus:


{ hXi }

Das dazugehrige Fi she so aus:

x = { x 0 }, x 0 X

Dementsprechend steht hchstwahrscheinlich in der rechten Seite Ri


ein rekursiver Aufruf F ( x 0 ).
Funktionen dieser Form, die der Struktur einer induktiven Menge
direkt folgen, heien strukturell rekursiv.
Hier ein Beispiel fr eine induktiv definierte Menge, deren Struktur
etwas reichhaltiger ist, als die der natrlichen Zahlen oder der Folgen.
Die Grammatik beschreibt einfache aussagenlogische Ausdrcke:
hEi > |
| hEi hEi
| hEi hEi
| hEi
Gedacht ist das folgendermaen: > steht fr wahr, fr falsch,
fr und, fr oder und fr nicht. Beispiele fr Ausdrcke
sind:
>
>
>

Beim Ausdruck > > ist nicht klar, wie er gemeint ist, da die
Klammerung fehlt. In solchen Fllen schreiben wir einen Ausdruck
genau wie in der Arithmetik mit Klammern:

(> ) >
> ( >)
Induktive Beweise und Definitionen 73

Jeder solche Ausdruck hat einen Wahrheitswert, also wahr oder


falsch, und eine strukturell rekursive Funktion kann diesen Wahr-
heitswert errechnen. Diese Funktion heit J K fr einen Ausdruck e
liefert JeK den Wert 1, falls der Ausdruck wahr ist und 1, falls falsch.
Die doppelten eckigen Klammern heien in der Fachsprache Seman-
tikklammern und werden oft benutzt, um Funktionen auf Mengen zu
schreiben, die durch eine Grammatik definiert sind.
Die Funktionsdefinition besteht, wie oben beschrieben, auf jeden Fall
aus einer Verzweigung, die fr jede Klausel der Grammatik einen Zweig
aufweist:


? falls e = >
? falls e =



def
JeK = ? falls e = e1 e2

? falls e = e1 e2




? falls e = e0

In den Zweigen fr , und sind rekursive Aufrufe erlaubt:



?

falls e = >
? falls e =



def
JeK = . . . Je1 K . . . Je2 K falls e = e1 e2

. . . Je1 K . . . Je2 K falls e = e1 e2




. . . Je0 K . . .

falls e = e0

Die ersten beiden Flle liefern 1 und 0 respektive, fr die weiteren Flle
werden folgende Hilfsfunktionen definiert:

( (
def 1 falls t1 = 1 und t2 = 1 def 0 falls t1 = 0 und t2 = 0
a ( t1 , t2 ) = o ( t1 , t2 ) =
0 sonst 1 sonst
(
def 1 falls t = 0
n(t) =
0 falls t = 1
Die Funktion a liefert nur dann 1, wenn t1 und t2 1 sind (sonst 0).
Die Funktion o liefert dann 1, wenn t1 oder t2 1 sind (sonst 0). Die
Funktion n liefert dann 1, wenn t nicht 1 ist (sonst 0). Diese Funktionen
vervollstndigen nun die Definition von J K:



1 falls e = >
0 falls e =




def
JeK = a(Je1 K, Je1 K) falls e = e1 e2

o (Je1 K, Je1 K) falls e = e1 e2




n(Je0 K)

falls e = e0

6.7 Strukturelle Induktion

Beweise von Eigenschaften strukturell rekursiver Funktionen funktio-


nieren wieder entsprechend den natrlichen Zahlen und den Folgen
mit struktureller Induktion. Strukturelle Induktion folgt ebenfalls der
74 Kapitel 6

Struktur der Grammatik vollstndige Induktion und Folgeninduktion


sind also Spezialflle. Folgendes Beispiel aufbauend auf den aussa-
genlogischen Ausdrcken aus dem vorigen Abschnitt illustriert die
Technik:

Satz 6.3 Aus einem aussagenlogischen Ausdruck e entsteht der Aus-


druck e, indem jedes > durch , jedes durch >, jedes durch und
jedes durch ersetzt wird. Es gilt fr jeden Ausdruck e:

JeK = n(JeK)

Die Behauptung hat bereits die fr einen Induktionsbeweis geeignete


Form fr alle e, wobei e aus einer induktiv definierten Menge kommt.
Die Behauptung mu jetzt fr alle mglichen Flle von e bewiesen
werden da die Grammatik fr Ausdrcke fnf Flle hat, sind auch
fnf Flle zu beweisen: e = >, e = , e = e1 e2 , e1 e2 und e = e0 ,
wobei e1 , e2 und e0 ihrerseits Ausdrcke sind.
Hier sind die Beweise fr die Flle e = >, e = :

J>K = 1 JK = 0

J>K = JK JK = J>K
=0 =1
= n (1) = n (0)
Als nchstes ist der Fall e = e1 e2 an der Reihe. Da die Klausel
hEi . . . | hEi hEi
zwei Selbstbezge hat, gibt es auch eine zweiteilige Induktionsvoraus-
setzung die Behauptung kann fr e1 und e2 angenommen werden:
Je1 K = n(Je1 K), Je2 K = n(Je2 K). Der Induktionsschlu dazu sieht so aus:

Je1 e2 K = a(Je1 K, Je2 K)


Je1 e2 K = Je1 e2 K Definition von
= o (Je1 K, Je2 K)
= o (n(Je1 K), n(Je2 K)) Induktionsvoraussetzung
= n( a(Je1 K, Je2 K))

Der letzte Schritt ergibt sich aus genauer Betrachtung der Definitionen
von o und a bzw. durch Einsetzen aller mglichen Werte fr t1 und t2 .
Der Fall e = e1 e2 folgt analog. Wieder gibt es zwei Selbstbezge,
also gilt wieder die zweiteilige Induktionsvoraussetzung Je1 K = n(Je1 K),
Je2 K = n(Je2 K). Induktionsschlu (zu zeigen):

Je1 e2 K = o (Je1 K, Je2 K)


Je1 e2 K = Je1 e2 K Definition von
= a(Je1 K, Je2 K)
= a(n(Je1 K), n(Je2 K)) Induktionsvoraussetzung
= n(o (Je1 K, Je2 K))
Induktive Beweise und Definitionen 75

Schlielich fehlt noch der Fall e = e0 . Hier gibt es nur einen Selbstbe-
zug, entsprechend lautet die Induktionsvoraussetzung: Je0 K = n(Je0 K).
Induktionsschlu (zu zeigen):
Je0 K = n(Je0 K)

J e 0 = e 0 Definition von
= n(Je0 K)
= n(n(Je0 K)) Induktionsvoraussetzung
Das Beispiel zeigt, da strukturell induktive Beweise durchaus mehr
als einen Induktionsanfang haben knnen hier die Flle > und .
Ebenso gibt es mehr als einen Induktionsschlu einen fr jede Klausel
mit Selbstbezug, hier sind das drei Flle.
Fr Beweise mit struktureller Induktion gilt also folgende Anleitung:
1. Formulieren Sie die zu beweisende Behauptung als Behauptung der
Form Fr alle x X gilt . . . (wobei X eine induktiv definierte
Menge ist), falls das noch nicht geschehen ist.
2. Fhren Sie jetzt einen Beweis fr jede einzelne Klausel Ci der induk-
tiven Definition:
3. Schreiben Sie die berschrift x = Fi , wobei Fi eine Bedingung ist,
die der Klausel entspricht. Schreiben Sie die Behauptung noch einmal
ab, wobei Sie das fr alle x X weglassen und fr x stattdessen Fi
einsetzen.
4. Wenn die Klausel keinen Selbstbezug enthlt, so beweisen Sie die
Behauptung direkt.
5. Wenn die Klausel einen oder mehrere Selbstbezge enthlt, so stehen
in der berschrift Variablen x j oder x 0 o.., die ihrerseits Element
von X sind. Schreiben Sie dann die berschrift Induktionsvorausset-
zung:. Schreiben Sie darunter fr jeden Selbstbezug die Behauptung
noch einmal ab, wobei Sie das fr alle x X weglassen und fr x
stattdessen x j bzw. x 0 einsetzen.
Schreiben Sie darunter das Wort Induktionsschlu und beweisen
die Behauptung. Denken Sie daran, die Induktionsvoraussetzung zu
benutzen.

Anmerkungen

Die erste konstruktive Definitionen der natrlichen Zahlen wurde be-


reits im Jahr 1889 von Peano vorgeschlagen:

Definition 6.4 (Peano-Axiome) Die Menge N der natrlichen Zahlen ist


durch folgende Eigenschaften, die Peano-Axiome, gegeben:
1. Es gibt eine natrliche Zahl 0 N.
2. Zu jeder Zahl n N gibt es eine Zahl n0 N, die Nachfolger von n
heit.
3. Fr alle n N ist n0 6= 0.
4. Aus n0 = m0 folgt n = m.
5. Eine Menge M von natrlichen Zahlen, welche die 0 enthlt und mit
jeder Zahl m M auch deren Nachfolger m0 , ist mit N identisch.
76 Kapitel 6

Diese Definition ist quivalent zu Definition 6.1 von Seite 66. Wie in
Definition 6.1, lt sich N sich aus den Peano-Axiomen schrittweise
konstruieren. Aus 1. und 2. folgt, da es natrliche Zahlen
0, 00 , 000 , 0000 , 00000 , . . .

gibt. Ohne die 0 am Anfang entsteht die Darstellung der natrlichen


Zahlen durch Striche. Die Axiome 3 und 4 besagen, da jede Zahl nur
eine Strichdarstellung besitzt: jedes Anfgen eines Striches 0 erzeugt
eine vllig neue Zahl.
Die Peano-Formulierung ist zwar hnlich zu Definition 6.1, weist
aber auch interessante Unterschiede auf:

Fr n0 ist im tglichen Umgang natrlich die Bezeichnung n + 1


gebruchlich.
Die dritte und vierte Bedingung zusammen besagen, da die Nach-
folgerfunktion injektiv ist, d.h. durch fortgesetzte Anwendung der
Nachfolgerfunktion entstehen immer neue Elemente.
Schlielich beschreibt die fnfte Bedingung den induktiven Abschlu,
der festlegt, da auer den solchermaen erzeugten Elementen kei-
ne weiteren existieren. Axiom 5 wird auch Induktionsaxiom genannt.
In Definition 6.1 hat das Induktionsaxiom die blichere Form Die
obigen Regeln definieren alle n N. Ebenfalls blich ist eine For-
mulierung wie wie . . . die kleinste Menge mit den Eigenschaften
. . . .

Aufgaben

Aufgabe 6.1 Beweisen Sie die Gausche Summenformel mit vollstndi-


ger Induktion:
n
n ( n + 1)
n N : i =
i =0
2

Aufgabe 6.2 Betrachten Sie folgende Tabelle:

1=1
1 4 = (1 + 2)
14+9 = 1+2+3
1 4 + 9 16 = (1 + 2 + 3 + 4)

Raten Sie die Gleichung, die dieser Tabelle zugrundeliegt und schreiben
Sie in mathematischer Notation auf. Beweisen Sie die Gleichung!

Aufgabe 6.3 Beweisen Sie, da folgende Gleichung fr alle n N gilt:


!2
n n
i 3
= i
i =0 i =0

Aufgabe 6.4 Was ist falsch an folgendem Induktionsbeweis?


Behauptung Alle Pferde haben die gleiche Farbe.
Beweis
Induktive Beweise und Definitionen 77

Induktionsanfang Fr eine leere Menge von Pferden gilt die Behaup-


tung trivialerweise.
Induktionsschlu Gegeben sei eine Menge von n + 1 Pferden. Nimm
ein Pferd aus der Menge heraus die restlichen Pferde haben per In-
duktionsannahme die gleiche Farbe. Nimm ein anderes Pferd aus der
Menge heraus wieder haben die restlichen Pferde per Induktions-
annahme die gleiche Farbe. Da die brigen Pferde die Farbe in der
Zwischenzeit nicht pltzlich gewechselt haben knnen, war es in beiden
Fllen die gleiche Farbe, und alle n + 1 Pferde haben diese Farbe. 

Aufgabe 6.5 Beweise mittels Induktion, da ber Folgen fr u, v M


gilt:
len(cat(u, v)) = len(u) + len(v)
Dabei sei len die Lnge von Folgen, definiert als:
(
def 0 falls f = e
len( f ) =
len( f 0 ) + 1 falls f = m f 0

Aufgabe 6.6 Beweisen Sie durch Folgeninduktion:

cat(v, w) = cat(z, w) v = z
cat(v, w) = cat(v, z) w = z

Aufgabe 6.7 Beweisen Sie die Korrektheit der Folgeninduktion aus Ab-
schnitt 6.4.2 (Seite 69) mit Hilfe der vollstndigen Induktion. Beschrei-
ben Sie dazu, wie sich eine Aussage ber alle Folgen in eine Aussage
ber die Lngen der Folgen umwandeln lt. Setzen Sie dann die Klau-
seln der vollstndigen Induktion mit den entsprechenden Klauseln der
Folgeninduktion in Beziehung.

Aufgabe 6.8 Geben Sie eine induktive Definition der umgangssprach-


lich definierten Funktion aus Abschnitt 6.7 (Seite 74) an.

Aufgabe 6.9 Geben Sie eine Datendefinition fr die aussagenlogischen


Ausdrcke aus Abschnitt 6.7 an. Programmieren Sie die Funktion J K
sowie die Funktion .

Aufgabe 6.10 Die Fibonacci-Funktion auf den natrlichen Zahlen ist


folgendermaen definiert:

0 falls x = 0
def
fib(n) = 1 falls x = 1
fib(n 2) + fib(n 1) sonst


n) die ganze Zahl ist, die am nchsten zu n / 5 liegt,
Beweise, da fib(
wobei = (1 + 5)/2.
Anleitung: Zeige, da fib ( n ) = ( n n ) / 5, wobei = (1

5)/2.
78 Kapitel 6

Aufgabe 6.11 Eine partielle Ordnung 4 auf einer Menge M heit wohl-
fundiert oder noethersch, wenn es keine unendlichen Folgen ( xi )iN gibt,
so da fr alle i N gilt

xi+1 4 xi und xi+1 6= xi

auch geschrieben als:


x i +1 x i .
Das Beweisprinzip der wohlfundierten oder noetherschen Induktion ist
dafr zustndig, die Gltigkeit eines Prdikats P auf M zu beweisen,
wenn es eine wohlfundierte Ordnung 4 auf auf M gibt. Es besagt, da
es ausreicht, P(z) unter der Voraussetzung nachzuweisen, da P(y) fr
alle Vorgnger y von z gilt. Anders gesagt:

z M : (y M : y z P(y)) P(z)) x M : P( x )

1. Beweise die Gltigkeit des Prinzips.


2. Leite die vollstndige und die strukturelle Induktion als Spezialflle
der wohlfundierten Induktion her.
7 Prozeduren ber natrlichen Zahlen

Aus der induktiven Definition der natrlichen Zahlen ergibt sich direkt
eine Schablone fr die Konstruktion von Prozeduren auf den natrli-
chen Zahlen. Angenommen, es sei eine Prozedur gefragt, die fr eine
Zahl n das Produkt aller Zahlen von 1 bis n berechnet. Dieses Produkt
heit Fakultt, auf Englisch factorial. Beschreibung und Signatur sind
wie folgt:
; das Produkt aller Zahlen von 1 bis n berechnen
(: factorial (natural -> natural))

Das Gerst ergibt sich aus der Signatur:


(define factorial
(lambda (n)
...))

Wie oben festgestellt, handelt es sich bei den natrlichen Zahlen um


eine Fallunterscheidung mit zwei Fllen. In der Schablone mu also
eine Verzweigung mit zwei Fllen stehen:
(define factorial
(lambda (n)
(cond
(... ...)
(... ...))))

Der erste Fall ist die 0, der andere Fall deckt alle anderen Zahlen ab:
(define factorial
(lambda (n)
(cond
((= n 0) ...)
(else ...))))

Diese Fallunterscheidung lt sich leichter mit if schreiben:1


1 Es wre genauso richtig, beide Zweige mit einem Test zu versehen, wie bei den Prozeduren
ber Listen:
(define factorial
(lambda (n)
(cond
((= n 0) ...)
((> n 0) ...))))

Umgekehrt liee sich auch die Fallunterscheidung bei Listen mit if schreiben. Welche
Variante am besten ist, ist Geschmackssache.
80 Kapitel 7

(define factorial
(lambda (n)
(if (= n 0)
...
...)))

Der Fall 0 ist hier gar nicht so einfach zu beantworten. Was sind schlie-
lich die Zahlen von 1 bis 0? Dafr ist der andere Zweig einfacher zu
ergnzen: hier ist nmlich die Konstruktionsanleitung fr zusammen-
gesetzte Daten anwendbar. Als Selektor wird die Vorgngeroperation
angewendet, es steht also (- n 1) in der Alternative. Genau wie bei
den endlichen Folgen liegt nahe, da auf (- n 1) ein rekursiver Aufruf
erfolgt:

(define factorial
(lambda (n)
(if (= n 0)
...
... (factorial (- n 1)) ...)))

Der rekursive Aufruf (factorial (- n 1)) soll nach Signatur und Be-
schreibung der Prozedur das Produkt der Zahlen von 1 bis n 1 be-
rechnen. Gefragt ist aber das Produkt der Zahlen von 1 bis n. Es fehlt
noch n selbst, das mit dem Ergebnis multipliziert werden mu:

(define factorial
(lambda (n)
(if (= n 0)
...
(* n (factorial (- n 1))))))

Es fehlt schlielich noch der erste Zweig der if-Form. In Fllen wie die-
sem, wo die Antwort nicht offensichtlich ist, lohnt es sich, ein oder zwei
einfache Beispiele durchzurechnen, die meist die Antwort zwingend
festlegen. Das einfachste Beispiel ist wohl 1 das Produkt der Zahlen
von 1 bis 1 sollte 1 sein. Auswertung mit dem Substitutionsmodell
ergibt:

(factorial 1)
= (if (= 1 0) ... (* 1 (factorial (- 1 1))))
= (if #f ... (* 1 (factorial (- 1 1))))
= (* 1 (factorial (- 1 1)))
= (* 1 (factorial 0))
= (* 1 (if (= 0 0) ... (* 0 (factorial (- 0 1)))))
= (* 1 (if #t ... (* 0 (factorial (- 0 1)))))
= (* 1 ...)

Damit ist klar, da die unbekannte Zahl, die an Stelle der ... stehen
mu, multipliziert mit 1 wiederum 1 ergeben mu. Die einzige Zahl,
die diese Bedingung erfllt, ist 1 selbst. Die vollstndige Definition von
factorial ist also:

(define factorial
(lambda (n)
Prozeduren ber natrlichen Zahlen 81

(if (= n 0)
1
(* n (factorial (- n 1))))))

An einem greren Beispiel lt sich anhand des Substitutionsmodells


oder im Stepper besonders gut sehen, wie die Rekursion verluft:
(factorial 4)
= (if (= 4 0) 1 (* 4 (factorial (- 4 1))))
= (if #f 1 (* 4 (factorial (- 4 1))))
= (* 4 (factorial (- 4 1)))
= (* 4 (factorial 3))
= (* 4 (if (= 3 0) 1 (* 3 (factorial (- 3 1)))))
= (* 4 (if #f 1 (* 3 (factorial (- 3 1)))))
= (* 4 (* 3 (factorial (- 3 1))))
= (* 4 (* 3 (factorial 2)))
...
= (* 4 (* 3 (* 2 (factorial 1))))
= (* 4 (* 3 (* 2 (if (= 1 0) 1 (* 1 (factorial (- 1 1)))))))
= (* 4 (* 3 (* 2 (if #f ... (* 1 (factorial (- 1 1)))))))
= (* 4 (* 3 (* 2 (* 1 (factorial (- 1 1))))))
= (* 4 (* 3 (* 2 (* 1 (factorial 0)))))
= (* 4 (* 3 (* 2 (* 1 (if (= 0 0) 1 (* 0 (factorial (- 0 1))))))))
= (* 4 (* 3 (* 2 (* 1 (if #t 1 (* 0 (factorial (- 0 1)))))))))
= (* 4 (* 3 (* 2 (* 1 1))))
= (* 4 (* 3 (* 2 1)))
= (* 4 (* 3 2))
= (* 4 6)
= 24

Die typischen Beobachtungen an diesem Beispiel sind:


Factorial ruft sich nie mit derselben Zahl n auf, mit der es selbst
aufgerufen wurde, sondern immer mit n 1.
Die natrlichen Zahlen sind so strukturiert, da die Kette n, n 1, n
2 . . . irgendwann bei 0 abbrechen mu.
Factorial ruft sich bei n = 0 nicht selbst auf.
Aus diesen Grnden kommt der von (factorial n) erzeugte Berech-
nungsproze immer zum Schlu.
Aus der Definition von factorial ergibt sich eine Konstruktionsanlei-
tung fr Prozeduren, die natrliche Zahlen verarbeiten. Die Schablone
fr solche Prozeduren sieht folgendermaen aus:
(: proc (natural -> ...))
(define p
(lambda (n)
(if (= n 0)
...
... ( p (- n 1)) ...)))
Konstruktionsanleitung ?? in Anhang ?? fat dies noch einmal zusam-
men.
FIXME: Prozeduren, die Listen erzeugen etc.
82 Kapitel 7

Aufgaben

Aufgabe 7.1 Schreibe eine Prozedur power2, die eine Zahl akzeptiert
und ihre Zweierpotenz zurckliefert.

Aufgabe 7.2 Schreibe eine Prozedur power, die fr eine Basis b und
einen Exponenten e N gerade be ausrechnet. Also:
(power 5 3)
, 125

Aufgabe 7.3 Die eingebaute Prozedur even? akzeptiert eine ganze


Zahl und liefert #t, falls diese gerade ist und #f sonst. Schreibe mit
Hilfe von even? eine Prozedur namens evens, welche fr zwei Zahlen
a und b eine Liste der geraden Zahlen zwischen a und b zurckgibt:
(evens 1 10)
, #<record:pair 2
#<record:pair 4
#<record:pair 6
#<record:pair 8
#<record:pair 10 #<empty-list>>>>>>

Die eingebaute Prozedur odd? akzeptiert eine ganze Zahl und liefert
#t, falls diese ungerade ist und #f sonst. Schreibe mit Hilfe von odd?
eine Prozedur odds, welche fr zwei Zahlen a und b eine Liste die
ungeraden Zahlen zwischen a und b zurckgibt:
(odds 1 10)
, #<record:pair 1
#<record:pair 3
#<record:pair 5
#<record:pair 7
#<record:pair 9 #<empty-list>>>>>>

Aufgabe 7.4 Das folgende Zahlenmuster heit das Pascalsche Dreieck:


1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
...
Die Zahlen an den Kanten des Dreiecks sind allesamt 1. Eine Zahl im
Innern des Dreiecks ist die Summe der beiden Zahlen darber. Schreibe
eine Prozedur pascal, die als Argumente die Nummer einer Zeile und
die Nummer einer Spalte innerhalb des Dreiecks akzeptiert, und
die Zahl im Pascalschen Dreieck berechnet. (Sowohl Zeilen- als auch
Spaltennummer fangen bei 1 an.)
(pascal 5 3)
, 6
TBD
Prozeduren ber natrlichen Zahlen 83

Aufgabe 7.5 Schreibe eine Prozedur drink-machine, die das Finanzma-


nagement eines Getrnkeautomaten durchfhrt.
Drink-machine soll drei Argumente akzeptieren: den Preis eines Ge-
trnks (in Euro-Cents), eine Liste der Centbetrge der Wechselgeldmn-
zen, die noch im Automaten sind, und den Geldbetrag, den der Kunde
eingeworfen hat. (Also gibt es ein Listenelement pro Mnze. Falls z.B.
mehrere Groschen im Automaten sind, finden sich in der Liste mehr-
mals die Zahl 10.) Herauskommen soll eine Liste der Centbetrge der
Mnzen, welche die Maschine herausgibt oder #f, falls die Maschine
nicht herausgeben kann.

(drink-machine 140 (list 50 100 500 10 10) 200)


, #<list 50 10>
8 Higher-Order-Programmierung

TBD

8.1 Higher-Order-Prozeduren auf Listen

Eine nahezu unerschpfliche Quelle fr Diskussionen stellen die Fu-


ballergebnisse dar. Hier stellen sich so bedeutsame Fragen wie:

Wann hat Bayern Mnchen zuletzt den 1. FC Kaiserslautern in einem


Auswrtsspiel geschlagen?
Wie viele Tore sind in dieser Saison schon gefallen?
Welche Mannschaft ist abstiegsgefhrdet?

In letzter Konsequenz luft die Beantwortung solcher und hnlicher


Fragen auf eine Datenbank hinaus; es geht aber auch schon wesentlich
einfacher:

; Ein Spiel hat:


; - Spieltag (natural)
; - Gastgeber-Team (string)
; - Gastgeber-Tore (natural)
; - Gast-Team (string)
; - Gast-Tore (natural)

(define-record-procedures game
make-game game?
(game-matchday
game-home-team game-home-goals game-guest-team game-guest-goals))
(: make-game (natural string natural string natural -> game))
(: game? (any -> boolean))
(: game-home-team (game -> string))
(: game-home-goals (game -> natural))
(: game-guest-team (game -> string))
(: game-guest-goals (game -> natural))

Es folgen hier beispielhaft die Ergebnisse des ersten Spieltags der


Bundesliga-Saison 2009/20101 :

(define g1 (make-game 1 "Wolfsburg" 2 "Stuttgart" 0))


(define g2 (make-game 1 "Mainz" 2 "Bayer 04" 2))

1 Die kompletten Ergebnisse dieser Saison lassen sich unter dem Namen soccer.rkt von
der Webseite des Buchs deinprogramm.de herunterladen.
86 Kapitel 8

(define g3 (make-game 1 "Hertha" 1 "Hannover" 0))


(define g4 (make-game 1 "Bremen" 2 "Frankfurt" 3))
(define g5 (make-game 1 "Nrnberg" 1 "Schalke" 2))
(define g6 (make-game 1 "Dortmund" 1 "1. FC Kln" 0))
(define g7 (make-game 1 "Hoffenheim" 1 "Bayern" 1))
(define g8 (make-game 1 "Bochum" 3 "Gladbach" 3))
(define g9 (make-game 1 "Freiburg" 1 "Hamburg" 1))

(define day1
(list g1 g2 g3 g4 g5 g6 g7 g8 g9))

Eine recht einfache Frage ist die Bestimmung der Punktzahl, welche
die Gastgebermannschaft in einem bestimmten Spiel erzielt hat. Eine
Prozedur, die das leistet, hat Beschreibung und Signatur wie folgt:

; Punktzahl in Spiel
(define points
(signature (one-of 0 1 3)))

; Punktzahl fr Gastgeber-Team berechnen


(: home-points (game -> points))

Die Punktzahl des Gastgeberteams errechnet sich auf einfache Weise


durch Vergleich der erzielten Tore:
(define home-points
(lambda (g)
(let ((g1 (game-home-goals g))
(g2 (game-guest-goals g)))
(cond
((> g1 g2) 3)
((< g1 g2) 0)
((= g1 g2) 1)))))

Aber auch das Gsteteam soll die ihm zustehenden Punkte bekommen.
Das fhrt zu folgender Prozedur:

; Punktzahl fr Gast-Team berechnen


(: guest-points (game -> points))

(define guest-points
(lambda (g)
(let ((g1 (game-guest-goals g))
(g2 (game-home-goals g)))
(cond
((> g1 g2) 3)
((< g1 g2) 0)
((= g1 g2) 1)))))

Wie nicht anders zu erwarten, sind diese beiden Prozeduren fast zu


hundert Prozent identisch, der einzige Unterschied ist die Definition
der lokalen Variablen g1 und g2. Eine solche Duplizierung von Code
ist immer schlecht, vor allem vor dem Gesichtspunkt der Wartbarkeit
von Programmen. Wenn etwa der Deutsche Fuballbund die Regeln fr
Higher-Order-Programmierung 87

die Vergabe von Punkten ndern sollte, mssten hier beide Prozeduren
in gleicher Weise angepasst werden. Die Lsung fr das Problem zeigt
sich dadurch, dass der gemeinsame Code aus den beiden Prozeduren
ausfaktorisiert wird.
Musterbildung ist eine der wichtigsten Abstraktionstechniken, und
deshalb gibt es ein eigenes Mantra:

Mantra 6 (Abstraktion aus Mustern) Wenn mehrere Prozeduren im Pro-


gramm bis auf wenige Stellen gleich aussehen, schreiben Sie eine allge-
meinere Prozedur, die darber abstrahiert, was an diesen Stellen steht.
Ersetzen Sie dann die ursprnglichen Prozeduren durch Anwendungen
der neuen, allgemeinen Prozedur.
Anwendung dieser Abstraktion knnte zu einer Prozedur compute-points
fhren, die dann wie folgt fr die Definition von home-points und
guest-points verwendet werden knnte:

(define home-points (compute-points game-home-goals game-guest-goals))


(define guest-points (compute-points game-guest-goals game-home-goals))

Die Signatur von compute-points msste dann wie folgt sein:


(: compute-points ((game -> natural) (game -> natural) -> (game -> points)))

Das verdient eine kurze Betrachtung: Hier liegt eine Prozedur vor, die
zwei Prozeduren als Argument akzeptiert und eine weitere Prozedur als
Ergebnis liefert. Solche Prozeduren heien Prozeduren hherer Ordnung
oder Higher-Order-Prozeduren.
Die Definition von compute-points ergibt sich recht leicht aus den
beiden schon vorgestellten Prozeduren:
(define compute-points
(lambda (goals-1 goals-2)
(lambda (g)
(let ((g1 (goals-1 g))
(g2 (goals-2 g)))
(cond
((> g1 g2) 3)
((< g1 g2) 0)
((= g1 g2) 1))))))

Eine weitere interessante Aufgabe ist es, aus einer Liste von Spielen
die unentschieden ausgegangenen Spiele herauszusuchen. Dazu ist
zunchst eine Prozedur notwendig, die feststellt, ob ein bestimmtes
Spiel unentschieden war:
; Ist Spiel unentschieden?
(: game-draw? (game -> boolean))

(define game-draw?
(lambda (g)
(= 1 (home-points g))))

Die jetzt gesuchte Prozedur muss Kurzbeschreibung und Signatur wie


folgt haben:
88 Kapitel 8

; Unentschiedene Spiele heraussuchen


(: games-draw ((list-of game) -> (list-of game)))

Die Definition folgt der Konstruktionsanleitung fr Prozeduren auf


Listen. Hier ist die Schablone:
(define games-draw
(lambda (lis)
(cond
((empty? lis) ...)
((pair? lis)
... (first lis) ...
... (games-draw (rest lis)) ...))))

Der erste Fall ist klar: wo keine Spiele sind, sind auch keine unentschie-
denen. Der zweite Fall betrachtet das erste Element (first lis). Dabei
ist entscheidend, ob es sich dabei um ein unentschiedenes Spiel handelt
oder nicht:
(define games-draw
(lambda (lis)
(cond
((empty? lis) empty)
((pair? lis)
... (game-draw? (first lis)) ...
... (games-draw (rest lis)) ...))))

Die Fallunterscheidung bestimmt, ob ein Spiel in die Ergebnisliste


kommt oder nicht:
(define games-draw
(lambda (lis)
(cond
((empty? lis) empty)
((pair? lis)
(let ((f (first lis))
(r (games-draw (rest lis))))
(if (game-draw? f)
(make-pair f r
r)))))))

Fertig!
Eine ganz hnliche Prozedur sortiert aus einer Liste von Spielen
diejenigen aus, an denen eine bestimmte Mannschaft teilgenommen hat:

; Spielt Team bei Spiel?


(: plays-game? (string game -> boolean))

(define plays-game?
(lambda (t g)
(or (string=? t (game-home-team g))
(string=? t (game-guest-team g)))))

; Alle Spiele mit einem Team herausfiltern


Higher-Order-Programmierung 89

(: games-playing (string (list-of game) -> (list-of game)))

(define games-playing
(lambda (t lis)
(cond
((empty? lis) empty)
((pair? lis)
(let ((f (first lis))
(r (games-playing t (rest lis))))
(if (plays-game? t f)
(make-pair f r)
r))))))

Die Prozeduren games-draw und games-playing unterscheiden sich, ab-


gesehen vom Namen und der Tatsache, dass games-playing noch den
Namen eines Teams als zustzlichen Parameter hat, nur an einer Stel-
le: games-draw verwendet game-draw? an der Stelle, wo games-playing
plays-game? verwendet. Eine einzelne Prozedur knnte die Aufgaben
sowohl von games-draw als auch von games-playing lsen, indem sie an
der Stelle, an der game-draw? bzw. plays-game? steht, verallgemeinert.
Das geht mit Abstraktion: fr das konkrete Prdikat wird ein Parameter
eingefhrt. Das Ergebnis, das sich ansonsten direkt aus den Definitio-
nen von games-draw und games-playing ergibt, sieht so aus (erst einmal
ohne Signatur, die nachgeliefert wird):

(define filter-games
(lambda (p? lis)
(cond
((empty? lis) empty)
((pair? lis)
(if (p? (first lis))
(make-pair (first lis)
(filter-games p? (rest lis)))
(filter-games p? (rest lis)))))))

Das funktioniert tatschlich:

(define plays-nrnberg?
(lambda (g)
(plays-game? "Nrnberg" g)))

(filter-games plays-nrnberg? day1)


, #<list #<record:game 1 "Nrnberg" 1 "Schalke" 2>>

Das Abstrahieren ber Prozeduren funktioniert also genauso wie die


Abstraktion ber andere Werte. Die Signatur fr die Prozedur mu
natrlich bercksichtigen, da p? eine Prozedur ist. Die Prozedur
plays-nrnberg?, die fr p? verwendet wird, hat die Signatur

(: plays-nrnberg? (game -> boolean))

und deshalb hat filter-games folgende Signatur:

(: filter-games ((game -> boolean) list(game) -> list(game)))


90 Kapitel 8

Tatschlich steht aber in filter-games auer dem Namen dieser Pro-


zedur jetzt nichts mehr, das berhaupt Bezug darauf nimmt, da es
sich bei den Listenelementen um game-Records handelt. Damit kann
das Wort games ganz aus der Prozedurdefinition verschwinden, und es
entsteht eine vielseitig verwendbare Prozedur namens list-filter:

; aus einer Liste eine Liste der Elemente bilden,


; die eine bestimmte Eigenschaft haben
(: list-filter ((%a -> boolean) (list-of %a) -> (list-of %a)))
(define filter
(lambda (p? lis)
(cond
((empty? lis) empty)
((pair? lis)
(if (p? (first lis))
(make-pair (first lis)
(list-filter p? (rest lis)))
(filter p? (rest lis)))))))

Die Entstehung von list-filter aus games-draw und games-playing


ist ein Paradebeispiel fr die Abstraktion mit Hilfe von Mustern. Die
Anwendung dieser Technik bringt eine Reihe von Vorteilen:

Das Programm wird krzer.


Das Programm wird leichter zu lesen.
Wenn die Prozedur korrekt ist, sind auch alle ihre Anwendungen
korrekt.

Damit diese Vorteile zur Geltung kommen, mssen die alten Ab-
straktionsvorlagen gelscht und durch Anwendungen der Abstraktion
ersetzt werden:

; aus einer Spieleliste eine Liste der unentschiedenen Spiele bilden


(: games-draw ((list-of game) -> (list-of game)))
(define games-draw
(lambda (lis)
(list-filter game-draw? lis)))

; aus einer Spieleliste eine Liste der Spiele einer Mannschaft t bilden
(: games-playing (string (list-of game) -> (list-of game)))
(define games-playing
(lambda (t lis)
(list-filter (lambda (g) (plays-game? t g)) lis)))

In der Definition von games-playing bekam die Prozedur list-filter


statt des Namens eines Prdikats direkt eine Prozedurdefinition. Hier
handelt es sich also technisch um eine anonyme Prozedur; dies wird in
Abschnitt 8.3 noch weiter ausgefhrt.

8.2 Listen zusammenfalten

Aus Abschnitt 5.1 ist die Prozedur list-sum bekannt, welche die Summe
einer Liste von Zahlen bildet:
Higher-Order-Programmierung 91

; Liste aufsummieren
(: list-sum ((list-of number) -> number))
(define list-sum
(lambda (lis)
(cond
((empty? lis) 0)
((pair? lis)
(+ (first lis)
(list-sum (rest lis)))))))

Eine eng verwandte Prozedur wrde die Elemente einer Liste nicht
aufsummieren, sondern aufmultiplizieren. Signatur und Schablone sind
identisch zu list-sum:

; Liste aufmultiplizieren
(: list-product ((list-of number) -> number))
(define list-product
(lambda (lis)
(cond
((empty? lis) ...)
((pair? lis)
... (first lis) ...
... (list-product (rest lis)) ...))))

Die erste Ellipse mu das Produkt der leeren Liste sein, also das neu-
trale Element 1 der Multiplikation.2 Aus dem ersten Element und dem
Produkt der Restliste wird das Produkt der Gesamtliste durch Multipli-
kation gebildet.

(define list-product
(lambda (lis)
(cond
((empty? lis) 1)
((pair? lis)
(* (first lis)
(list-product (rest lis)))))))

Die Definitionen von list-sum und list-product unterscheiden sich,


bis auf den Namen, nur an zwei Stellen: beim ersten Zweig, wo das
jeweilige neutrale Element steht, und bei der Prozedur, die benutzt
wird, um das erste Element mit dem Ergebnis des rekursiven Aufrufs
zu kombinieren. Die abstrahierte Prozedur heit list-fold und sieht
folgendermaen aus (die Signatur mu noch einen Moment warten):

; die Elemente einer Liste kombinieren


(define list-fold
(lambda (unit combine lis)
(cond
((empty? lis) unit)
((pair? lis)
(combine (first lis)
(list-fold unit combine (rest lis)))))))

2 0 funktioniert hier nicht es wrde dafr sorgen, da jede Liste 0 als Produkt hat.
92 Kapitel 8

Listen lassen sich damit folgendermaen summieren:


(list-fold 0 + (list 1 2 3 4))
, 10
und so aufmultiplizieren:
(list-fold 1 * (list 1 2 3 4))
, 24
Die Signatur fr list-fold ist nicht auf den ersten Blick ersichtlich. Hier
ein erster Anlauf:
(: list-fold (%a (%a %a -> %a) (list-of %a) -> %a))

Wie sich weiter unten herausstellen wird, kann diese Signatur aber
noch verallgemeinert werden. Erst kommen allerdings noch einige
Erluterungen zur Funktionsweise.
List-fold funktioniert wie folgt: Die Prozedur hat als Parameter eine
Prozedur mit zwei Parametern, einen Wert und eine Liste von Werten.
Es gilt folgende Gleichung:
(list-fold u o #<a1 . . . an >) = (o a1 (o a2 (. . . (o an u). . .)))
Die Funktionsweise von list-fold lt sich daran veranschaulichen,
da sich die ursprngliche Liste auch als
(make-pair a1 (make-pair a2 (. . . (make-pair an empty). . .)))
schreiben lt. Das heit, an die Stelle von make-pair tritt o und an die
Stelle von empty tritt u.
Eine andere, praktische Darstellung von list-fold ist, die Gleichung
mit dem Operator zwischen den Operanden zu schreiben (und nicht
davor), in Infix-Schreibweise also:
(list-fold u ( a1 . . . an )) = a1 ( a2 (. . . ( an u) . . .))
Nach dieser Sichtweise wird zwischen die Elemente der Liste einge-
fgt.
In jedem Fall wird die Liste eingefaltet daher der Name.
Die Definition von concatenate aus Abschnitt 5.5 pat ebenfalls auf
das abstrahierte Muster von list-fold:
(list-fold (list 4 5 6) make-pair (list 1 2 3))
, #<list 1 2 3 4 5 6>
Diese Applikation pat aber nicht mehr auf den obigen Signaturversuch
von list-fold, da make-pair nicht die Signatur
%a %a -> %a

sondern
%a (list-of %a) -> (list-of %a)

und deshalb
%a %b -> %b

besitzt. List-fold hat also folgende Signatur:


(: list-fold (%b (%a %b -> %b) (list-of %a) -> %b)))
Higher-Order-Programmierung 93

8.3 Anonyme Prozeduren

List-fold kann auch benutzt werden, um die Lnge einer Liste auszu-
rechnen. Ganz so einfach wie bei den vorigen Beispielen ist das nicht,
da list-length aus Abschnitt 5.5 nicht direkt dem Muster entspricht:

(define list-length
(lambda (lis)
(cond
((empty? lis) 0)
((pair? lis)
(+ 1
(list-length (rest lis)))))))

Fr das combine-Argument von list-fold wrde hier eine Prozedur


bentigt, die ihr erstes Argument (first lis) ignoriert (es spielt ja fr
die Listenlnge keine Rolle) und auf das zweite Argument eins addiert.
Diese Hilfsprozedur sieht so aus:

(: add-1-for-length (a N -> N))


(define add-1-for-length
(lambda (ignore n)
(+ n 1)))

Damit funktioniert es:

(list-fold 0 add-1-for-length (list 1 2 3 4 5))


, 5

Fr solche Mini-Prozeduren lohnt es sich oft kaum, eine eigene Defi-


nition anzugeben und einen sinnstiftenden Namen zu finden. Das ist
auch nicht notwendig: die rechte Seite der Definition, also der Lambda-
Ausdruck, kann auch direkt eingesetzt werden:

(list-fold 0 (lambda (ignore n) (+ n 1)) (list 1 2 3 4 5))


, 5

Meist tauchen Lambda-Ausdrcke zwar als Teil von Prozedurdefinition


auf, aber es ist natrlich mglich, Prozeduren auerhalb einer Definition
zu verwenden, ohne ihnen einen Namen zu geben. Dafr gab es schon
in Kapitel 1 Beispiele. Mit Hilfe solcher anonymer Prozeduren lt
sich auch list-filter durch list-fold definieren:

(define list-filter
(lambda (p? lis)
(list-fold empty
(lambda (first result)
(if (p? first)
(make-pair first result)
result))
lis)))

Ein weiteres Beispiel die Prozedur every? findet heraus, ob ein ber-
gebenes Prdikat auf alle Elemente einer Liste zutrifft:
94 Kapitel 8

; prfen, ob Prdikat auf alle Elemente einer Liste zutrifft


(: every? ((%a -> boolean) (list-of %a) -> boolean))
(define every?
(lambda (p? lis)
(list-fold #t
(lambda (first result)
(and result
(p? first)))
lis)))

Anders als list-length lassen sich diese Definitionen nicht mit sepa-
raten Hilfsprozeduren schreiben. Fr list-filter wrde ein Versuch
zwar so aussehen:

(define list-filter-helper
(lambda (first result)
(if (p? first)
(make-pair first result)
result)))

In DrRacket erscheint bei dieser Definition eine Fehlermeldung unbound


variable und p? wird rosa markiert. Das liegt daran, da p? weiter
auen im lambda von filter gebunden ist. Dieses p? ist aber nach den
Regeln der lexikalischen Bindung (siehe Abschnitt ??) nur im Rumpf
des ueren lambda in der Definition von filter sichtbar. Darum mu
der Lambda-Ausdruck der Hilfsprozedur ebenfalls in diesem Rumpf
stehen.
FIXME: Hier auch noch map einfhren? Das Unterrichtsbeispiel tat
es. . .

8.4 Prozedurfabriken

Ein ntzlicheres Beispiel fr eine Higher-Order-Funktion in der Ma-


thematik ist die Komposition . Seien f : B C und g : A B
Funktionen. Dann ist f g folgendermaen definiert:

def
( f g)( x ) = f ( g( x ))

lt sich direkt in Scheme bertragen:


; zwei Prozeduren komponieren
(: compose ((%b -> %c) (%a -> %b) -> (%a -> %c)))
(define compose
(lambda (f g)
(lambda (x)
(f (g x)))))

Die beiden Argumente fr f und g mssen Prozeduren mit einem


Parameter sein:

(define add-5
(lambda (x)
(+ x 5)))
(define add-23
Higher-Order-Programmierung 95

(lambda (x)
(+ 23 x)))
(define add-28 (compose add-5 add-23))
(add-28 3)
, 31
((compose (lambda (x) (* x 2)) add-1) 5)
, 12

Compose ist eine Prozedurfabrik sie liefert selbst eine Prozedur zurck,
die abhngig von f und g konstruiert wird.
Compose lt sich benutzen, um eine weitere praktische Higher-Order-
Prozedur namens repeat zu definieren:

; Prozedur wiederholt anwenden


(: repeat (natural (%a -> %a) -> (%a -> %a)))
(define repeat
(lambda (n proc)
(if (= n 0)
(lambda (x) x)
(compose proc (repeat (- n 1) proc)))))

Repeat ist das Pendant zur Potenzierung von Funktionen in der Mathe-
matik, siehe Definition 1.16:

((repeat 5 (lambda (n) (* n 2))) 1)


, 32

8.5 Der Schnfinkel-Isomorphismus

Hier ist eine Prozedurfabrik, die Prozeduren erzeugt, die auf eine Zahl
eine Konstante addieren:

; Prozedur erzeugen, die eine Konstante addiert


(: make-add (number -> (number -> number)))
(define make-add
(lambda (a)
(lambda (b)
(+ a b))))

Angewendet werden kann sie folgendermaen:

(define add-1 (make-add 1))


(add-1 15)
, 16
(define add-7 (make-add 7))
(add-7 15)
, 22

Das geht auch ohne Zwischendefinitionen:

((make-add 7) 15)
, 22
((make-add 13) 42)
, 55
96 Kapitel 8

Make-add ist eine andere Version von +, nmlich eine, die zwei Argumen-
te nicht auf einmal akzeptiert, sondern nacheinander. Summen von
zwei Zahlen, normalerweise geschrieben als (+ a b) lassen sich auch
als ((make-add a) b) schreiben. Diese Transformation von einer Proze-
dur mit zwei Parametern in eine Prozedur mit nur einem Parameter,
die eine Prozedur mit einem weiteren Parameter zurckgibt, die dann
schlielich den Wert liefert, lt sich auch auf andere Prozeduren
anwenden:
; Prozedur erzeugen, die mit einer Konstante multipliziert
(: make-mult (number -> (number -> number)))
(define make-mult
(lambda (a)
(lambda (b)
(* a b))))

; Prozedur erzeugen, die an eine Liste ein Element vorn anhngt


(: make-prepend (a -> (list(a) -> list(a))))
(define make-prepend
(lambda (a)
(lambda (b)
(make-pair a b))))

Erneut folgt eine ganze Familie von Prozeduren einem gemeinsamen


Muster, und erneut lt sich dieses Muster als Prozedur hherer Ord-
nung formulieren. Die Prozedur curry akzeptiert eine Prozedur mit
zwei Parametern und liefert eine entsprechend transformierte Prozedur
zurck:
; Prozedur mit zwei Parametern staffeln
(: curry ((%a %b -> %c) -> (%a -> (%b -> %c))))
(define curry
(lambda (proc)
(lambda (a)
(lambda (b)
(proc a b)))))

Nun lassen sich die make-x-Prozeduren von oben mit Hilfe von curry
definieren:
(define make-add (curry +))
(define make-mult (curry *))
(define make-prepend (curry make-pair))

Die curry-Transformation wurde unabhngig voneinander von den


Mathematikern Moses Schnfinkel und Haskell Curry entdeckt.
Im englischsprachigen Raum heit das Verb dazu darum currify, im
deutschsprachigen Raum schnfinkeln oder curryfizieren.
Die Schnfinkel-Transformation lt sich auch umdrehen:
; Prozedur zu einer Prozedur mit zwei Parametern entstaffeln
(: uncurry ((%a -> (%b -> %c)) -> (%a %b -> %c)))
(define uncurry
(lambda (proc)
(lambda (a b)
((proc a) b))))
Higher-Order-Programmierung 97

Damit ist die Transformation ein Isomorphismus; es gilt folgende Glei-


chung fr Prozeduren p mit zwei Parametern:
(uncurry (curry p)) p

Aufgaben

TBD
TBD
Abbildung 9.1. Teachpack image2.ss

9 Zeitabhngige Modelle

TBD

9.1 Das Teachpack image2.ss

Fr die Grafikprogrammierung mit DrRacket ist es notwendig, ein


sogenanntes Teachpack zu laden ein kleiner Sprachzusatz, in diesem
Fall mit einer Reihe von Prozeduren zur Erzeugung von Bildern. Dazu
mu im Men Sprache (oder Language in der englischen Ausgabe) der
Punkt Teachpack hinzufgen (Add teachpack) angewhlt werden, und
im dann erscheinenden Auswahl-Dialog im Verzeichnis deinprogramm
die Datei image22.ss.
Im Teachpack image2.ss erzeugen verschiedene Prozeduren einfache
Bilder. So hat z.B. die Prozedur rectangle folgende Signatur:

(: rectangle (natural natural mode color -> image))

Dabei sind die ersten beiden Argumente Breite und Hhe eines Recht-
ecks in Pixeln. Das Argument von der Sorte mode ist eine Zeichenkette,
die entweder "solid" oder "outline" sein mu. Sie bestimmt, ob das
Rechteck als durchgngiger Klotz oder nur als Umri gezeichnet wird.
Das Argument von der Sorte color ist eine Zeichenkette, die eine Farbe
(auf Englisch) bezeichnet, zum Beispiel "red", "blue", "yellow", "black",
"white" oder "gray". Als Ergebnis liefert rectangle ein Bild, das von der
DrRacket-REPL entsprechend angezeigt wird wie andere Werte auch.
Es gibt es noch weitere Prozeduren, die geometrische Figuren zeich-
nen:

(: circle (natural mode color -> image))

Die circle-Prozedur liefert einen Kreis, wobei das erste Argument den
Radius angibt. Die mode- und color-Argumente sind wie bei rectangle.

(: ellipse (natural natural mode color -> image))

Diese Prozedur liefert eine Ellipse, wobei das erste Argument die Breite
und das zweite die Hhe angibt.

(: triangle (natural mode color -> image))


100 Kapitel 9

Diese Prozedur liefert ein nach oben zeigendes gleichseitiges Dreieck,


wobei das erste Argument die Seitenlnge angibt.

(: line (natural natural real real real real color -> image))

zeichnet eine Linie. Der Aufruf (line w h x1 y1 x2 y2 c) liefert ein


Bild mit Breite w und Hhe h, in dem eine Linie von ( x1 , y1 ) nach
( x2 , y2 ) luft. Der Ursprung (0, 0) ist links oben, also nicht, wie in der
Mathematik blich, links unten.
Da diese geometrischen Formen fr sich genommen langweilig sind,
knnen mehrere Bilder miteinander kombiniert werden.
Zum Aufeinanderlegen gibt es die Prozedur overlay:

(: overlay (image image h-place v-place -> image))

Dabei sind die ersten beiden Argumente die Bilder, die aufeinanderge-
legt werden das zweite auf das erste. Die beiden anderen Argumente
geben an, wie die beiden Bilder zueinander positioniert werden. Die
Signatur von h-place, das die horizontale Positionierung festlegt, ist:

(define h-place
(signature
(mixed natural
(one-of "left"
"right"
"center"))))

Im ersten Fall, wenn es sich um eine Zahl x handelt, wird das zweite
Bild x Pixel vom linken Rand auf das erste gelegt. Die drei Flle mit
Zeichenketten sagen, da die Bilder am linken Rand bzw. am rechten
Rand bndig plaziert werden, bzw. das zweite Bild horizontal in die
Mitte des ersten gesetzt wird. Dementsprechend ist v-place, das die
vertikale Positionierung festlegt, wie folgt definiert:

(define h-place
(signature
(mixed natural
(one-of "top"
"bottom"
"center"))))

Im ersten Fall, wenn es sich um eine Zahl y handelt, wird das zweite
Bild y Pixel vom oberen Rand auf das erste gelegt. Die drei Flle mit
Zeichenketten sagen, da die Bilder am oberen Rand bzw. am unteren
Rand bndig plaziert werden, bzw. das zweite Bild vertikal in die Mitte
des ersten gesetzt wird.
Das Bild, das bei overlay herauskommt, ist gro genug, da beide
Eingabebilder genau hineinpassen.
Die folgenden Hilfsprozeduren sind Spezialflle von overlay:

(: above (image image h-mode -> image))


(: beside (image image v-mode -> image))

Die Prozedur above ordnet zwei Bilder bereinander an, beside neben-
einenander. Dabei ist h-mode eine der Zeichenketten "left", "right"
Zeitabhngige Modelle 101

TBD
Abbildung 9.2. Eingefgte Bilder in der DrRacket-REPL

und "center", die angibt, ob die Bilder bei above an der linken oder
rechten Kante oder der Mitte ausgerichtet werden. Entsprechend ist
v-mode eine der Zeichenketten "top", "bottom" und "center", die angibt,
ob die Bilder bei beside oben, unten oder an der Mitte ausgerichtet
werden.
Die Prozeduren clip und pad beschneiden bzw. erweitern ein Bild:

(: clip (image natural natural natural natural -> image))


(: pad (image natural natural natural natural -> image))

Ein Aufruf (clip i x y w h) liefert das Teilrechteck des Bildes i mit


Ecke bei ( x, y), Breite w und Hhe h. Der Aufruf (pad i l r t b) fgt
an den Seiten von i noch transparenten Leerraum an: l Pixel links, r
Pixel rechts, t Pixel oben und b Pixel unten.
Abbildung 9.1 zeigt, wie sich die einige der image.ss-Prozeduren in
der DrRacket-REPL verhalten.
Es ist auch mglich, externe Bilder-Dateien in image2.ss-Bilder zu
verwandeln. Dazu dient der Menpunkt Bild einfgen im Spezial-
Men: DrRacket fragt nach dem Namen einer Bilddatei, die dann in den
Programmtext da eingefgt wird, wo der Cursor steht. Die eingefgten
Bilder dienen dann als Literale fr Bild-Objekte. Abbildung 9.2 zeigt
ein Beispiel.
Die folgenden Prozeduren ermitteln Breite und Hhe eines Bildes:

(: image-width (image -> natural))


(: image-height (image -> natural))

9.2 Zwischenergebnisse benennen

Im nchsten Abschnitt geht es um ein etwas umfangreicheres Programm


mit vielen Zwischenergebnissen. Die let-Form erlaubt, Zwischenergebnis-
se zu benennen und beliebig oft zu verwenden. Abbildung 9.3 erlutert
die Funktionsweise. Let ist selbst dann ntzlich, wenn ein Zwischen-
ergebnis nicht mehrfach verwendet wird. Es kann die Lesbarkeit des
Programmtextes erhhen, besonders wenn ein aussagekrftiger Name
verwendet wird. Zum Beispiel berechnet die folgende Prozedur das
Materialvolumen eines Rohrs, von dem Auenradius, Dicke und Hhe
angegeben sind:

; Materialvolumen eines Rohrs berechnen


(: pipe-volume (number number number -> number))
(define pipe-volume
(lambda (outer-radius thickness height)
(let ((inner-radius (- outer-radius thickness)))
(- (cylinder-volume outer-radius height)
(cylinder-volume inner-radius height)))))

In diesem Beispiel wird eine einzelne lokale Variable namens inner-radius


eingefhrt, die fr den Wert von (- outer-radius thickness) steht.
102 Kapitel 9

Let ist fr das Anlegen lokaler Variablen zustndig. Ein let-Ausdruck


hat die folgende allgemeine Form:
(let ((v1 e1 ) . . . (vn en )) b)

Dabei mssen die vi Variablen sein und die ei und b (der Rumpf )
beliebige Ausdrcke. Bei der Auswertung eines solchen let-Ausdrucks
werden zunchst alle ei ausgewertet. Dann werden deren Werte fr die
Variablen vi im Rumpf eingesetzt; dessen Wert wird dann zum Wert
des let-Ausdrucks.
Ein let-Ausdruck hat die gleiche Bedeutung wie folgende Kombination
aus Lambda-Ausdruck und Applikation:
(let ((v1 e1 ) . . . (vn en )) b)
7 ((lambda (v1 . . . vn ) b) e1 . . . en )

Abbildung 9.3. Lokale Variablen mit let

Da die Variablen, die durch let und lambda gebunden werden, nur
jeweils im Rumpf des let bzw. lambda gelten, heien sie auch lokale
Variablen. Die durch define gebundenen Variablen heien dementspre-
chend da sie berall gelten globale Variablen.
Let kann auch mehrere lokale Variablen gleichzeitig einfhren, wie
im folgenden Beispiel:

(let ((a 1)
(b 2)
(c 3))
(list a b c))
, #<list 1 2 3>
Bei der Benutzung von let ist zu beachten, da die Ausdrcke, deren
Werte an die Variablen gebunden werden, allesamt auerhalb des Ein-
zugsbereich des let ausgewertet werden. Folgender Ausdruck fhrt
also bei der Auswertung zu einer Fehlermeldung:

(let ((a 1)
(b (+ a 1)))
b)
, reference to an identifier before its definition: a

Mantra 7 (lokale Variablen) Benenne Zwischenergebnisse mit lokalen


Variablen.

9.3 Modelle und Ansichten

TBD

9.4 Bewegung und Zustand

TBD Dafr ist ein weiteres Teachpack namens universe.ss zustndig.


Es kann in DrRacket genauso wie bei image2.ss geladen werden.
Alle Definitionen von image2.ss sind auch in universe.ss verfgbar.
Zeitabhngige Modelle 103

In der Terminologie von universe.ss ist ein Modell eine world, auf
deutsch eine Welt: Die Idee dahinter ist, da ein Bild eine Ansicht
einer kleinen Welt ist. Damit das funktioniert, mu bei universe.ss eine
erste Welt angemeldet werden, zusammen mit Angaben, wie gro die
Ansicht wird. Dazu gibt es die Prozedur big-bang:

(: big-bang (natural natural number world -> #t))

(Big Bang heit zu deutsch Urknall.) Die ersten beiden Argumente


geben Breite und Hhe der Ansicht an. Das dritte Argument gibt die
Dauer (in Sekunden) zwischen Ticks der Uhr an, die fr die Animation
bentigt wird. Das vierte Argument gibt schlielich die erste Welt an.
(Der Rckgabewert, immer #t, ist ohne Bedeutung.) Fr den Himmel
mit Sonne sieht der Aufruf von big-bang folgendermaen aus:

(big-bang sky-width sky-height 0.1 0)

Dieser Aufruf erzeugt ein Fenster mit Breite und Hhe des Himmels,
startet die Uhr, die jede Sekunde zehnmal tickt, und legt als erste Welt
0, also den Anfang der Zeit fest. (Eine zehntel Sekunde reicht etwa
aus, damit die Animation dem menschlichen Auge als Bewegung
erscheint.)
Damit das Teachpack die Welt in eine Ansicht umwandeln kann, mu
eine entsprechende Ansicht angemeldet werden. Dafr ist die Prozedur
on-redraw zustndig:

(: on-redraw ((world -> image) -> #t))

Als Argument akzeptiert on-redraw also eine Prozedur, die aus einer
Welt ein Bild macht. TBD
Auch diese Prozedur mu noch beim Teachpack angemeldet werden.
Dafr die Teachpack-Prozedur on-tick-event zustndig:

(: on-tick-event ((world -> world) -> #t))

Die on-tick-event-Prozedur akzeptiert eine Prozedur, die bei jedem


Uhren-Tick aufgerufen wird, um aus der alten Welt eine neue zu
machen. Auf diese Beschreibung und auch auf die Signatur pat aber
next-time. Der Aufruf kann also so aussehen:

(on-tick-event next-time)

Wenn das Programm beendet werden soll, mu on-tick-event die Pro-


zedur end-of-time des Teachpacks aufrufen, die folgende Signatur hat:

(: end-of-time (string -> world))

9.5 Andere Welten

Eine kleine (wenn auch nicht besonders sinnvolle) Erweiterung zeigt,


wie die Animation auf Benutzereingaben reagieren kann. Dazu mu sie
noch eine weitere Prozedur anmelden, und zwar mittels on-key-event,
das hnlich funktioniert wie on-tick-event:

(: on-key-event ((world string -> world) -> #t))


104 Kapitel 9

Die Prozedur, die mit on-key-event angemeldet wird, wird immer auf-
gerufen, wenn der Benutzer eine Taste drckt. Welche Taste gedrckt
wurde, gibt das zweite Argument an. Wenn der Benutzer eine regulre
Zeichen-Taste drckt (also keine Cursor-Taste o..), ist dieses Argument
eine Zeichenkette bestehend aus diesem einen Zeichen.
TBD

Aufgaben

TBD

Aufgabe 9.1 Schreiben Sie ein kleines Telespiel Ihrer Wahl.


10 Eigenschaften von Prozeduren

Da 1 + 1 gleich 2 ist, ist ein Beispiel fr die Arbeitsweise der Addition.


Dieses Beispiel knnte auch als Testfall fr eine programmierte Version
der Addition durchgehen. Unter Umstnden kann ein Beispiel, als
Testfall formuliert, einen Fehler in einem Programm finden. Allerdings
ist das Formulieren von Beispielen mhsam. Schlimmer noch, eine
Menge von Testfllen reicht nur selten aus, um die Korrektheit einer
Prozedur auch zu garantieren: Die Testflle decken meist nicht alle
mglichen Anwendungen einer Prozedur ab. Darum ist es oft sinnvoll,
statt isolierter Beispiele allgemeine Eigenschaften zu formulieren und
zu berprfen am besten sogar, diese zu beweisen. Dieses Kapitel
zeigt, wie das geht.

10.1 Eigenschaften von eingebauten Operationen

In diesem Abschnitt wird die Formulierung und berprfung von


Eigenschaften anhand von bekannten eingebauten Operationen wie +,
and, = etc. demonstriert.

10.1.1 Binre Operationen

Eine allgemein bekannte Eigenschaft der Addition ist die Kommutativi-


tt:
a+b = b+a
Auch wenn intuitiv die Bedeutung klar ist, ist die Eigenschaft genau
genommen so noch nicht przise schriftlich festgehalten, da nicht notiert
ist, was a und b sind: Die Idee ist natrlich, da a und b beliebige Zahlen
sind. Im allgemeinen also:
a C, b C : a + b = b + a
(Wer sich mit der Vorstellung komplexer Zahlen nicht wohlfhlt, kann
das C auch durch R oder Q ersetzen.)
In Scheme lt sich diese Eigenschaft fr die eingebaute Prozedur +
aufschreiben das ist auf Tastaturen nicht vertreten und wird darum
ausgeschrieben (siehe Abbildung 10.1):
(for-all ((a number)
(b number))
(= (+ a b) (+ b a)))

Das Ergebnis dieses Ausdrucks wird in der REPL etwas undurchsichtig


angezeigt:
106 Kapitel 10

For-all ermglicht das Formulieren von Eigenschaften. Ein for-all-


Ausdruck hat die folgende allgemeine Form:
(for-all ((v1 c1 ) . . . (vn cn )) b)

Dabei mssen die vi Variablen sein, die ci Signaturen und b (der Rumpf)
ein Ausdruck, der entweder einen booleschen Wert oder eine Eigen-
schaft liefert. Der for-all-Ausdruck hat als Wert eine Eigenschaft, die
besagt, da b gilt fr alle Werte der vi , welche die Signaturen ci erfllen.

Abbildung 10.1. for-all

, #<:property>

Check-property testet eine Eigenschaft analog zu check-expect. Eine


check-property-Form sieht so aus:
(check-property e)
e ist ein Ausdruck, der eine Eigenschaft liefern mu in der Regel also
ein for-all-Ausdruck.
Bei der Auswertung setzt check-property fr die Variablen der for-all-
Ausdrcke verschiedene Werte ein und testet, ob die Eigenschaft jeweils
erfllt ist.
Check-property funktioniert nur fr Eigenschaften, bei denen aus
den Signaturen sinnvoll Werte generiert werden knnen. Dies ist
fr die meisten eingebauten Signaturen der Fall, aber nicht fr Si-
gnaturvariablen und Signaturen, die mit predicate, property oder
define-record-procedures definiert wurden.

Abbildung 10.2. check-property

Bessere Informationen lassen sich erzielen, wenn for-all-Ausdrcke in


eine check-property-Form (siehe Abbildung 10.2) eingebettet werden:

(check-property
(for-all ((a number)
(b number))
(= (+ a b) (+ b a))))

Check-property fungiert, wie check-expect oder check-within, als Test-


fall und wird auch als solcher ausgewertet. Da + tatschlich kommutativ
ist, luft der Testfall auch anstandslos durch.
Interessanter wird es erst bei Eigenschaften, die nicht stimmen. Zum
Beispiel ist die Subtraktion - nicht kommutativ:

(check-property
(for-all ((a number)
(b number))
(= (- a b)
(- b a))))
Eigenschaften von Prozeduren 107

Hierfr liefert DrRacket folgende Meldung:

Eigenschaft falsifizierbar mit a = 0.0 b = -1.5

Falsifizierbar bedeutet, da es ein Gegenbeispiel fr die Eigenschaft gibt,


also Werte fr die Variablen a und b, welche die Eigenschaft falsch
werden lassen. DrRacket hat in diesem Fall ein Gegenbeispiel gefunden,
bei dem a den Wert 0.0 und b den Wert 1.5 hat:
(- 0.0 -1.5)
, 1.5
(- -1.5 0.0)
, -1.5

Dieses Beispiel widerlegt also tatschlich die Behauptung der Eigen-


schaft.
Hinter den Kulissen hat DrRacket verschiedene Werte fr a und
b durchprobiert und in die Eigenschaft eingesetzt, also effektiv nach
einem Gegenbeispiel gesucht. Fr die Kommutativitt von + gibt es
kein Gegenbeispiel DrRacket konnte also auch keins finden. Da
ausgerechnet das merkwrdige Beispiel 0.0 und 1.5 herauskam, liegt
an der relativ komplexen Suchstrategie von DrRacket.
Auf diese Art und Weise lassen sich eine Reihe von interessanten
Eigenschaften formulieren, so zum Beispiel die Assoziativitt von +:
(check-property
(for-all ((a number)
(b number)
(c number))
(= (+ a (+ b c))
(+ (+ a b) c))))

Hierbei gibt es allerdings eine bse berraschung DrRacket produziert


ein Gegenbeispiel:
Eigenschaft falsifizierbar mit
a = 2.6666666666666665 b = 6.857142857142857 c = -6.857142857142857

Es ist kein Zufall, da es sich um Zahlen mit vielen Nachkommastellen


handelt. Wenn dieses Gegenbeispiel in die Eigenschaft eingesetzt wird,
liefert die REPL folgende Ergebnisse:
(+ 2.6666666666666665 (+ 6.857142857142857 -6.857142857142857))
, 2.6666666666666665
(+ (+ 2.6666666666666665 6.857142857142857) -6.857142857142857)
, 2.666666666666667
Hier wird sichtbar da, wie bereits in Abschnitt 1.2 erwhnt, bei Berech-
nungen mit sogenannten inexakten Zahlen, das sind Zahlen mit einem
Dezimalpunkt, die mathematischen Operationen nur mit einer begrenz-
ten Anzahl von Stellen durchgefhrt werden und dann runden da
auch noch binr und nicht dezimal gerundet wird, sieht das Ergebnis
dieser Rundung oft unintuitiv aus. Dieses Beispiel zeigt nun, da Addi-
tion plus binre Rundung nicht assoziativ ist. Die Assoziativitt gilt nur
fr das Rechnen mit exakten Zahlen. Immerhin sind alle Zahlen mit der
Signatur rational exakt, die Eigenschaft lt sich also reformulieren:
108 Kapitel 10

(check-property
(for-all ((a rational)
(b rational)
(c rational))
(= (+ a (+ b c))
(+ (+ a b) c))))

Und tatschlich, in dieser Form wird die Eigenschaft nicht beanstandet.


Kommutativitt und Assoziativitt sind jeweils Eigenschaften einer
einzelnen Operation, in diesem Fall +. Manche Eigenschaften beschrei-
ben auch das Zusammenspiel mehrerer Operationen, wie zum Beispiel
die Distributivitt, die fr Addition und Multiplikation gilt:

a C, b C, c C : a (b + c) = a b + b c

Auch dies lt sich direkt nach Scheme bersetzen, diesmal gleich mit
rational statt number:

(check-property
(for-all ((a rational)
(b rational)
(c rational))
(= (* a (+ b c))
(+ (* a b) (* a c)))))

Auch hier hat DrRacket nichts zu meckern.


Neben der Addition ist auch die Multiplikation kommutativ:

(check-property
(for-all ((a rational)
(b rational))
(= (* a b)
(* b a))))

Wenn Sie diese Eigenschaft neben die Kommutativitt fr + legen, sehen


Sie, da diese fast identisch sind und damit natrliche Kandidaten fr
Abstraktion: Nur die Operation * im einen und + im anderen Fall ist
unterschiedlich. Wenn wir ber die Operation abstrahieren, bekommen
wir so etwas wie eine allgemeine Definition der Kommutativitt, und
das sieht so aus:

(define commutativity
(lambda (op)
(for-all ((a rational)
(b rational))
(= (op a b)
(op b a)))))

Mit Hilfe dieser Definition knnen wir die Kommutativitt von + und *
deutlich kompakter formulieren:

(check-property (commutativity *))


(check-property (commutativity +))
Eigenschaften von Prozeduren 109

ber dem check-property knnen wir nicht abstrahieren es mu ganz


auen stehen, damit DrRacket Fehlermeldungen den dazu passenden
Programmstellen zuordnen kann.
Der Vollstndigkeit halber braucht commutativity noch eine Signatur:
+ und * sind jeweils Prozeduren, die zwei Zahlen akzeptieren und
wieder eine Zahl zurckliefern. Der Rckgabewert von commutativity
ist eine Eigenschaft, fr die in DrRacket die Signatur property fest
eingebaut ist. Die fertige Signatur ist also diese hier:

(: commutativity ((rational rational -> rational) -> property))

Diese drei Eigenschaften Kommutativitt, Assoziativitt und Distri-


butivitt tauchen immer wieder auf, da sie nicht nur fr arithmetische
Operationen gelten (auch die Multiplikation ist kommutativ und asso-
ziativ) sondern auch anderswo.
Zum Beispiel gelten Kommutativitt und Assoziativitt auch fr das
logische and:

(check-property
(for-all ((a boolean)
(b boolean))
(boolean=? (and a b)
(and b a))))

(check-property
(for-all ((a boolean)
(b boolean)
(c boolean))
(boolean=? (and a (and b c))
(and (and a b) c))))

Hier mu die eingebaute Prozedur boolean=? verwendet werden, die


boolesche Werte vergleicht, analog zu =, die nur Zahlen vergleichen
kann.
Schn wre natrlich, wenn wir auch fr die Kommutativitt von
and die obige Prozedur commutativity verwenden knnten: Das Pro-
blem ist aber, da sich die Kommutativitt von and an zwei weiteren
Stellen von der Kommutativitt fr * und + unterscheidet, nmlich
bei der Signatur (boolean statt rational) und auch bei der Vergleichs-
operation (boolean=? statt =). Um auch and in den Einzugsbereich von
commutativity zu holen, mssen wir also auch noch ber diese beiden
Werte abstrahieren:

(define commutativity
(lambda (op sig =?)
(for-all ((a sig)
(b sig))
(=? (op a b)
(op b a)))))

Fr * und + mssen wir commutativity nun wie folgt aufrufen:

(check-property (commutativity * (signature rational) =))


(check-property (commutativity + (signature rational) =))
110 Kapitel 10

Denken Sie an das signature, das immer notwendig ist, wenn eine
Signatur auerhalb einer Signaturdeklaration mit : sowie einem for-all
vorkommt.
Um commutativity auch auf and und or loszulassen, gibt es allerdings
noch ein weiteres Hindernis: Das Argument zu op mu eine Prozedur
sein and und or sind aber Spezialformen. Wir knnen Sie aber zu
Prozeduren machen, indem wir lambdas darumwickeln:

(check-property (commutativity (lambda (a b) (and a b))


(signature boolean) boolean=?))
(check-property (commutativity (lambda (a b) (or a b))
(signature boolean)
boolean=?))

Bei der neuen Version von commutativity fehlt noch die Signatur. Wir
mssen dazu die ursprngliche Signatur

(: commutativity ((rational rational -> rational) -> property))

ziemlich radikal renovieren: Das erste Argument ist zwar immer noch
eine zweistellige Prozedur, aber nicht mehr notwendigerweise auf ratio-
nalen Zahlen. Wir skizzieren erstmal, was wir wissen:

(: commutativity ((? ? -> ?) signature (? ? -> boolean) -> property))

Die eingebaute Signatur signature ist fr Signaturen zustndig das


zweite Argument ist ja eine Signatur. Von der Vergleichsprozedur an
dritter Stelle ist klar, da sie ein boolean liefert. Fr die restlichen Frage-
zeichen ist die genaue Signatur abhngig vom konkreten Operator und
dieser (ebenfalls variablen) Signatur, wir mssen also Signaturvariablen
verwenden.
Was ist noch bekannt? Die beiden Argumente der Prozedur op mssen
auf dieselbe Signatur passen, da sie ja vertauschbar sind:

(: commutativity ((%a %a -> ?) signature (? ? -> boolean) -> property))

Auerdem wird der Rckgabewert von op in die Vergleichsprozedur


gefttert, fr die restliche drei Fragezeichen mssen wir also dieselbe
Signatur einsetzen. Ist erforderlich, da der Rckgabewert von op auf
die gleiche Signatur pat wie die Argumente? Der Rckgabewert wird
nicht wieder in op hineingefttert, die Antwort ist also nein. Wir knnen
also eine von %a verschiedene Signaturvariable benutzen:

(: commutativity ((%a %a -> %b) signature (%b %b -> boolean) -> property))

Genauso wie bei der Kommutativitt knnen wir auch bei der Assozia-
tivitt abstrahieren. Hier die Abstraktion, die dabei herauskommt:

(define associativity
(lambda (op sig =?)
(for-all ((a sig)
(b sig)
(c sig))
(=? (op a (op b c))
(op (op a b) c)))))
Eigenschaften von Prozeduren 111

Benutzen knnen wir Sie hnlich wie bei der Kommutativitt:

(check-property (associativity + (signature rational) =))


(check-property (associativity * (signature rational) =))
(check-property (associativity (lambda (a b) (and a b))
(signature boolean)
boolean=?))
(check-property (associativity (lambda (a b) (or a b))
(signature boolean)
boolean=?))

Auch hier die Formulierung der Signatur nicht so einfach. Die erste
Skizze knnte so aussehen:

(: associativity ((? ? -> ?) signature (? ? -> boolean) -> property))

Wie bei commutativity wird der Rckgabewert von op als Argument


fr die Vergleichsprozedur verwendet: Die letzten drei Fragezeichen
mssen also wieder gleich sein. Anders als bei der Kommutativitt
wird der Rckgabewert von op auch wieder als Argument in op her-
eingefttert. Damit mssen auch die ersten beiden Fragezeichen den
anderen entsprechen. Die beste Signatur ist also wie folgt:

(: associativity ((%a %a -> %a) signature (%a %a -> boolean) -> property))

And und or erfllen auch zwei Distributivgesetze. Damit beschftigt sich


Aufgabe 10.3.
Auch das DeMorgansche Gesetz (siehe Abschnitt 1.1) lt sich in
Scheme formulieren:

(check-property
(for-all ((a boolean)
(b boolean))
(boolean=? (not (and a b))
(or (not a) (not b)))))

Bei vielen Operationen ist auerdem interessant, ob sie ein neutrales


Element besitzen, also ein Argument, das dafr sorgt, da die Operation
ein anderes Argument unverndert zurckgibt. Die Addition hat z.B.
die 0 als neutrales Element:

(check-property
(for-all ((a rational))
(= (+ a 0) a)))

Streng genommen ist damit nur gesichert, da 0 rechtsneutrales Element


ist, also von rechts addiert das andere Argument unverndert heraus-
kommt. Aus der Kommutativitt folgt aber, da jedes rechtsneutrale
Element auch ein linksneutrales Element ist.
Bei manchen Operationen gibt es neben dem neutralen Element zu
jedem Element auch ein inverses Element: Wenn eine binre Operation
auf ein Element und sein Inverses angewendet wird, so mu das neu-
trale Element herauskommen. Bei der Addition entsteht das Inverse zu
einer Zahl durch Umdrehen des Vorzeichens:
112 Kapitel 10

(check-property
(for-all ((a rational))
(= (+ a (- a)) 0)))

(check-property
(for-all ((a rational))
(= (+ (- a) a) 0)))

Hier noch einmal eine Zusammenfassung der in diesem Abschnitt


behandelten Eigenschaften, mit Kurzfassungen der mathematischen
Formulierungen:

Mantra 8 (Eigenschaften von binren Operationen) Folgende Eigenschaf-


ten sind prinzipiell auf alle binren Operationen denkbar, die zwei Ele-
mente einer Menge M akzeptieren und wiederum ein Element von M
zurckgeben.

Kommutativitt a ? b = b ? a
Assoziativitt ( a ? b) ? c = a ? (b ? c)
Distributivitt a (b ? c) = ( a b) ? ( a c); (b ? c) a = (b a) ?
(c a)
neutrales Element (a ? = a; ? a = a)
inverses Element a ? a1 = ; a1 ? a =

10.1.2 Eigenschaften von binren Prdikaten

Die Prozedur = pat nicht in das Scheme der Eigenschaften des folgen-
den Abschnitts. Sie hat folgende Signatur:

(: = (number number -> boolean))

Damit akzeptiert sie zwar zwei Argumente aus derselben Menge, liefert
aber einen booleschen Wert zurck. Stattdessen handelt es sich um ein
binres Prdikat bzw. eine binre Relation. Fr binre Relationen kommt
ein anderer Satz von Eigenschaften in Frage. (Die mathematische Seite
ist in Anhang 1.5 beschrieben.) Insbesondere ist = eine quivalenzrelation
und damit reflexiv, symmetrisch und transitiv.
Die Reflexivitt besagt, da jedes Element der Grundmenge (in die-
sem Fall die Menge der Zahlen) zu sich selbst in Beziehung steht:

(check-property
(for-all ((a number))
(= a a)))

Die Symmetrie bedeutet fr =, da aus (= a b) , #t das Spiegelbild


(= b a), #t folgt. Mathematisch geschrieben she das so aus:

a C, b C : a = b b = a

Der Implikationspfeil wird in Scheme ==> geschrieben. (Siehe


Abbildung 10.3.) Der Test der Symmetrie sieht also folgendermaen
aus:
Eigenschaften von Prozeduren 113

Eine Implikation in einer Eigenschaft wird folgendermaen geschrieben:


(==> e ep)

Dabei mu e ein Ausdruck mit booleschem Wert sein (die Voraussetzung)


und e p eine Eigenschaft oder ein boolescher Ausdruck. Die Implikation
liefert ihrerseits wieder eine Eigenschaft, die gilt, wenn e p immer dann
gilt, wenn die Voraussetzung erfllt ist, also #t liefert.

Abbildung 10.3. ==>

(check-property
(for-all ((a number)
(b number))
(==> (= a b)
(= b a))))

hnlich luft es mit der Transitivitt: Wenn zwei Zahlen a und b gleich
sind sowie b und eine dritte Zahl c, dann mssen auch a und c gleich
sein:
(check-property
(for-all ((a number)
(b number)
(c number))
(==> (and (= a b) (= b c))
(= a c))))

Neben den drei Eigenschaften von quivalenzrelationen tritt auch


gelegentlich die Eigenschaft Antisymmetrie auf (die mathematische Defi-
nition steht in Anhang 1.5).

Mantra 9 (Eigenschaften von binren Prdikaten) Folgende Eigenschaf-


ten sind fr binre Prdikate denkbar:
Reflexivitt a ! a
Symmetrie a ! b b ! a
Transitivitt a ! b b ! c a ! c
Antisymmetrie a ! b b ! a a = b

10.2 Eigenschaften von Prozeduren auf Listen

Es wird Zeit, Eigenschaften von selbstgeschriebenen Prozeduren zu


berprfen. In diesem Abschnitt geht es um einige der Prozeduren, die
auf Listen operieren: concatenate, invert, und list-sum.

10.2.1 Eigenschaften von concatenate

Die Prozedur concatenate aus Abschnitt 5.6.2 hngt zwei Listen anein-
ander. Auch concatenate ist assoziativ: Wenn drei Listen mit Hilfe von
concatenate aneinandergehngt werden, spielt es keine Rolle, ob zuerst
die ersten beiden oder zuerst die letzten beiden Listen aneinanderge-
hngt werden. Nach dem Muster der Assoziativitt von + und and sieht
der Test dieser Eigenschaft folgendermaen aus:
114 Kapitel 10

(check-property
(associativity concatenate (signature (list-of number)) ...))

Beim Test ist die Signatur von lis-1, lis-2 und lis-3 jeweils (list-of
number). Die Signatur von concatenate

(: concatenate ((list-of %a) (list-of %a) -> (list-of %a)))

suggeriert allerdings, da die Signatur von lis-1, lis-2 und lis-3 je-
weils (list-of %a) lauten sollte, also allgemeiner als (list-of number).
Signaturen mit Signaturvariablen funktionieren allerdings nicht im
Zusammenhang mit Eigenschaften, wie folgendes Beispiel zeigt:

(check-property
(for-all ((x %a))
...))

Dieser Code liefert die Fehlermeldung Signatur hat keinen Generator:


Das liegt daran, da die Signaturvariable %a zuwenig Information ber
die zugrundeliegenden Werte liefert, als da DrRacket sinnvoll Werte
fr die Tests generieren knnte. Aus diesem Grund mssen in for-all
immer konkrete Signaturen ohne Signaturvariablen angegeben wer-
den. (Aus hnlichen Grnden funktionieren auch einige andere Arten
von Signaturen nicht bei for-all, inbesondere Record-Signaturen. Pro-
zedursignaturen sind allerdings zulssig und werden in Abschnitt 10.3
behandelt.)
Fr concatenate wre es zwar grndlicher, die Tests auch noch fr
andere Sorten von Listenelementen als number durchzufhren da
aber concatenate mit den Listenelementen nichts anfngt, auer sie in
weitere Liste zu stecken, reicht die Formulierung der Eigenschaft mit
(list-of number) aus.
Es bleibt noch ein weiteres Problem bei der Formulierung der Assozia-
tivitt fr concatenate: Es steht noch keine Prozedur fr den Vergleich
der beiden Listen zur Verfgung, die mu erst noch geschrieben werden.
Kurzbeschreibung und Signatur:

; Zwei Listen aus Zahlen vergleichen


(: number-list=? ((list-of number) (list-of number) -> boolean))

Die Testflle sollten insbesondere Listen unterschiedlicher Lnge be-


rcksichtigen:

(check-expect (number-list=? empty empty) #t)


(check-expect (number-list=? (list 1.0 2.0 3.0) (list 1.0 2.0 3.0)) #t)
(check-expect (number-list=? (list 1.0 2.0 3.0) (list 1.0 2.0)) #f)
(check-expect (number-list=? (list 1.0 2.0) (list 1.0 2.0 3.0)) #f)
(check-expect (number-list=? (list 1.0 2.0 3.0) (list 1.0 2.1 3.0)) #f)

Die erste Schablone, ausgewhlt nach dem ersten Listenparameter lis-1,


sieht so aus:

(define number-list=?
(lambda (lis-1 lis-2)
(cond
((empty? lis-1)
Eigenschaften von Prozeduren 115

...)
((pair? lis-1)
... (first lis-1) ...
... (number-list=? (rest lis-1) ...) ...))))

Die Schablone fr den zweiten Listenparameter lis-2 wird in beide


Zweige des cond eingesetzt:
(define number-list=?
(lambda (lis-1 lis-2)
(cond
((empty? lis-1)
(cond
((empty? lis-2) ...)
((pair? lis-2)
... (first lis-2) ...
... (number-list=? ... (rest lis-2)))))
((pair? lis-1)
... (first lis-1) ...
... (number-list=? (rest lis-1) ...) ...
(cond
((empty? lis-2) ...)
((pair? lis-2)
... (first lis-2) ...
... (number-list=? ... (rest lis-2))))))))

Es gibt also insgesamt vier Flle bei den Verzweigungen:


Im ersten Fall sind beide Listen leer, das Ergebnis ist also #t.
Im zweiten Fall ist die erste Liste leer und die zweite nichtleer. Das
Ergebnis ist also #f und die Schablonenelemente sind berflssig.
Im dritten Fall ist die erste Liste nichtleer und die zweite leer. Das
Ergebnis ist also wiederum #f.
Im vierten Fall sind beide Listen nichtleer und in der Schablone
stehen die jeweils ersten Elemente von lis-1 und lis-2. Die beiden
Listen sind nur gleich, wenn die beiden ersten Elemente gleich sind.
Auerdem mssen natrlich die beiden Reste der Listen ebenfalls
gleich sind die beiden rekursiven Aufrufe aus den Schablonen
knnen also kombiniert werden:

(define number-list=?
(lambda (lis-1 lis-2)
(cond
((empty? lis-1)
(cond
((empty? lis-2) #t)
((pair? lis-2) #f)))
((pair? lis-1)
(cond
((empty? lis-2) #f)
((pair? lis-2)
(and (= (first lis-1) (first lis-2))
(number-list=? (rest lis-1) (rest lis-2)))))))))
116 Kapitel 10

Damit kann jetzt die Assoziativitt von concatenate getestet werden:


(check-property
(associativity concatenate (signature (list-of number)) number-list=?))

Concatenate hat auerdem ein neutrales Element, und zwar sowohl im


linken als auch im rechten Argument:
(check-property
(for-all ((lis (list-of number)))
(number-list=? lis (concatenate empty lis))))

(check-property
(for-all ((lis (list-of number)))
(number-list=? lis (concatenate lis empty))))

Concatenateist allerdings demonstrierbar nicht kommutativ. Der ent-


sprechende Test sieht so aus:
(check-property
(commutativity concatenate (signature (list-of number)) number-list=?))

DrRacket liefert hierfr ein Gegenbeispiel:


Eigenschaft falsifizierbar mit
lis-1 = #<list -3.75> lis-2 = #<list 1.5 1.5>

10.2.2 Eigenschaften von number-list=?

Wie der Zufall so will, hat auch die Hilfsprozedur number-list=? in-
teressante Eigenschaften: Wie = mu auch number-list=? eine qui-
valenzrelation sein schlielich testet sie wie = auf Gleichheit. Die
dazugehrigen Eigenschaften Reflexivitt, Symmetrie und Transitivi-
tt knnen ebenso wie bei = formuliert werden:
Reflexivitt:
(check-property
(for-all ((lis (list-of number)))
(number-list=? lis lis)))

Symmetrie:
(check-property
(for-all ((lis-1 (list-of number))
(lis-2 (list-of number)))
(==> (number-list=? lis-1 lis-2)
(number-list=? lis-2 lis-1))))

Transitivitt
(check-property
(for-all ((lis-1 (list-of number))
(lis-2 (list-of number))
(lis-3 (list-of number)))
(==> (and (number-list=? lis-1 lis-2)
(number-list=? lis-2 lis-3))
(number-list=? lis-1 lis-3))))
Eigenschaften von Prozeduren 117

10.2.3 Eigenschaften von invert

Die Prozedur invert aus Abschnitt 12.1 dreht die Reihenfolge der
Elemente einer Liste um. Eine naheliegende Eigenschaft von invert ist,
da zweimaliges Umdrehen wieder die Ursprungsliste liefern sollte:
(check-property
(for-all ((lis (list-of number)))
(number-list=? lis (invert (invert lis)))))

Auch bei invert enthlt die Signatur eine Signaturvariable:


(: invert ((list-of %a) -> (list-of %a)))

Genau wie bei concatenate macht invert mit den Listenelementen


nichts spezielles, es knnen also auch zum Beispiel Zeichenketten be-
nutzt werden. Diese nderung allein funktioniert allerdings nicht:
(check-property
(for-all ((lis (list-of string)))
(number-list=? lis (invert (invert lis)))))

Die Prozedur number-list=? funktioniert nur auf Listen von Zahlen.


Es wre mglich, number-list=? ber der Vergleichsprozedur auf den
Elementen zu abstrahieren, aber es wre trotzdem umstndliche Arbeit
nur fr den Zweck des Testens. Deshalb gibt es eine Vereinfachung
analog zu check-expect. Die eingebaute Form expect akzeptiert zwei
beliebige Werte und ist dann erfllt, wenn diese Werte gleich sind.
(Siehe Abbildung 10.4.) Die Eigenschaft von invert sieht damit so aus:

Expect liefert eine Eigenschaft analog zur Funktionsweise von


check-expect. Ein expect-Ausdruck hat folgende Form:

(expect e1 e2 )
e1 und e2 sind Ausdrcke. Die resultierende Eigenschaft ist erfllt, wenn
e1 und e2 den gleichen Wert liefern der Vergleich wird dabei wie bei
check-expect angestellt.

Abbildung 10.4. expect

(check-property
(for-all ((lis (list-of string)))
(expect lis (invert (invert lis)))))

Viele Prozeduren auf Listen haben Eigenschaften, welche die Prozedur


jeweils im Zusammenspiel mit einer oder mehreren anderen Proze-
duren zeigen. Bei Prozeduren mit Listen ist es hufig interessant, das
Zusammenspiel mit concatenate zu betrachten. Damit concatenate et-
was sinnvolles tun kann, sind zwei Listen notwendig:
(check-property
(for-all ((lis-1 (list-of number))
(lis-2 (list-of number)))
...))
118 Kapitel 10

Auf diese zwei Listen kann concatenate aber auch jeweils invert ange-
wendet werden:
(check-property
(for-all ((lis-1 (list-of number))
(lis-2 (list-of number)))
... (invert lis-1) ...
... (invert lis-2) ...
... (invert (concatenate lis-1 lis-2)) ...))

Wie lt sich die Liste (invert (concatenate lis-1 lis-2)) noch be-
schreiben? Angenommen, lis-1 ist die Liste #<list 1 2 3> und lis-2
die Liste #<list 4 5 6>. Dann gilt:
(invert (concatenate lis-1 lis-2))
=
(invert (concatenate #<list 1 2 3> #<list 4 5 6>))
= ... = (invert #<list 1 2 3 4 5 6>))| {z } | {z }
lis-1 lis-2

= ... = #<list 6 5 4
| {z } 3 2 1
| {z } >
(invert lis-2) (invert lis-1)

Dies lt vermuten, da die gesuchte Eigenschaft folgendermaen


aussieht:
(check-property
(for-all ((lis-1 (list-of number))
(lis-2 (list-of number)))
(expect (invert (concatenate lis-1 lis-2))
(concatenate (invert lis-2) (invert lis-1)))))

Mantra 10 (Eigenschaften von Prozeduren auf Listen) Prozeduren, die


Listen akzeptieren, haben hufig interessante Eigenschaften im Zusam-
menspiel mit concatenate.

10.2.4 Eigenschaften von list-sum

List-sum aus Abschnitt 5.2 ist, wie invert, eine Prozedur, die eine Liste
akzeptiert. Genau wie bei invert ist es eine gute Idee, die Interaktion
zwischen list-sum und concatenate zu untersuchen. Es mssen also
wieder zwei Listen her die zu invert analoge Vorgehensweise liefert
folgende Schablone:
(check-property
(for-all ((lis-1 (list-of number))
(lis-2 (list-of number)))
... (list-sum lis-1) ...
... (list-sum lis-2) ...
... (list-sum (concatenate lis-1 lis-2)) ...

Da list-sum die Elemente der Liste addiert und die Addition assoziativ
ist, mte folgendes gelten:
(check-property
(for-all ((lis-1 (list-of number))
Eigenschaften von Prozeduren 119

(lis-2 (list-of number)))


(expect (+ (list-sum lis-1) (list-sum lis-2))
(list-sum (concatenate lis-1 lis-2)))))

Hier allerdings schlgt das Rundungsproblem aus Abschnitt 10.1.1 wie-


der zu: Die Addition auf number ist eben nicht assoziativ, aber immerhin
auf rational. Der fertige Test mu also so aussehen:

(check-property
(for-all ((lis-1 (list-of rational))
(lis-2 (list-of rational)))
(expect (+ (list-sum lis-1) (list-sum lis-2))
(list-sum (concatenate lis-1 lis-2)))))

Eine Alternative ist die Verwendung der Form expect-within, die eine
Eigenschaft analog zu check-within erzeugt. (Siehe Abbildung 10.5.)

Expect-within liefert eine Eigenschaft analog zur Funktionsweise von


check-within. Ein expect-within-Ausdruck hat folgende Form:
(expect-within e1 e2 e )
e1 , e2 und e sind Ausdrcke, wobei e eine reelle Zahl liefern mu. Die
resultierende Eigenschaft ist erfllt, wenn e1 und e2 den gleichen Wert
liefern der Vergleich wird dabei wie bei check-within angestellt, d.h.
alle inexakten Zahlen in den Ergebnissen von e1 und e2 mssen nicht
gleich sein, drfen sich aber hchstens um e voneinander unterschei-
den.

Abbildung 10.5. expect-within

Mit expect-within sieht der Testfall folgendermaen aus:

(check-property
(for-all ((lis-1 (list-of number))
(lis-2 (list-of number)))
(expect-within (+ (list-sum lis-1) (list-sum lis-2))
(list-sum (concatenate lis-1 lis-2))
0.1)))

Auch dieser Testfall luft durch.


Auch der Test fr die Assoziativitt von + aus Abschnitt 10.1.1 kann
mit expect-within formuliert werden:

(check-property
(for-all ((a number)
(b number)
(c number))
(expect-within (+ a (+ b c))
(+ (+ a b) c)
0.1)))

So wie sich die Assoziativitt von + in einer Eigenschaft von list-sum


niederschlgt, tut dies auch die Kommutativitt: Sie besagt, da die
120 Kapitel 10

Reihenfolge der Elemente der Liste keine Rolle spielt. Eine einfache
Mglichkeit, dies zu testen, ist wiederum mit zwei Listen zu arbeiten
und diese einmal in der einen und dann in der anderen Richtung
aneinanderzuhngen:
(check-property
(for-all ((lis-1 (list-of rational))
(lis-2 (list-of rational)))
(expect (list-sum (concatenate lis-1 lis-2))
(list-sum (concatenate lis-2 lis-1)))))

10.3 Eigenschaften von Prozeduren hherer Ordnung

In Abschnitt 8.5 wurde bereits eine Eigenschaft von curry und uncurry
aufgefhrt:
(uncurry (curry p)) p
Mit anderen Worten: curry und uncurry sind jeweils Inverse voneinan-
der. Auch diese Eigenschaft lt sich direkt mit check-property und
for-all formulieren. Zu beachten ist wieder, obwohl curry und uncurry
polymorphe Signaturen mit Signaturvariablen haben, da fr den Test
mit check-property eine konkrete Signatur ohne Signaturvariablen
fr das Prozedur-Argument benutzt werden mu, also zum Beispiel
string:

(check-property
(for-all ((proc (string string -> string)))
(expect (curry (uncurry proc))
proc)))

Leider schlgt dieser Test fehl, und zwar mit einer mysterisen Mel-
dung:

Eigenschaft falsifizierbar mit #<procedure:?>

Offenbar ist DrRacket also der Ansicht, es hat eine Prozedur gefun-
den, welche die Eigenschaft nicht erfllt, kann aber nicht genau sagen,
welche Prozedur: Das liegt daran, da es prinzipiell unmglich ist,
zwei Prozeduren auf Gleichheit zu berprfen Gleichheit zweier Pro-
zeduren heit ja, da die eine Prozedur angewendet auf einen Satz
Argumente immer das gleiche Ergebnis wie die andere Prozedur liefert.
Im obigen Beispiel akzeptiert proc zwei Zeichenketten, von denen es un-
endlich viele gibt; die Gleichheit zu berprfen, wrde also unendlich
lange dauern. Expect versucht es darum gar nicht erst, sondern sieht es
als notwendige (nicht als hinreichende) Bedingung fr die Gleichheit
zweier Prozeduren an, da sie Wert desselben lambda-Ausdrucks sind.
Expect testet also bei Prozeduren auf sogenannte intensionale Gleichheit,
was soviel heit, da expect vergleicht, auf welche Weise die beiden Pro-
zeduren entstanden sind, nicht aber, ob sich die beiden Prozeduren
gleich verhalten. Die letztere Eigenschaft heit extensionale Gleichheit
und ist, wie gesagt, nicht effektiv testbar.
Der lambda-Ausdruck der Prozedur, die von (curry (uncurry proc))
geliefert wird, ist aber der Rumpf von curry, whrend der lambda-
Ausdruck von proc i.d.R. woanders steht; damit sind die beiden Proze-
Eigenschaften von Prozeduren 121

duren intensional ungleich, und der obige Test mu fehlschlagen, auch


wenn die beiden Operanden von expect quivalent sind.
Damit ein check-property-Test die quivalenz testen kann, mu er
selbst (curry (uncurry proc)) anwenden:

(check-property
(for-all ((a string)
(b string)
(proc (string string -> string)))
(expect ((uncurry (curry proc)) a b)
(proc a b))))

Dieser Test funktioniert.

10.4 Programme beweisen

Check-property ist ntzlich, um zu berprfen, ob eine Eigenschaft


gilt oder nicht. Da check-property allerdings nur eine endliche Menge
zuflliger Tests durchfhrt, reicht es nicht aus, um sicherzugehen, da
eine bestimmte Eigenschaft fr alle Werte der for-all-Variablen gilt:
Dazu ist ein mathematischer Beweis notwendig.
An verschiedenen Stellen im Buch wurden Beweise fr mathemati-
sche Funktionen durchgefhrt zuletzt in Kapitel 6 fr eine rekursive
Funktion. Beweise ber mathematische Funktionen erlauben, bei jedem
Schrittan beliebigen Stellen Gleichungen einzusetzen. Beweise ber
Prozeduren in Programmen sind schwieriger, da sie das Substitutions-
modell bercksichtigen mssen: Bei jedem Reduktionsschritt kommt
immer nur eine bestimmte Substitution in Frage.

10.4.1 Arithmetische Gesetze beweisen

Als erstes Beispiel fr den Beweis an einem Programm dient der Beweis
der Kommutativitt von +. Der Beweis ist nicht besonders tiefgreifend,
demonstriert aber die wichtigsten Techniken, die beim Beweisen von
Programmen zum Einsatz kommen. Zu beweisen ist:

(= (+ a b) (+ b a))
= . . . = #t
. . . und zwar fr beliebige Bindungen von a und b an Zahlen. Seien
die Zahlen, die an a bzw. b gebunden sind, x und y mit x, y C. (Die
mathematischen Namen knnten auch a und b sein, aber das birgt
ein Verwirrungsrisiko mit a und b.) Wenn nun also der obige Term fr
bestimmte Werte von x und y im Substitutionsmodell reduziert wird,
wird zuerst x fr a und y fr b eingesetzt. Fr x = 5 und y = 17 also:

(= (+ 5 17) (+ 17 5))

Der Beweis soll aber fr beliebige Werte fr x und y funktionieren: x und


y mssen also im Beweis auftauchen. Um den Unterschied zwischen
Variablen des Programms a und b und den Zahlen zu machen, die fr
x und y eingesetzt werden, werden diese noch mit d_e umgeben: d x e in
einem Reduktionsschritt des Substitutionsmodell ist also ein Platzhalter
fr die Zahl, fr die x steht entsprechend fr y. Es gilt also:
122 Kapitel 10

(= (+ a b) (+ b a))
= (= (+ d x e dye) (+ dye d x e))

Dort ist der erste Teilausdruck unterstrichen, der beim ersten Substitu-
tionsschritt ersetzt wird. Wenn die Scheme-Prozedur + tatschlich die
mathematische Operation + realisiert,1 wird der Teilausdruck (+ d x e
dye) durch x + y ersetzt beziehungsweise durch die Zahl, fr die der
mathematische Ausdruck x + y steht. Es kommt also wieder d_e zum
Einsatz:

(= (+ d x e d y e) (+ dye d x e))
= (= d x + ye (+ dye d x e))

Entsprechend geht es weiter mit der zweiten Summe und schlielich


der Vergleichsoperation =, die dem mathematischen = entspricht:

= (= d x + ye dy + x e)
= d x + y = y + x e
= #t

Die Kommutativitt der Scheme-Prozedur + folgt also aus der Kommu-


tativitt des mathematischen +, durch das sie definiert ist.

10.5 Rekursive Programme beweisen

Beweise ber rekursive Programme sind anspruchsvoller als der Beweis


der Kommutativitt von +, benutzen aber die gleichen Techniken so-
wie genau wie bei Beweisen mathematischer rekursiver Funktionen
Induktion als Beweisprinzip.

10.5.1 Rekursive Programme auf Listen

Als erstes Beispiel dient die Reflexivitt. Es gilt fr die Bindung von
lis an eine beliebige Liste von Zahlen folgendes zu beweisen:

(number-list=? lis lis)


= . . . = #t

Wieder wird fr den Wert der Bindung eine mathematische Variable


eingefhrt l. Dann luft der Beweis auf folgendes hinaus:

(number-list=? lis lis)


= (number-list=? dl e dl e)
= . . . = #t

. . . und dies ist zu beweisen fr alle Listen von Zahlen l. Fr diesen


Beweis kommt uns die induktive Struktur der Listen zur Hilfe, die
der Struktur der endlichen Folgen bekannt aus Abschnitt 6.4 auf
Seite 67 entspricht. Dazu mssen wir zunchst einmal unterscheiden
allen Fllen der gemischten Datendefinition, also zwischen den leeren
und den nichtleeren Listen.
1 Die Komplikationen durch inexakte Zahlen und Rundungen bleiben hier unbercksich-
tigt.
Eigenschaften von Prozeduren 123

Leere Liste. Angenommen, l ist die leere Liste. Dann beginnt die
Reduktion folgendermaen:
(number-list=? d l e d l e)
= ((lambda (lis-1 lis-2) ...) d l e d l e)
= (cond ((empty? d l e) ...) ((pair? d l e) ...))
= (cond ((empty? d l e) ...) ((pair? d l e) ...))
= (cond (#t (cond ...)) ((pair? d l e) #f))
= (cond ((empty? d l e) #t) ((pair? d l e) #f))
= (cond (#t #t) ((pair? d l e) #f))
= #t

Nichtleere Liste. Fr diesen Fall stimmt die Behauptung also. An-


genommen, l ist nicht die leere Liste, hat also erstes Element f und
Rest r. In diesem Fall knnen wir strukturelle Induktion benutzen. Die
Induktionsvoraussetzung bezieht sich dann auf den Rest r:
(number-list=? dr e dr e)
= . . . = #t

Diese knnen wir benutzen. Zunchst einmal mssen wir aber soweit
reduzieren wie mglich:
(number-list=? d l e d l e)
= ((lambda (lis-1 lis-2) ...) dl e dl e)
= (cond ((empty? dl e) ...) ((pair? dl e) ...))
= (cond (#f ...) ((pair? dl e) ...))
= (cond (#t (cond ...)))
= (cond ((empty? d l e) ...) ((pair? d l e) ...))
= (cond (#f ...) ((pair? d l e) ...))
= (cond (#t (and ...)))
= (and (= (first d l e) (first d l e)) (number-list=? ...))

Da (first dl e) das erste Element f liefert, geht es so weiter:


= (and (= dfe (first dl e)) (number-list=? ...))
= (and (= d f e d f e) (number-list=? ...))
= (and #t (number-list=? ...))
= (number-list=? (rest d l e) (rest d l e))

Da (rest dl e) den Rest r liefert, geht es dann so weiter:


= (number-list=? dr e (rest dl e))
= (number-list=? dr e dr e)
Nach der Induktionsvoraussetzung wissen wir aber, da der letzte
Ausdruck zu #t reduziert wird. Die Behauptung ist damit bewisen.

10.5.2 Rekursive Programme auf Zahlen

Die Definition von factorial am Anfang von Abschnitt 7 folgt der


induktiven Definition der zugrundeliegenden Daten, der natrlichen
Zahlen. Dementsprechend ist der Induktionsbeweis fr dessen Korrekt-
heit einfach. Es ist aber entscheidend, die zu beweisende Eigenschaft,
welche die Korrektheit von factorial begrndet, sorgfltig aufzuschrei-
ben:
Die Prozedur factorial soll die Fakultt berechnen, es soll also fr
alle natrlichen Zahlen k gelten:
124 Kapitel 10

(factorial d k e)
= . . . = dk!e

(Diese Eigenschaft lt sich nicht sinnvoll mit for-all hinschreiben, da


die mathematische Fakultt nicht fest eingebaut ist.)
Da es um natrliche Zahlen geht, ist vollstndige Induktion anwend-
bar. Wir verwenden das Schema aus Abschnitt 6.2 auf Seite 63:

1. Die Behauptung ist bereits in der geforderten Form.


2. k = 0:

(factorial 0)
Longrightarrow d0!e

3. Beweis fr k = 0:

(factorial 0)
= ((lambda (n) ...) 0)
= (if (= 0 0) ...)
= (if #t 1 ...)
= 1
= d0!e

4. Induktionsvoraussetzung:

(factorial d k e)
= . . . = dk!e

5. Induktionsschlu (zu zeigen):

(factorial d k + 1e)
= . . . = d(k + 1)!e

Der Beweis sieht so aus:


6. (factorial dk + 1e)
= ((lambda (n) ...) dk + 1e)
= (if (= dk + 1e 0) ...)
= (if #f 1 (* ...))
= (* dk + 1e (factorial (- dk + 1e 1)))
= (* dk + 1e (factorial dke))
= (* dk + 1e (factorial dke))
Mit der Induktionsannahme kann (factorial dke) ersetzt werden:

(*dke (factorial dl e))


= . . . = (* dk + 1e dk!e)
= d(k + 1) k!e
= d(k + 1)!e

Damit ist der Beweis fertig.


Die Technik funktioniert auch mit Beispielen, bei denen die zu bewei-
sende Eigenschaft nicht so einfach zu sehen ist wie bei factorial.
Die folgende Scheme-Prozedur verrt nicht auf den ersten Blick, was
sie berechnet:
Eigenschaften von Prozeduren 125

(: f (natural -> rational))

(define f
(lambda (n)
(if (= n 0)
0
(+ (f (- n 1))
(/ 1 (* n (+ n 1)))))))

Tatschlich berechnet der Prozeduraufruf (f dke) fr eine natrliche


Zahl k die Zahl k+k 1 .
Die Eigenschaft ist plausibel, wie sich mit check-property feststellen
lt:
(check-property
(for-all ((k natural))
(= (f k) (/ k (+ k 1)))))

Ein Beweis schafft Sicherheit. Wieder gehen wir nach dem bekannten
Schema vor:
1. Behauptung:
Fr alle k N gilt:

(f d k e)
= . . . = d k+k 1 e

2. k = 0:
(f d0e)
=...= d 0+0 1 e
3. Beweis:
(f d0e)
= ((lambda (n) ...) 0)
= (if (= 0 0) 0 ...)
= (if #t 0 ...)
= 0
= d 0+0 1 e
4. Induktionsvoraussetzung:

(f d k e)
= . . . = d k+k 1 e

5. Induktionsschlu (zu zeigen):


(f d k + 1e)
= . . . = d (k+k+1)+
1
1
e

6. Beweis:
(f d k + 1e)
= ((lambda (n) (if ...)) dk + 1e)
= (if (= dk + 1e 0) ...)
126 Kapitel 10

= (if #f ... (+ ...))


= (+ (f (- dk + 1e 1)) (/ 1 (* dk + 1e (+ dk + 1e 1))))
= (+ (f dke) (/ 1 (* dk + 1e (+ dk + 1e 1))))
= . . . = (+ d k+k 1 e (/ 1 (* dk + 1e (+ dk + 1e 1))))
= (+ d k+k 1 e (/ 1 (* dk + 1e d(k + 1) + 1e)))
= (+ d k+k 1 e (/ 1 d(k + 1) ((k + 1) + 1)e))
= (+ d k+k 1 e d (k+1)((1k+1)+1) e)
= d k+k 1 + 1
(k+1)((k+1)+1)
e
= d k+k 1 + (k+1)( 1
k +2)
e
k (k +2) + 1
= d (k+1)(k+2) e
2
= d k(k++1)(2k + 1
k +2)
e
2
= d (k+(k1+ 1)
)(k+2)
e
k +1
= d k +2 e
= d (k+k+1)+
1
1
e

Damit die die Behauptung bewiesen.

10.6 Invarianten

Die bisher angewendete Technik fr den Beweis rekursiver Prozeduren


mit Induktion funktioniert bei Prozeduren mit Akkumulator nicht mehr
direkt: Angenommen, die Korrektheit der endrekursiven Fakuktt ! aus
Abschnitt 12.1 soll hnlich die die Korrektheit von factorial bewiesen
werden. Wieder sei n an eine natrliche Zahl k gebunden:

(! n)
= (! dke)
= ((lambda (n) (!-helper n 1)) dke)
= (!-helper dke 1)
= ((lambda (n acc) ...) dke 1)
= (if (= dke 0) 1 (!-helper (- dke 1) (* 1 dke)))

Wie bei factorial mu zwischen k = 0 und k > 0 unterschieden


werden. Fr k = 0 geht es folgendermaen weiter:

= (if (= d k e 0) 1 (!-helper (- d k e 1) (* 1 dke)))


= (if #t 1 (!-helper (- d k e 1) (* 1 d k e)))
= 1

Fr k = 0 funktioniert also der Beweis. Fr k > 0 allerdings verluft die


Reduktion folgendermaen:

= (if (= d k e 0) 1 (!-helper (- d k e 1) (* 1 dke)))


= (if #f 1 (!-helper (- d k e 1) (* 1 d k e)))
= (!-helper (- d k e 1) (* 1 d k e))

Hier gibt es zwar einen rekursiven Aufruf mit Argument (- dke 1),
aber der Akkumulator hat sich auch verndert. Damit ist die naheliegende
Induktionsannahme fr (!-helper (- dke 1) d ae) (falls der Wert des
Akkumulators acc a ist) wertlos. Prozeduren mit Akkumulator sind also
Eigenschaften von Prozeduren 127

nicht nur schwieriger zu schreiben als regulr rekursive Prozeduren


sie sind auch schwerer zu beweisen.
Stattdessen ist es bei Prozeduren mit Akkumulator ntzlich, eine
Invariante aufzustellen, also eine Eigenschaft, welche Zwischenergebnis
und noch zu leistende Arbeit in Beziehung setzt. Wie in Abschnitt 12.1
beschrieben, geht die Fakulttsprozedur mit Akkumulator folgender-
maen vor, um (! 4) auszurechnen:

(((1 4) 3) 2) 1

Bei jedem rekursiven Aufruf lt sich dieser Aufruf in geleistete Arbeit


(die durch den Akkumulator reprsentiert ist) und noch zu leisten-
de Arbeit unterteilen. Zum Beispiel entsteht ein rekursiver Aufruf
(!-helper 2 12), bei der Akkumulator der Wert des unterstrichenen
Teilaufrufs ist:
(((1 4) 3) 2) 1
Es ist zu sehen, da die noch zu leistende Arbeit gerade darin besteht,
den Akkumulator noch mit der Fakultt von 2 zu multiplizieren. Wenn
bei einem rekursiven Aufruf von !-helper der Wert von n k ist und der
Wert des Akkumulators a, und am Ende die Fakultt von N berechnet
werden soll, dann gilt bei jedem rekursiven Aufruf (!-helper dne d ae)
immer:
a k! = N!
Dies ist die Invariante von !-helper und heit so, weil sie beim rekursi-
ven Aufruf von !-helper unverndert bleibt. Dies ist zunchst nur eine
Behauptung, aber wenn sie gelten sollte, dann folgt daraus automatisch
die Korrektheit der Prozedur, da bei k = 0 gilt:

a 0! = a 1 = a = N!

Da a k! = N! tatschlich die Invariante ist, lt sich folgendermaen


folgern:
Sie gilt fr den ersten Aufruf von !-helper von !, da dort k = N und
a = 1 gilt, also:
a k! = 1 N! = N!
Jeder rekursive Aufruf erhlt die Invariante. Angenommen, sie gilt fr
k und a, dann sind die neuen Werte fr k und a im rekursiven Aufruf
(!-helper (- d n e 1) (* d a e d k e)) gerade k 7 k 1 und a 7 a n,
die Invariante wre also:

( a k ) ( k 1) ! = a ( k ( k 1) !
= a k!
= N!

Diese Technik funktioniert auch bei weniger offensichtlichen Proze-


duren. Hier eine Prozedur, die quivalent ist zu der Prozedur f aus
Abschnitt 10.5:

; n/(n+1) berechnen
(: f (natural -> natural))
128 Kapitel 10

(define f
(lambda (n)
(f-helper n 0)))

(define f-helper
(lambda (n acc)
(if (= n 0)
acc
(f-helper (- n 1)
(+ (/ 1 (* n (+ n 1)))
acc)))))

Die Prozedur geht folgendermaen vor, um das Ergebnis fr eine


Eingabe n zu berechnen:
1 1 1 1
(. . . (( + )+ )+...+ )
n (n + 1) (n 1) ((n 1) + 1) (n 2) ((n 2) + 1) 1 (1 + 1)
Diese Summe ist bei jedem rekursiven Aufruf aufgeteilt als Summe von
zwei Teilen, z.B. wie folgt:
1 1 1 1
(. . . (( + )+ )+...+ )
n (n + 1) (n 1) ((n 1) + 1) (n 2) ((n 2) + 1) 1 (1 + 1)
Der unterstrichene Teil ist gerade der Wert des Akkumulators, die
Summe rechts davon der noch zu berechnende Summand. Wenn die
Prozedur tatschlich n/(n + 1) berechnen sollte, ist dieser rechte Teil
im Beispiel (n 3)/((n 3) + 1). Damit ergibt sich die Invariante als
a + n/(n + 1), wobei a der Wert von acc ist. Um die Annahme zu
beweisen, da dies die Invariante ist, mu im wesentlichen folgende
Gleichung bewiesen werden:
n n1 1
a+ = a+ +
n+1 n n ( n + 1)
Dies ist eine lohnende Fingerbung.

Aufgaben

Aufgabe 10.1 Welche interessanten Eigenschaften hat die Division?


Schreiben Sie diese als Eigenschaften von / in Scheme auf.

Aufgabe 10.2 Schreiben Sie eine mglichst vollstndige Liste interes-


santer Eigenschaften sowohl der Ihnen bekannten arithmetischen Opera-
tionen als auch der logischen Operationen auf. Beziehen Sie dazu auch
die Vergleichsoperationen <, etc. ein. Finden Sie auerdem fr jede
Operation eine interessante Eigenschaft, die nicht gilt und berprfen,
ob DrRacket jeweils ein Gegenbeispiel findet.

Aufgabe 10.3 Fr and und or gelten zwei Distributivgesetze analog dem


Distributivgesetz fr * und +: Formulieren Sie diese als Eigenschaften
und lassen Sie DrRacket sie berprfen.
Abstrahieren Sie dann ber die nun insgesamt drei Distributivgesetze
analog zu commutativity und associativity und formulieren Sie die
drei Distributivgesetze mit Hilfe der Abstraktion neu. Schreiben Sie
eine mglichst aussagekrftige Signatur fr Ihre Abstraktion!
Eigenschaften von Prozeduren 129

Aufgabe 10.4 Schreiben Sie Abstraktionen analog zu commutativity


und associativity fr folgende Eigenschaften:

1. DeMorgan
2. Reflexivitt
3. Symmetrie
4. Antisymmetrie
5. Transitivitt
6. linksneutrales Element
7. rechtsneutrales Element
8. inverses Element

Aufgabe 10.5 Versuchen Sie, die Eigenschafts-Tests fr number-list=?


auszutricksen, also eine fehlerhafte Version von number-list=? zu schrei-
ben, die alle drei check-property-Tests besteht. Die check-expect-Tests
sind fr diese Aufgabe nicht relevant.

Aufgabe 10.6 Formulieren Sie Eigenschaften von filter und map im


Zusammenhang mit concatenate und testen Sie diese.

Aufgabe 10.7 Finden Sie eine przisere Formulierung der Kommutati-


vitt von list-sum als die in Abschnitt 10.2.4, also eine, an der sich die
Eigenschaft, da die Reihenfolge der Elemente der Liste keine Rolle
spielt klarer zu sehen ist.
Schreiben Sie dazu eine Prozedur, welche die Reihenfolge der Ele-
mente einer Liste abhngig von einer natrlichen Zahl n verndert, z.B.
indem die nte Permutation der Elemente ausgewhlt wird.

Aufgabe 10.8 Schreiben Sie einen check-property-Test fr folgende Ei-


genschaft:

(uncurry (curry p2 )) p2

Aufgabe 10.9 Formulieren Sie sinnvolle Eigenschaften von compose und


repeat von Seite 8.4 und berprfen Sie diese mit check-property!

Aufgabe 10.10 Beweise, da fr Prozeduren p1 mit einem Parameter,


die einparametrige Prozeduren zurckgeben, und Prozeduren p2 mit
zwei Parametern gilt:

(curry (uncurry p1 )) p1
(uncurry (curry p2 )) p2

Aufgabe 10.11 Beweisen Sie die entsprechend dem Beweis der Kom-
mutativitt von + in Abschnitt 10.4.1 die Assoziativitt von + sowie die
Distributivitt von + und * aus Abschnitt 10.1.1.

Aufgabe 10.12 Beweisen Sie, da die folgende Prozedur natrliche


Zahlen quadriert:
130 Kapitel 10

; Quadrat einer Zahl berechnen


(: square (natural -> natural))
(define square
(lambda (n)
(if (= n 0)
0
(+ (square (- n 1))
(- (+ n n) 1)))))

Formulieren Sie dazu auch eine Eigenschaft und berprfen Sie diese
mit check-property.

Aufgabe 10.13 Beweisen Sie, da auch die folgende Prozedur square


natrliche Zahlen quadriert. Geben Sie die Invariante von square-helper
an!
; Quadrat einer Zahl berechnen
(: square (natural -> natural))
(define square
(lambda (n)
(square-helper n 0)))

(define square-helper
(lambda (n acc)
(if (= n 0)
acc
(square-helper (- n 1)
(+ acc
(- (+ n n) 1))))))

Formulieren Sie dazu auch eine Eigenschaft und berprfen Sie diese
mit check-property.

Aufgabe 10.14 Beweise mit Hilfe des Substitutionsmodells, da die


concatenate-Prozedur aus Abschnitt 5.1 assoziativ ist, da also fr
Listen l1 , l2 und l3 gilt:

(concatenate l1 (concatenate l2 l3 )) = (concatenate (concatenate l1 l2 ) l3 )


11 Fortgeschrittenes Programmieren mit
Rekursion

FIXME: Vielleicht sowas wie Mergesort, parallele Listenverarbeitung

11.1 Lastwagen optimal beladen

FIXME: Backtracking sagen


Hier ist ein weiteres Problem, zu dessen Lsung Listen hervorra-
gend taugen: Die Aufgabe ist, einen Lastwagen optimal auszulasten:
Aus einem Lager mit zu transportierenden Artikeln, jeder mit einem
bestimmten Gewicht, sind solche Artikel auszuwhlen, dass die Tragf-
higkeit des Lastwagens mglichst gut ausgeschpft wird.
Fr die Lsung mu erst einmal festgelegt werden, wie Ein- und
Ausgabe des Programms aussehen sollen. Die elementare Gre im Pro-
blem ist ein Artikel, der aus einer Artikelnummer und seinem Gewicht
besteht. Folgende Daten- und Record-Definition passen dazu:
; Ein Artikel ist ein Wert
; (make-article n w)
; wobei n die Nummer des Artikels ist
; und w das Gewicht des Artikels in Kilo
(define-record-procedures article
make-article article?
(article-number article-weight))

Ein Beispiel-Lager wird durch die folgende Liste beschrieben:


; Ein Beispiel-Lager
(: stock (list(article))
(define stock
(list (make-article 1 274)
(make-article 2 203)
(make-article 3 268)
(make-article 4 264)
(make-article 5 229)
(make-article 6 406)
(make-article 7 220)
(make-article 8 232)
(make-article 9 356)
(make-article 10 197)
(make-article 11 207)
(make-article 12 373)))
132 Kapitel 11

Die Lsung des Problems soll eine Prozedur load-list sein, welche eine
Liste der Artikel zurckliefert, die in den Lastwagen geladen werden
sollen. Neben der Liste der Artikel braucht load-list auch noch die
Tragfhigkeit eines Lastwagens. Die Prozedur soll Kurzbeschreibung
und Signatur wie folgt haben:

; Maximale Liste von Artikeln berechnen,


; die auf einen Lastwagen passen
(: load-list (list(article) number -> list(article))

Im Fall des Beispiel-Lagers und eines Lastwagens mit 1800 kg Tragf-


higkeit soll folgendes passieren, wenn das Programm fertig ist:

(load-list stock 1800)


, #<list #<record:article 1 274> #<record:article 2 203>
#<record:article 3 268> #<record:article 6 406>
#<record:article 7 220> #<record:article 8 232>
#<record:article 10 197>>

Die Prozedur arbeitet auf Listen, was folgende Schablone nahelegt:


(define load-list
(lambda (articles capacity)
(cond
((empty? articles) ...)
((pair? articles)
... (first articles) ...
... (load-list (rest articles) capacity) ...))))

Wenn keine Artikel da sind, kommen auch keine in den Lastwagen. Die
Liste im ersten Zweig ist also leer. Der zweite Fall ist etwas komplizierter.
Das liegt daran, da es dort eine weitere Fallunterscheidung gibt, je nach
dem ersten Artikel (first articles): die Prozedur mu entscheiden,
ob dieser erste Artikel schlielich in den Lastwagen kommen soll oder
nicht. Ein erstes Ausschlukriterium ist, wenn der Artikel schwerer ist
als die Tragfhigkeit erlaubt:

(define load-list
(lambda (articles capacity)
(cond
((empty? articles) empty)
((pair? articles)
(if (> (article-weight (first articles)) capacity)
(load-list (rest articles) capacity)
... (load-list (rest articles) capacity) ...)))))

Damit ist die Frage, ob der erste Artikel im Lastwagen landet, aber
immer noch nicht abschlieend beantwortet. Schlielich mu load-list
noch entscheiden, ob unter Verwendung dieses Artikels der Lastwagen
optimal vollgepackt werden kann. Dazu mu die Prozedur vergleichen,
wie ein Lastwagen mit dem ersten Artikel und wie er ohne diesen Artikel
am besten vollgepackt werden wrde. Die Variante ohne wird mit
folgendem Ausdruck ausgerechnet:
(load-list (rest articles) capacity)
Fortgeschrittenes Programmieren mit Rekursion 133

Die Variante mit ist etwas trickreicher sie entsteht, wenn im Last-
wagen der Platz fr den ersten Artikel reserviert wird und der Rest
der Tragfhigkeit mit den restlichen Artikeln optimal gefllt wird. Die
optimale Fllung fr den Rest wird mit folgendem Ausdruck berechnet,
der die Induktionsannahme fr load-list benutzt:

(load-list (rest articles)


(- capacity (article-weight (first articles))))

Die vollstndige Artikelliste entsteht dann durch nachtrgliches Wieder-


Anhngen des ersten Artikels:

(make-pair (first articles)


(load-list (rest articles)
(- capacity (article-weight (first articles)))))

Diese beiden Listen mssen jetzt nach ihrem Gesamtgewicht verglichen


werden. Die Liste mit dem greren Gewicht gewinnt. Als erster Schritt
werden die beiden obigen Ausdrcke in die Schablone eingefgt:

(define load-list
(lambda (articles capacity)
(cond
((empty? articles) empty)
((pair? articles)
(if (> (article-weight (first articles)) capacity)
(load-list (rest articles) capacity)
... (load-list (rest articles) capacity) ...
... (make-pair (first articles)
(load-list
(rest articles)
(- capacity
(article-weight (first articles)))))
...)))))

Die Ausdrcke fr die beiden Alternativen sind in dieser Form un-


handlich gro, was die Prozedur schon unbersichtlich macht, bevor
sie berhaupt fertig ist. Es lohnt sich also, ihnen Namen zu geben:

(define load-list
(lambda (articles capacity)
(cond
((empty? articles) empty)
((pair? articles)
(if (> (article-weight (first articles)) capacity)
(load-list (rest articles) capacity)
(let ((articles-1 (load-list (rest articles) capacity))
(articles-2
(make-pair (first articles)
(load-list
(rest articles)
(- capacity
(article-weight (first articles)))))
...))))))))
134 Kapitel 11

Der Ausdruck (article-weight (first articles)) kommt zweimal vor.


Die Einfhrung einer weiteren lokalen Variable macht die Prozedur
noch bersichtlicher:

(define load-list
(lambda (articles capacity)
(cond
((empty? articles) empty)
((pair? articles)
(let ((first-weight (article-weight (first articles))))
(if (> first-weight capacity)
(load-list (rest articles) capacity)
(let ((articles-1 (load-list (rest articles) capacity))
(articles-2
(make-pair (first articles)
(load-list
(rest articles)
(- capacity first-weight)))
...)))))))))

Zurck zur eigentlichen Aufgabe: articles-1 und articles-2 sollen


hinsichtlich ihres Gewichts verglichen werden. Dies mu natrlich
berechnet werden. Da dafr noch eine Prozedur fehlt, kommt Wunsch-
denken zur Anwendung:

; Gesamtgewicht einer Liste von Artikeln berechnen


(: articles-weight (list(article) -> number)

Damit kann load-list vervollstndigt werden:

(define load-list
(lambda (articles capacity)
(cond
((empty? articles) empty)
((pair? articles)
(let ((first-weight (article-weight (first articles))))
(if (> first-weight capacity)
(load-list (rest articles) capacity)
(let ((articles-1 (load-list (rest articles) capacity))
(articles-2
(make-pair (first articles)
(load-list
(rest articles)
(- capacity first-weight)))))
(if (> (articles-weight articles-1)
(articles-weight articles-2))
articles-1
articles-2))))))))

Es fehlt noch articles-weight, die wieder streng nach Anleitung geht


und fr welche die Schablone folgendermaen aussieht:

(define articles-weight
(lambda (articles)
Fortgeschrittenes Programmieren mit Rekursion 135

(cond
((empty? articles) ...)
((pair? articles)
... (first articles) ...
... (articles-weight (rest articles)) ...))))

Das Gesamtgewicht der leeren Liste ist 0 der erste Fall ist also wieder
einmal einfach. Im zweiten Fall interessiert vom ersten Artikel nur das
Gewicht:
(define articles-weight
(lambda (articles)
(cond
((empty? articles) 0)
((pair? articles)
... (article-weight (first articles)) ...
... (articles-weight (rest articles)) ...))))

Nach Induktionsannahme liefert (articles-weight (rest articles))


das Gewicht der restlichen Artikel. Das Gewicht des ersten Artikels
mu also nur addiert werden:
(define articles-weight
(lambda (articles)
(cond
((empty? articles) 0)
((pair? articles)
(+ (article-weight (first articles))
(articles-weight (rest articles)))))))

Mit articles-weight lt sich bestimmen, wie voll der Lastwagen bela-


den ist. Im Falle von stock ist das Ergebnis sehr erfreulich; kein Platz
wird verschenkt:
(articles-weight (load-list stock 1800))
, 1800
12 Programmieren mit Akkumulatoren

Bei den rekursiven Prozeduren der vergangenen Kapitel war der Wert
eines rekursiven Aufrufs stets unabhngig vom Kontext: Die Fakultt
von 4 wute nicht, da sie spter noch mit 5 multipliziert wird, die
Summe der Zahlen von 1 bis 4 wute nicht, da spter noch 5 dazu-
addiert wird, etc. Manche Probleme sind aber so formuliert, da bei der
Berechnung ein Zwischenergebnis mitgefhrt und aktualisiert wird. Die
Konstruktionsanleitungen fr Prozeduren, die Listen oder natrliche
Zahlen verarbeiten, fhren aber bei direkter Anwendung zu Prozeduren,
die kein Zwischenergebnisses mitfhren knnen: Solche Probleme erfor-
dern deshalb eine neue Programmiertechnik, das Programmieren mit
Akkumulatoren, und entsprechend angepate Konstruktionsanleitungen.

12.1 Zwischenergebnisse mitfhren

Gefragt ist eine Prozedur, welche eine Liste invertiert, also die Reihen-
folge ihrer Elemente umdreht:
; Liste umdrehen
(: invert ((list-of %a) -> (list-of %a)))

(check-expect (invert empty) empty)


(check-expect (invert (list 1 2 3 4)) (list 4 3 2 1))

Gerst und Schablone sehen wie folgt aus:


(define invert
(lambda (lis)
(cond
((empty? lis) ...)
((pair? lis)
... (first lis) ...
... (invert (rest lis)) ...))))

Der Ausdruck (invert (rest lis)) liefert den Rest der Liste in umge-
kehrter Reihenfolge. Falls lis, wie im zweiten Testfall, also die Liste
#<list 1 2 3 4> ist, so ist der invertierte Rest #<list 4 3 2>. Das ge-
wnschte Ergebnis #<list 4 3 2 1> entsteht durch das Anhngen des
ersten Elements hinten an die Liste. Durch Wunschdenken wird eine
Prozedur append-element angenommen, die hinten an eine Liste ein
Element anhngt:
; Element an Liste anhngen
(: append-element ((list-of %a) %a -> (list-of %a)))
138 Kapitel 12

Abbildung 12.1. Prozeduraufrufe bei invert

Mit Hilfe von append-element lt sich invert leicht vervollstndigen:


(define invert
(lambda (lis)
(cond
((empty? lis) empty)
((pair? lis)
(append-element (invert (rest lis))
(first lis))))))

Die Prozedur append-element ist ganz hnlich der Prozedur concatenate


aus Abschnitt 5.5. Zunchst Testflle:
(check-expect (append-element (list 1 2 3) 4) (list 1 2 3 4))
(check-expect (append-element empty 4) (list 4))

Gerst und Schablone:


(define append-element
(lambda (lis el)
(cond
((empty? lis) ...)
((pair? lis)
... (first lis) ...
... (append-element (rest lis) el) ...))))

Die Schablone lt sich leicht vervollstndigen:


(define append-element
(lambda (lis el)
(cond
((empty? lis) (list el))
((pair? lis)
(make-pair (first lis)
(append-element (rest lis) el))))))

Doch zurck zu invert. Obwohl die zu erledigende Aufgabe einfach


erscheint, dauert schon das Invertieren von Listen der Lnge 1000 eine
ganze Weile.1 Tatschlich ist es so, da z.B. das Invertieren einer Liste
1 So war es zumindest zur Zeit der Drucklegung dieses Buchs auf handelsblichen Com-
putern. Ggf. mssen es auf moderneren Rechnern Listen der Lnge 10000 sein, um das
Problem deutlich zu machen.
Programmieren mit Akkumulatoren 139

der Lnge 400 mehr als doppelt so lang wie das Invertieren einer Liste
der Lnge 200 bentigt. Das liegt daran, da invert bei jedem rekursiven
Aufruf append-element aufruft, und append-element selbst macht soviele
rekursive Aufrufe wie die Liste lang ist. Das wiederum heit aber, da
die Gesamtanzahl der Prozeduraufrufe fr das Invertieren einer Liste
der Lnge n so steigt wie in der Kurve in Abbildung 12.1 gezeigt, also
offenbar strker als linear: Das erklrt das berproportionale Ansteigen
der Rechenzeit. (Dafr ist auch Aufgabe 12.1 relevant.) Dies ist fr so
eine einfache Aufgabe inakzeptabel: Listen der Lnge 10000 sind nichts
ungewhnliches, und das Invertieren sollte dem Computer leichtfallen.
Tatschlich gibt es eine bessere Methode, eine Liste umzudrehen: Die
obige invert-Prozedur konstruiert die Ergebnisliste, indem stets Ele-
mente hinten angehngt werden. Das entspricht nicht der natrlichen
Konstruktion von Listen mit make-pair, das ein Element vorn anhngt.
Das Ergebnis liee sich aber durch Anhngen vorn ganz einfach konstru-
ieren, und zwar, indem in folgender Reihenfolge Zwischenergebnisse
berechnet werden, wie in folgendem Beispiel fr den Testfall (invert
(list 1 2 3 4)):

#<empty-list>
#<list 1>
#<list 2 1>
#<list 3 2 1>
#<list 4 3 2 1>

Jedes Zwischenergebnis entsteht aus dem vorhergehenden, indem ein


Element vorn an die Liste darber angehngt wird. Dies geschieht
in der Reihenfolge, in der die Elemente in der ursprnglichen Liste
auftreten: scheinbar einfach. Allerdings erlaubt die normale Konstruk-
tionsanleitung fr Listen nicht, dieses Zwischenergebnis mitzufhren:
Das Ergebnis des rekursiven Aufrufs (invert (rest lis)) ist unabhn-
gig vom Wert von (first lis). Damit aber ist es der Prozedur aus der
normalen Konstruktionsanleitung unmglich, die obige Folge von Zwi-
schenergebnissen nachzuvollziehen, da von einem Zwischenergebnis
zum nchsten gerade (first lis) vorn angehngt wird. Fr diesen
speziellen Fall wenn eine Berechnung das Mitfhren von Zwischener-
gebnissen erfordert mu die normale Konstruktionsanleitung deshalb
angepat werden.
Dieses Problem lt sich durch Mitfhren des Zwischenergebnisses
in einem separaten Parameter lsen, dem sogenannten Akkumulator.
Dazu wird eine Hilfsprozedur invert-helper definiert, die neben der
Eingabeliste diesen Akkumulator akzeptiert:
(: invert-helper ((list-of %a) (list-of %a) -> (list-of %a)))

(define invert-helper
(lambda (lis acc)
...))

Die Liste lis ist nach wie vor die bestimmende Eingabe, es greifen
also die entsprechenden Konstruktionsanleitungen fr gemischte und
zusammengesetzte Daten:
(define invert-helper
140 Kapitel 12

(lambda (lis acc)


(cond
((empty? lis) ...)
((pair? lis)
... (first lis) ...
... (invert-helper (rest lis) ...) ...))))

Wenn invert-helper aufgerufen wird, sind die noch zu verarbeitenden


Elemente der ursprnglichen Liste in lis, und das Zwischenergebnis,
was aus den Elementen davor berechnet wurde, ist in acc. Wenn lis
leer ist, sind alle Elemente verarbeitet und das Zwischenergebnis ist
das Endergebnis:
(define invert-helper
(lambda (lis acc)
(cond
((empty? lis) acc)
((pair? lis)
... (first lis) ...
... (invert-helper (rest lis) ...) ...))))

Fr den rekursiven Aufruf mu noch ein neuer Wert fr acc bergeben


werden. Dieser entsteht, wie im Beispiel zu sehen ist, dadurch, da an
den Akkumulator das erste Element der Liste vorn angehngt wird:

(define invert-helper
(lambda (lis acc)
(cond
((empty? lis) acc)
((pair? lis)
... (invert-helper (rest lis)
(make-pair (first lis) acc)) ...))))

Da der rekursive Aufrufs von invert-helper schlielich direkt das End-


ergebnis zurckgegeben wird, ist damit die Prozedur auch schon fertig:
(define invert-helper
(lambda (lis acc)
(cond
((empty? lis) acc)
((pair? lis)
(invert-helper (rest lis)
(make-pair (first lis) acc))))))

Die neue Hilfsprozedur invert-helper pat nicht auf die Signatur von
invert; invert mu also separat definiert werden und den passenden
Anfangswert fr acc bergeben:
(define invert
(lambda (lis)
(invert-helper lis empty)))

Die neue Version von invert kommt ohne append-element aus. Der
Beispielaufruf von oben fhrt zu folgender Auswertung im Substituti-
onsmodell, die sich auch im Stepper gut nachvollziehen lt:
Programmieren mit Akkumulatoren 141

(invert (list 1 2 3 4))


= . . . = (invert-helper #<list 1 2 3 4> empty)
= . . . = (cond ((empty? #<list 1 2 3 4>) ...) ((pair? #<list 1 2 3 4>) ...))
= . . . = (invert-helper (rest #<list 1 2 3 4>) (make-pair (first #<list 1 2 3 4>) empty))
= . . . = (invert-helper #<list 2 3 4> (make-pair 1 empty))
= . . . = (invert-helper #<list 2 3 4> #<list 1>)
= . . . = (cond ((empty? #<list 2 3 4>) ...) ((pair? #<list 2 3 4>) ...))
= . . . = (invert-helper (rest #<list 2 3 4>) (make-pair (first #<list 2 3 4>) #<list 1>))
= . . . = (invert-helper #<list 3 4> (make-pair 2 #<list 1>))
= . . . = (invert-helper #<list 3 4> #<list 2 1>)
= . . . = (cond ((empty? #<list 3 4>) ...) ((pair? #<list 3 4>) ...))
= . . . = (invert-helper (rest #<list 3 4>) (make-pair (first #<list 3 4>) #<list 2 1>))
= . . . = (invert-helper #<list 4> (make-pair 3 #<list 2 1>))
= . . . = (invert-helper #<list 4> #<list 3 2 1>)
= . . . = (cond ((empty? #<list 4>) ...) ((pair? #<list 4>) ...))
= . . . = (invert-helper (rest #<list 4>) (make-pair (first #<list 4>) empty))
= . . . = (invert-helper #<empty-list> (make-pair 4 #<list 3 2 1>))
= . . . = (invert-helper #<empty-list> #<list 4 3 2 1>)
= . . . = (cond ((empty? #<empty-list>) #<list 4 3 2 1>) ((pair? #<empty-list>) ...))
= #<list 4 3 2 1>

Tatschlich arbeitet die neue Prozedur auch effizienter: Da invert-helper


nicht bei jedem Aufruf selbst wieder eine Prozedur aufruft, die eine
komplette Liste verarbeitet, steigt die Rechenzeit nur noch proportional
zur Lnge der Liste.

Da die Prozedur invert generell ntzlich ist, ist sie unter dem Namen
reverse fest eingebaut.

Die letrec-Form bindet lokale Variablen, hnlich wie let. Ihre Syntax
ist mit der von let identisch. Whrend bei let die Variablen, die an
die Werte der Ausdrcke gebunden werden, in den Ausdrcken selbst
nicht sichtbar sind, sind bei letrec die Bindungen sichtbar.

Abbildung 12.2. letrec

Die Prozedur invert-helper wird nur an einer einzigen Stelle auf-


gerufen, und zwar innerhalb von invert. Es wrde auch kaum einen
Sinn ergeben, invert-helper von einer anderen Stelle aus aufzurufen.
Deshalb bietet es sich an, fr invert-helper eine lokale Definition zu
verwenden, die nur innerhalb von invert sichtbar ist. Dazu gibt es im
Prinzip die let-Form, die allerdings fr diesen Zweck nicht verwendbar
ist (Abb. 12.2). Die dort vorgestellte letrec-Form lst das Problem.

Akkumulatoren, die Zwischenergebnisse verwalten, sind bei der


Lsung einer Reihe von Problemen ntzlich. Zum Beispiel knnte eine
Prozedur, welche die Fakultt n! einer Zahl n berechnet, vorgehen,
142 Kapitel 12

indem sie das Produkt n . . . 1 schrittweise von links her ausrechnet:

1u 4 = 4

4 u 3 = 12
u
12 2 = 24
u
24 1 = 24
u
24

Der Anfangswert fr das Zwischenergebnis ist 1, also gerade die Fakul-


tt von 0.
Fr die Konstruktion wird die Schablone fr Prozeduren, die natrli-
che Zahlen verarbeiten, um einen Akkumulator erweitert:
; Fakultt berechnen

(: ! (natural -> natural))

(check-expect (! 0) 1)
(check-expect (! 3) 6)
(check-expect (! 5) 120)

(: ! (natural -> natural))


(define !
(lambda (n)
(letrec
((!-helper
(lambda (n acc)
(if (= n 0)
acc
... ))))
(!-helper n 1))))

Wie aus der Beispielrechnung ersichtlich ist, wird aus dem alten
Zwischenergebnis das neue Zwischenergebnis, indem jeweils mit n
multipliziert wird:
(letrec
((!-helper
(lambda (n acc)
(if (= n 0)
acc
... (!-helper (- n 1) (* n acc)) ...))))

Wie schon bei invert ist es nicht notwendig, da fr die noch ver-
bleibenden Ellipsen etwas eingesetzt wird; das Programm ist bereits
fertig:
(: ! (natural -> natural))
(define !
Programmieren mit Akkumulatoren 143

(lambda (n)
(letrec
((!-helper
(lambda (n acc)
(if (= n 0)
acc
(!-helper (- n 1) (* n acc)) ...))))
(!-helper n 1))))

Tatschlich ist es bei Prozeduren mit Akkumulator grundstzlich nicht


notwendig, fr die Ellipsen am Schlu etwas einzusetzen. Bei der nor-
malen Schablone fr Prozeduren, die Listen bzw. natrliche Zahlen
verarbeiten, wird fr diese Ellipsen Code eingesetzt, was das erste
Element der Liste mit dem Ergebnis des rekursiven Ausdrucks zum
Rckgabewert kombiniert. Dies ist beim Einsatz eines Akkumulators
aber nicht notwendig, da das erste Element der Liste bereits in die
Berechnung des nchsten Zwischenergebnisses eingeht und dieses Zwi-
schenergebnis beim letzten Aufruf bereits das Endergebnis ist.

12.2 Schablonen fr Prozeduren mit Akkumulator

Aus den beiden Beispielen des vorgehenden Abschnitts ergeben sich


direkt Schablonen fr Prozeduren mit Akkumulator. Zunchst die
Schablone fr Prozeduren mit Akkumulator, die Listen akzeptieren:
(: proc ((list-of elem) -> ...))

(define proc
(lambda (lis)
(letrec
((proc-helper
(lambda (lis acc)
(cond
((empty? lis) acc)
((pair? lis)
(proc-helper (rest lis)
(... (first lis) ... acc ...)))))))
(proc-helper lis z))))

Hier ist proc der Name der zu definierenden Prozedur und proc-helper
der Name der Hilfsprozedur mit Akkumulator. Der Anfangswert fr
den Akkumulator also das initiale Zwischenergebnis ist der Wert von
z. Der Ausdruck, der fr (... (first lis) ... acc ...) einzusetzen
ist, macht aus dem alten Zwischenergebnis acc das neue Zwischener-
gebnis.
Die Schablone fr Prozeduren mit Akkumulator, die natrliche Zah-
len akzeptieren, ist analog:
(: proc (natural -> ...))

(define proc
(lambda (n)
(letrec
((proc-helper
144 Kapitel 12

(lambda (n acc)
(if (= n 0)
acc
(proc-helper (- n 1) (... acc ...))))))
(proc-helper n z))))

Wieder ist z der Ausdruck fr das initiale Zwischenergebnis und fr


(... acc ...) ist ein Ausdruck einzusetzen, der aus dem alten Zwi-
schenergebnis acc ein neues macht.

12.3 Kontext und Endrekursion

Ein Vergleich der beiden Versionen der Fakulttsfunktion von S. 79


und S. 142 zeigt, da Formulierungen mit und ohne Akkumulator
unterschiedliche Berechnungsprozesse erzeugen. Hier ein Proze mit
Akkumulator:
(! 4)
= (!-helper 4 1)
= (if (= 4 0) 1 (!-helper (- 4 1) (* 1 4)))
= (if #f 1 (!-helper (- 4 1) (* 1 4)))
= (!-helper (- 4 1) (* 1 4))
= (!-helper 3 4)
= (if (= 3 0) 4 (!-helper (- 3 1) (* 4 3)))
= (if #f 4 (!-helper (- 3 1) (* 4 3)))
= (!-helper (- 3 1) (* 4 3))
= (!-helper 2 12)
= (if (= 2 0) 12 (!-helper (- 2 1) (* 12 2)))
= (if #f 12 (!-helper (- 2 1) (* 12 2)))
= (!-helper (- 2 1) (* 12 2))
= (!-helper 1 24)
= (if (= 1 0) 24 (!-helper (- 1 1) (* 24 1)))
= (if #f 24 (!-helper (- 1 1) (* 24 1)))
= (!-helper (- 1 1) (* 1 24))
= (!-helper 0 24)
= (if (= 0 0) 24 (!-helper (- 0 1) (* 24 0)))
= (if #t 24 (!-helper (- 0 1) (* 24 0)))
= 24

Demgegenber hier der Proze ohne Akkumulator:


(! 4)
= (if (= 4 0) 1 (* 4 (! (- 4 1))))
= (if #f 1 (* 4 (! (- 4 1))))
= (* 4 (! (- 4 1)))
= (* 4 (! 3))
= (* 4 (if (= 3 0) 1 (* 3 (! (- 3 1)))))
= (* 4 (if #f 1 (* 3 (! (- 3 1)))))
= (* 4 (* 3 (! (- 3 1))))
= (* 4 (* 3 (! 2)))
...
= (* 4 (* 3 (* 2 (! 1))))
= (* 4 (* 3 (* 2 (if (= 1 0) 1 (* 1 (! (- 1 1)))))))
= (* 4 (* 3 (* 2 (if #f ... (* 1 (! (- 1 1)))))))
Programmieren mit Akkumulatoren 145

= (* 4 (* 3 (* 2 (* 1 (! (- 1 1))))))
= (* 4 (* 3 (* 2 (* 1 (! 0)))))
= (* 4 (* 3 (* 2 (* 1 (if (= 0 0) 1 (* 0 (! (- 0 1))))))))
= (* 4 (* 3 (* 2 (* 1 (if #t 1 (* 0 (! (- 0 1)))))))))
= (* 4 (* 3 (* 2 (* 1 1))))
= (* 4 (* 3 (* 2 1)))
= (* 4 (* 3 2))
= (* 4 6)
= 24

Es ist deutlich sichtbar, da die Version ohne Akkumulator alle Mul-


tiplikationen bis zum Schlu aufstaut. Das heit aber auch, da im
Laufe des Berechnungsprozesses Ausdrcke auftauchen, die desto gr-
er werden je grer das Argument von ! ist: Bei (! 100) werden zum
Beispiel 100 Multiplikationen aufgestaut.
Die Version mit Akkumulator hingegen scheint in der Gre der
zwischenzeitlich auftretenden Ausdrcke begrenzt zu sein. Tatschlich
stellt sich das Wachstum der Version ohne Akkumulator bei der Version
mit Akkumulator nicht ein.
Der Grund dafr sind die Schablonen: In der Schablone fr Prozedu-
ren ohne Akkumulator steht (... (proc (- n 1)) ...), das heit, um
den rekursiven Aufruf von proc wird noch etwas herumgewickelt,
oder, anders gesagt, mit dem Ergebnis des rekursiven Aufrufs passiert
noch etwas. Das, was mit dem Ergebnis noch passiert, heit der Kontext
des Aufrufs. Bei ! ist der vollstndige Ausdruck (* n (! (- n 1))).
Wenn aus diesem Ausdruck der rekursive Aufruf (! (- n 1)) heraus-
genommen wird, bleibt der Kontext (* n ), wobei markiert, wo der
Aufruf entfernt wurde. Tatschlich wird in der Literatur diese Markie-
rung Loch genannt und [] geschrieben. Der Kontext (* n []) macht
deutlich, da mit Ergebnis eines Aufrufs, der spter fr [] eingesetzt
wird, noch n multipliziert wird. Dementsprechend stauen sich in der
Reduktionsfolge die Multiplikationen mit den verschiedenen Werten
von n.
Bei der Fakultts-Prozedur mit Akkumulator ist der Ausdruck, zu
dem der Rumpf bei n 6= 0 reduziert wird, (!-helper (- n 1) (* n
acc)). Der Kontext des Aufrufs von !-helper innerhalb dieses Aus-
drucks ist [], also leer nichts passiert mehr mit dem Rckgabewert von
!-helper, und damit stauen sich auch bei der Reduktion keine Kontexte
an. Solche Prozeduraufrufe ohne Kontext heien endrekursiv eben,
weil nach dem rekursiven Aufruf Ende ist.2 Die Berechnungsprozesse,
die von endrekursiven Aufrufen generiert werden, heien auch iterative
Prozesse.

12.4 Das Phnomen der umgedrehten Liste

Die beiden Varianten der Fakultts-Prozedur berechnen zwar beide


stets das gleiche Ergebnis. Die beiden Reduktionsfolgen fr (! 4) aus
dem vorigen Abschnitt zeigen allerdings, da die beiden Prozeduren
bei der Berechnung unterschiedlich vorgehen: Whrend die Variante
ohne Akkumulator von rechts multipliziert, also folgendermaen
2 Das Konzept des Aufrufs ohne Kontext ist nicht auf rekursive Aufrufe beschrnkt. Im
Englischen heien solche Aufrufe allgemeiner tail calls (also ohne recursive).
146 Kapitel 12

auswertet:
4 (3 (2 (1 1)))
multipliziert die Variante mit Akkumulator von links:

(((1 4) 3) 2) 1

Die Multiplikationen passieren also in umgekehrter Reihenfolge. Dies


macht bei der Fakultt keinen Unterschied, da die Multiplikation as-
soziativ ist. Diese Assoziativitt ist jedoch nicht immer gegeben ins-
besondere nicht bei Prozeduren, die Listen zurckgeben. Hier zum
Beispiel eine Prozedur, die eine Zahl n akzeptiert und eine absteigende
Liste der Zahlen von n bis 1 zurckliefert:

; Liste der Zahlen von n bis 1 generieren


(: build-list (natural -> (list-of natural)))

(check-expect (build-list 0) empty)


(check-expect (build-list 3) (list 3 2 1))

(define build-list
(lambda (n)
(if (= n 0)
empty
(make-pair n (build-list (- n 1))))))

Die direkte bersetzung in eine Variante mit Akkumulator liefert:

(define build-list
(lambda (n)
(letrec
((build-list-helper
(lambda (n acc)
(if (= n 0)
acc
(build-list-helper (- n 1) (make-pair n acc))))))
(build-list-helper n empty))))

Diese Variante ist inkorrekt: Sie liefert z.B. fr (build-list 3) das Er-
gebnis #<list 1 2 3>, die Elemente der Liste sind also in umgekehrter
Reihenfolge. Da schon die Fakulttsprozedur mit Akkumulator die Mul-
tiplikationen gegenber der Variante ohne Akkumulator in umgekehrter
Reihenfolge durchgefhrt hat, war dies allerdings zu erwarten, und ist
ein generelles Phnomen bei der Berechnung von Listen-Ausgaben mit
Akkumulator. Das Problem kann durch das Umdrehen der Ergebnisliste
gelst werden:

(letrec
((build-list-helper
(lambda (n acc)
(if (= n 0)
(reverse acc)
(build-list-helper (- n 1) (make-pair n acc))))))
Programmieren mit Akkumulatoren 147

Anmerkungen

Bei der Auswertung von Programmen durch den Computer wird fr die
Verwaltung von Kontexten Speicherplatz bentigt: Bei rekursiven Pro-
zeduren ohne Akkumulator wchst dieser Speicherplatz mit der Gre
der Argumente. Entsprechend wird prinzipiell kein Speicherplatz ben-
tigt, wenn kein Kontext anfllt. In Scheme wird auch tatschlich kein
Speicherplatz fr endrekursive Aufrufe verbraucht; dies ist allerdings
bei vielen anderen Programmiersprachen nicht der Fall. Mehr dazu in
Kapitel 17.

Aufgaben

Aufgabe 12.1 Entwicklen Sie eine Formel fr die Anzahl der rekursiven
Aufrufe in der ersten Version von invert! (Hinweis: Greifen Sie auf die
Gausche Summenformel zurck.)

Aufgabe 12.2 Schreiben Sie eine Prozedur list-sum+product, die eine


Liste von Zahlen akzeptiert und eine zweielementige Liste zurckgibt,
deren erstes Element die Summe der Listenelemente und deren zweites
Element ihr Produkt ist. Schreiben Sie zwei Varianten der Prozedur:
eine ohne Akkumulator und eine mit zwei Akkumulatoren.

Aufgabe 12.3 Schreiben Sie eine Prozedur, die als Eingabe eine Liste
von Kursen einer Aktie (als Zahlen) eines Tages akzeptiert (nach Tages-
zeit aufsteigend sortiert), und als Rckgabewert den hchstmglichen
Gewinn liefert, die durch den Kauf und folgenden Verkauf der Aktie
an diesem Tag erreicht werden kann.
Hinweis: Diese Prozedur bentigt zwei Akkumulatoren.

Aufgabe 12.4 Schreibe zu der Prozedur power aus Aufgabe 7.2 eine
endrekursive Variante.

Aufgabe 12.5 Identifizieren Sie die Kontexte der Aufrufe der Prozedu-
ren namens p in folgenden Ausdrcken:

(+ (p (- n 1)) 1)
(p (- n 1) acc)
(* (p (rest lis)) b)
(+ (* 2 (p (- n 1))) 1)
(p (- n 1) (* acc n))
(f (p n))
(+ (f (p n)) 5)
(p (f (- n 1)) (* n (h n)))
(+ (f (p n)) (h n))

Welche Aufrufe sind endrekursiv bzw. tail calls?

Aufgabe 12.6 Schreibe eine endrekursive Variante von list-length.

Aufgabe 12.7 Schreibe eine endrekursive Variante von concatenate.


Falls du Hilfsprozeduren auf Listen dafr benutzt, gib auch dafr
endrekursive Definitionen an.
148 Kapitel 12

Aufgabe 12.8 Schreibe endrekursive Varianten von evens und odds aus
Aufgabe 7.3 auf Seite 82. Falls du Hilfsprozeduren auf Listen dafr
benutzt, gib auch dafr endrekursive Definitionen an.

Aufgabe 12.9 Schreiben Sie eine endrekursive Variante von power aus
Aufgabe 7.2 von Seite 82.
13 Bume

Bume sind induktive Datenstrukturen, die viele Anwendungen in der


praktischen Programmierung haben. Sie sind beispielsweise die Grund-
lage fr schnelle Suchverfahren in Datenbanken und einige Datenkom-
pressionsverfahren. Dieses Kapitel demonstriert die Programmierung
mit Binrbumen anhand von Suchbumen.

13.1 Binrbume

Viele Anwendungen von Bumen bauen auf einem Spezialfall auf, den
binren Bumen oder Binrbumen.
Die Binrbaume bilden eine induktiv definierte Menge. Ein Baum ist
eins der folgenden:
Der leere Binrbaum ist ein Binrbaum.
Ein Knoten, bestehend seinerseits aus zwei Binrbumen, genannt lin-
ker und rechter Teilbaum des Knotens und einer beliebigen Markierung
ist ebenfalls ein Binrbaum.

Abbildung 13.1. Binrbaum

Die Markierung an den Knoten kann beliebige Daten aufnehmen, je


nach Verwendungszweck des Baums.
Binrbume haben eine einleuchtende grafische Darstellung, die in
Abbildung 13.1 vorgestellt wird. Die Punkte unten am Baum stehen fr
leere Binrbume. Ein Bild der Form:
steht fr einen Knoten mit Markierung L, unter dem sich ein linker
und ein rechter Teilbaum befinden. Die Teilbume werden auch Zweige
genannt. Knoten, die als linken und rechten Teilbaum jeweils den leeren
Baum haben, heien auch Bltter.
Die Datendefinition fr Bume zeigt klar, da es sich um gemischte
Daten handelt. Fr die leeren Bume kommt ein eigener Record-Typ
zum Einsatz:
; leerer Baum
(define-record-procedures empty-tree
make-empty-tree empty-tree?
())

Auf den ersten Blick scheint hier ein Mibrauch vorzuliegen immerhin
handelt es sich bei leeren Bumen eindeutig nicht um zusammengesetz-
te Daten: Der Record-Typ hat kein einziges Feld. Record-Typen haben
150 Kapitel 13

aber noch die Funktion, neue Datensorten einzufhren, und sind darum
auch dann das Mittel der Wahl fr die Realisierung gemischter Daten,
wenn es sich nicht wirklich um zusammengesetzte Daten handelt. In
diesem Fall reicht es, nur einen leeren Baum zu haben, genauso wie es
nur eine leere Liste gibt:

(define the-empty-tree (make-empty-tree))

Knoten hingegen sind tatschlich zusammengesetzte Daten: Ein Knoten


besteht aus seiner Markierung sowie linkem und rechtem Teilbaum. Da
es Bume ber verschiedenen Sorten von Markierungen gibt, ist die
Signatur fr Knoten parametrisch:

; Ein Knoten besteht aus:


; - Markierung
; - linker Teilbaum
; - rechter Teilbaum
(define-record-procedures-parametric node node-of*
make-node node?
(node-label
node-left-branch node-right-branch))

(define node-of
(lambda (x)
(signature
(node-of* x (tree-of x) (tree-of x)))))

Hier ist die Datendefinition fr Bume im allgemeinen:

; Ein Binrbaum ist eins der folgenden:


; - ein leerer Baum
; - ein Knoten
(define tree-of
(lambda (x)
(signature (mixed empty-tree (node-of x)))))

Damit kann ein Baum wie der in Abbildung 13.1 durch folgenden
Scheme-Ausdruck konstruiert werden:

(: t (tree-of string))
(define t
(make-node
"A"
(make-node
"B"
(make-node
"C"
the-empty-tree
(make-node "D" the-empty-tree the-empty-tree))
the-empty-tree)
(make-node
"E"
(make-node "F" the-empty-tree the-empty-tree)
the-empty-tree)))
Bume 151

Hier sind zwei weitere Beispielbume ber ganzen Zahlen:


(: t1 (tree-of integer))
(define t1
(make-node 3
(make-node 4
the-empty-tree
(make-node 7 the-empty-tree the-empty-tree))
(make-node 8 the-empty-tree the-empty-tree)))

(: t2 (tree-of integer))
(define t2 (make-node 17 (make-node 3 the-empty-tree t1) the-empty-tree))

Als Beispiel fr das Programmieren mit Bumen dient die Tiefe eines
Binrbaums, also die maximale Anzahl von Ebenen im Bild des Bi-
nrbaums. Hier sind Kurzbeschreibung, Signatur, Testflle und Gerst:
; Tiefe eines Baums berechnen
(: depth ((tree-of %a) -> natural))

(check-expect (depth t1) 3)


(check-expect (depth t2) 5)

(define depth
(lambda (tree)
...))

Es geht weiter strikt nach Anleitung: Es handelt sich um gemischte


Daten, also kommt eine Verzweigung zum Einsatz. Da es zwei verschie-
dene Sorten Bume gibt, hat die Verzweigung zwei Zweige:
(define depth
(lambda (t)
(cond
((empty-tree? t)
...)
((node? t)
...))))

Der erste Fall ist einfach: Der leere Baum hat die Tiefe 0. Im zweiten
Fall geht es um Knoten, die wiederum Bume enthalten. Genau wie bei
Listen gibt es also Selbstreferenzen und damit Rekursion:
(define depth
(lambda (t)
(cond
((empty-tree? t)
0)
((node? t)
... (node-label t) ...
... (depth (node-left-branch t)) ...
... (depth (node-right-branch t)) ...))))

Die Markierung spielt keine Rolle fr die Tiefe, kann also wegfallen.
Bei den Teilbumen spielt fr die Tiefe des Knotens nur der tiefere der
152 Kapitel 13

Abbildung 13.2. Ein Suchbaum ber Buchstaben

beiden eine Rolle. Der Knoten ist um eins tiefer als das Maximum der
Tiefen der Teilbume:
(define depth
(lambda (t)
(cond
((empty-tree? t)
0)
((node? t)
(+ 1
(max (depth (node-left-branch t))
(depth (node-right-branch t))))))))

(Max ist eine eingebaute Prozedur in Scheme, die das Maximum ihrer
Argumente ermittelt.)
Auch depth folgt einer Schablone, die fr viele Prozeduren auf Bu-
men gilt; Aufgabe 13.1 beschftigt sich damit. Ihr folgt auch die Pro-
zedur node-count, welche die Anzahl der Knoten in einem Binrbaum
liefert:
; Knoten in Baum zhlen
(: node-count ((tree-of %a) -> natural))

(check-expect (node-count t1) 4)


(check-expect (node-count t2) 6)

(define node-count
(lambda (t)
(cond
((empty-tree? t)
0)
((node? t)
(+ 1
(node-count (node-left-branch t))
(node-count (node-right-branch t)))))))

13.2 Suchbume

Viele Programme bentigen irgendeine Form von Suchfunktionalitt in


einer Menge von Daten: Es knnte sich um die Suche nach einer Telefon-
nummer, einer Primzahl oder einer schon geleisteten Aufgabe handeln.
Eine effiziente Realisierung fr eine Suchfunktionalitt sind Suchbume.
Ein Suchbaum besteht aus Elementen: Neue Elemente knnen einge-
fgt werden, und fr ein gegebenes Element kann festgestellt werden,
ob es im Suchbaum vorhanden ist.
Suchbume setzen eine Gleichheitsoperation und eine totale Ordnung
auf den Elementen (siehe Definition 1.18) voraus.
Sei also S eine total geordnete Menge. Dann ist ein Suchbaum ber S
ein Binrbaum, so da bei jedem Knoten alle Markierungen in seinem
linken Teilbaum kleiner und alle in seinem rechten Teilbaum grer
Bume 153

sind als die Markierung des Knotens selbst. Diese Eigenschaft des
Baums heit auch Suchbaumeigenschaft (bezglich der gewhlten totalen
Ordnung). Abbildung 13.2 zeigt einen Suchbaum ber Buchstaben, die
alphabetisch geordnet sind.
Die Markierung eines Knotens bestimmt, in welchem Teilbaum des
Knotens eine gesuchte Markierung stecken mu, wenn diese nicht
sowieso schon die gesuchte ist: Ist die gesuchte Markierung kleiner
als die des Knotens, so mu sie (wenn berhaupt) im linken Teilbaum
stecken; wenn sie grer ist, im rechten. Insbesondere ist es nicht ntig,
im jeweils anderen Teilbaum nach der Markierung zu suchen.
Fr Suchbume wird ein neuer Record-Typ definiert. Zu einem Such-
baum gehren neben dem Baum selbst auch noch Operationen fr
Gleichheit und die Kleiner-als-Relation auf den Markierungen, beide
reprsentiert durch Prdikate (die zum Binrbaum und zueinander
passen mssen). Genau wie Bume sind auch Suchbume parametrisch:
; Ein Suchbaum besteht aus
; - einer Prozedur, die zwei Markierungen auf Gleichheit testet,
; - einer Prozedur, die vergleicht, ob eine Markierung kleiner als die andere ist
; - einem Binrbaum
(: make-search-tree ((%a %a -> boolean)
(%a %a -> boolean)
(tree-of %a)
-> (search-tree-of %a)))
(: search-tree? (any -> boolean))
(: search-tree-label-equal-proc ((search-tree-of %a) -> (%a %a -> boolean)))
(: search-tree-label-less-than-proc ((search-tree-of %a) -> (%a %a -> boolean)))
(: search-tree-tree ((search-tree-of %a) -> (tree-of %a)))

(define-record-procedures-parametric search-tree search-tree-of*


make-search-tree search-tree?
(search-tree-label-equal-proc
search-tree-label-less-than-proc
search-tree-tree))

(define search-tree-of
(lambda (x)
(signature
(search-tree-of* (x x -> boolean) (x x -> boolean) (tree-of x)))))

Alle Suchbume fangen beim leeren Suchbaum an:


; leeren Suchbaum konstruieren
(: make-empty-search-tree
((%a %a -> boolean) (%a %a -> boolean) -> (search-tree-of %a)))
(define make-empty-search-tree
(lambda (label-equal-proc label-less-than-proc)
(make-search-tree label-equal-proc label-less-than-proc
the-empty-tree)))

Ohne weiterfhrende Prozeduren gibt es hier noch nichts zu testen.


Hier kommt aber schon ein Beispiel zu Testzwecken:

(define s1
154 Kapitel 13

(make-search-tree
= <
(make-node 5
(make-node 3 the-empty-tree the-empty-tree)
(make-node 17
(make-node 10
the-empty-tree
(make-node 12 the-empty-tree the-empty-tree))
the-empty-tree))))

Die nachfolgende Prozedur search-tree-member? stellt fest, ob ein Kno-


ten mit Markierung l in einem Suchbaum s vorhanden ist. Die ei-
gentliche Arbeit macht die lokale Hilfsprozedur member?, die auf dem
zugrundeliegenden Binrbaum operiert. Da member? rekursiv ist, wird
sie mit letrec (siehe Abbildung 12.2) gebunden.
; feststellen, ob Element in Suchbaum vorhanden ist
(: search-tree-member? (%a (search-tree-of %a) -> boolean))

(check-expect (search-tree-member? 3 s1) #t)


(check-expect (search-tree-member? 5 s1) #t)
(check-expect (search-tree-member? 9 s1) #f)
(check-expect (search-tree-member? 10 s1) #t)
(check-expect (search-tree-member? 17 s1) #t)

(define search-tree-member?
(lambda (l s)
(let ((label-equal? (search-tree-label-equal-proc s))
(label-less-than? (search-tree-label-less-than-proc s)))
(letrec
;; member? : tree -> bool
((member?
(lambda (t)
(cond
((empty-tree? t) #f)
((node? t)
(cond
((label-equal? (node-label t) l)
#t)
((label-less-than? l (node-label t))
(member? (node-left-branch t)))
(else
(member? (node-right-branch t)))))))))
(member? (search-tree-tree s))))))

Search-tree-member? packt zunchst die beiden Vergleichsoperationen


label-equal? und label-less-than? aus dem Suchbaum aus. Dann wird
die Hilfsprozedur member? aufgerufen.
Da es zwei Arten Binrbume gibt, folgt member? zunchst der Kon-
struktionsanleitung fr gemischte Daten. Im Zweig fr den leeren Baum
ist die Antwort klar. Im Zweig fr einen Knoten vergleicht member? die
gesuchte Markierung mit der des Knotens. Dabei gibt es drei Mg-
lichkeiten, also auch drei Zweige: Bei Gleichheit ist die Markierung
Bume 155

gefunden. Ansonsten wird member? entweder auf den linken oder den
rechten Teilbaum angewendet, je nachdem, in welchem Teilbaum die
Markierung stehen mu.
Search-tree-member? kann nur richtig funktionieren, wenn das Argu-
ment s tatschlich die Suchbaumeigenschaft erfllt. Rein prinzipiell ist
es mglich, durch Mibrauch von make-search-tree einen Wert vom
Typ search-tree zu erzeugen, der nicht die Suchbaumeigenschaft er-
fllt, wie etwa s2 hier:
(define s2
(make-search-tree
= <
(make-node 5
(make-node 17 the-empty-tree the-empty-tree)
(make-node 3 the-empty-tree the-empty-tree))))

Zu s2 pat das folgende Bild: In diesem Suchbaum fin-


det search-tree-member? zwar die 5, nicht aber die anderen beiden
Elemente:
(search-tree-member? 5 s2)
, #t
(search-tree-member? 17 s2)
, #f
(search-tree-member? 3 s2)
, #f
Aus diesem Grund sollte make-search-tree nur intern verwendet
werden. Ansonsten sollten nur die Prozeduren make-empty-search-tree
und eine neue Prozedur search-tree-insert verwendet werden, die ein
neues Element in den Suchbaum einfgt und dabei die Suchbaumei-
genschaft erhlt. Hier Kurzbeschreibung und Signatur:
; neues Element in Suchbaum einfgen
(: search-tree-insert (%a (search-tree-of %a) -> (search-tree-of %a)))

Fr die Testflle wird ein Suchbaum s3 wie folgt definiert:

(define s3
(search-tree-insert
5
(search-tree-insert
17
(search-tree-insert
3
(make-empty-search-tree = <)))))

Die Testflle werden dann wie zuvor mit Hilfe von search-tree-member?
formuliert:

(check-expect (search-tree-member? 5 s3) #t)


(check-expect (search-tree-member? 17 s3) #t)
(check-expect (search-tree-member? 3 s3) #t)
(check-expect (search-tree-member? 13 s3) #f)
(check-expect (search-tree-member? -1 s3) #f)
156 Kapitel 13

Zu beachten ist, da die Definition von s3 im Programm hinter die


Definition von search-tree-insert gestellt wird, da diese Prozedur fr
die Auswertung der rechten Seite bentigt wird.

(define search-tree-insert
(lambda (l s)
(let ((label-equal? (search-tree-label-equal-proc s))
(label-less-than? (search-tree-label-less-than-proc s)))
(letrec
; (: insert (tree-of %a) -> (tree-of %a))
((insert
(lambda (t)
(cond
((empty-tree? t)
(make-node l the-empty-tree the-empty-tree))
((node? t)
(cond
((label-equal? l (node-label t))
t)
((label-less-than? l (node-label t))
(make-node (node-label t)
(insert (node-left-branch t))
(node-right-branch t)))
(else
(make-node (node-label t)
(node-left-branch t)
(insert (node-right-branch t))))))))))
(make-search-tree
label-equal? label-less-than?
(insert (search-tree-tree s)))))))

Im Herzen von search-tree-insert erledigt die rekursive Hilfsproze-


dur insert die eigentliche Arbeit: Soll l in den leeren Baum eingefgt
werden, so gibt insert einen trivialen Baum der Form zurck.
Wenn t ein Knoten ist, gibt es wieder drei Flle: Wenn l mit der Knoten-
markierung bereinstimmt, so ist es bereits im alten Baum vorhanden
insert kann t unverndert zurckgeben. Ansonsten mu l im linken
oder rechten Teilbaum eingefgt werden, und insert bastelt aus dem
neuen Teilbaum und dem anderen, alten Teilbaum einen neuen Baum
zusammen.
Das Resultat des Aufrufs von insert am Ende der Prozedur wird
schlielich wieder in einen search-tree-Wert eingepackt, mit denselben
label-equal- und label-less-than-Operationen wie vorher.
Die Prozedur search-tree-member? mu fr den Suchbaum in Ab-
bildung 13.2 nicht alle Elemente nach dem gesuchten durchforsten;
search-tree-member? sucht auf direktem Weg von der Wurzel des Such-
baums nach unten zum gesuchten Element. Da pro weiterer Ebene
eines Binrbaums jeweils doppelt soviele Elemente Platz finden als in
der vorhergehenden, wchst die Anzahl der Ebenen des Baums die
Tiefe also nur mit dem Zweierlogarithmus der Anzahl der Elemente,
also viel langsamer als zum Beispiel die Lnge einer Liste, die alle
Elemente aufnehmen mte.
Bume 157

Leider ist nicht jeder Suchbaum so angenehm organisiert wie der


in Abbildung 13.2. Abbildung ?? zeigt einen Binrbaum, der zwar die
Suchbaumeigenschaft erfllt, aber entartet ist: In diesem Suchbaum
dauert die Suche genauso lang wie in einer Liste. Welche Form der
Suchbaum hat und ob er entartet wird, hngt von der Reihenfolge der
Aufrufe von search-tree-insert ab, mit denen er konstruiert wird. Es
gibt allerdings Varianten von Suchbumen, die bei search-tree-insert
die Entartung vermeiden und den Suchbaum balancieren.

13.3 Eigenschaften der Suchbaum-Operationen

Search-tree-insert ist eine der komplizierteren Prozeduren in diesem


Buch; es ist alles andere als offensichtlich, da sie korrekt ist. Die Testfl-
le mgen zwar punktuell auf die Korrektheit von search-tree-insert
und search-tree-member? hindeuten, Sicherheit liefern sie jedoch nicht.
Der erste Schritt, um mehr Vertrauen in die Korrektheit zu gewinnen,
ist die Formulierung von Eigenschaften und deren berprfung mit
check-expect. Der zweite Schritt ist ein Beweis der Suchbaumeigen-
schaft.
Um interessante Eigenschaften zu formulieren, mssen search-tree-insert
und search-tree-member gemeinsam betrachtet werden: search-tree-insert
allein kann nichts sinnvolles anstellen, wenn nach den eingefgten Ele-
menten nicht auch gesucht werden kann. Die wichtigsten Eigenschaften
sind folgende:

1. Ein mit search-tree-insert eingefgtes Element wird stets von search-


tree-member? wieder gefunden.
2. Wenn ein Element nicht mit in den Suchbaum eingefgt wurde, wird
es von search-tree-member? nicht gefunden.

Die erste Eigenschaft ergibt ohne die zweite wenig Sinn: Sie wre auch
erfllt, wenn search-tree-member? immer #t liefern wrde.
Fr einen Test mit for-all mssen beliebige Suchbume betrachtet
werden. Allerdings funktioniert der Ansatz

(for-all ((st (search-tree-of %a)))


...)

nicht, schon weil search-tree-of eine parametrisierte Signatur ist und


damit nicht direkt in for-all verwendet kann. Auerdem sollen die
Suchbume, die betrachtet werden, ja gerade mit search-tree-insert
konstruiert werden.
Im folgenden legen wir uns auf eine bestimmte Parametrisierung der
Suchbume fest, wohl wissend, dass es fr die Suchbaumeigenschaft auf
die Parametrisierung im Grunde nicht ankommt. Der nchste Versuch
knnte deshalb so aussehen:

(for-all ((el natural))


... (search-tree-insert el (make-empty-search-tree = <) ...)

Eine solche Eigenschaft wrde allerdings nur Suchbume mit einem


einzigen Element einbeziehen, also eher uninteressante Vertreter ihrer
158 Kapitel 13

Spezies. Fr substantielle Tests ist es notwendig, Suchbume zu betrach-


ten, die aus unterschiedlichen (insbesondere unterschiedlich langen)
Folgen von search-tree-insert-Operationen entstanden sind. Da fr
die Reprsentation von Folgen in Scheme Listen zustndig sind, bietet
sich eine Eigenschaft folgender Form an:
(for-all ((els (list-of natural)))
...)

Damit es funktioniert, mu nur die Liste els noch in einen Suchbaum


umgewandelt werden, der gerade ihre Elemente enthlt. Das kann eine
Hilfsprozedur namens list->search-tree leisten. Hier sind Kurzbe-
schreibung und Signatur:
; aus allen Zahlen einer Liste einen Suchbaum machen
(: list->search-tree ((%a %a -> boolean)
(%a %a -> boolean) (list-of %a) -> (search-tree-of %a)))

Da list->search-tree nur sukzessive fr alle Elemente der Liste search-tree-insert


aufruft, wird sie am einfachsten mit fold programmiert:
(define list->search-tree
(lambda (= < els)
(fold (make-empty-search-tree = <)
search-tree-insert
els)))

Die erste Eigenschaft ob also jedes mit search-tree-insert in einen


Suchbaum eingefgte Element auch von search-tree-member? gefunden
wird, lt sich jetzt mit Hilfe der Prozedur every? aus Abschnitt 8.3
formulieren. (Zur Erinnerung: every? wendet ein Prdikat auf alle
Elemente einer Liste an und gibt #t zurck, wenn das Prdikat fr jedes
Element #t liefert, sonst #f.)
(check-property
(for-all ((els (list-of natural)))
(let ((st (list->search-tree = < els)))
(every? (lambda (el)
(search-tree-member? el st))
els))))

Es fehlt noch die zweite Eigenschaft: Fr ein Element, das nicht im


Suchbaum vorhanden ist, darf search-tree-member? auch nicht #t lie-
fern. Zum Test der Eigenschaft gehrt also wie schon bei der ersten
Eigenschaft ein beliebiger Suchbaum sowie ein einzelnes Element:
(for-all ((els (list-of natural))
(el natural))
... (list->search-tree = < els) ...)

Der Test ist nur sinnvoll, wenn el nicht Element der Liste els ist: Das
mu erst einmal berprft werden, und zwar durch eine Prozedur, die
testet, ob ein Wert Element einer Liste ist. Hier sind Kurzbeschreibung
und Signatur:
; ist Wert Element einer Liste?
(: member? ((%a %a -> boolean) %a (list-of %a) -> boolean))
Bume 159

Das erste Argument ist ein Gleichheitsprdikat, welches den gesuchten


Wert mit den Listenelementen vergleicht. Hier sind einige Tests, Gerst
und Schablone fr die Prozedur, die eine Liste akzeptiert:

(check-expect (member? = 5 empty) #f)


(check-expect (member? = 5 (list 1 2 3)) #f)
(check-expect (member? = 1 (list 1 2 3)) #t)
(check-expect (member? = 2 (list 1 2 3)) #t)
(check-expect (member? = 3 (list 1 2 3)) #t)

(define member?
(lambda (= el lis)
(cond
((empty? lis) ...)
((pair? lis)
... (first lis)
... (member? = el (rest lis)) ...))))

Im empty?-Zweig ist die Liste leer, das Ergebnis also #f. Im anderen
Fall mu die Prozedur feststellen, ob (first lis) gerade das gesuchte
Element el ist. Vollstndig sieht die Prozedur so aus:

(define member?
(lambda (= el lis)
(cond
((empty? lis) #f)
((pair? lis)
(if (= el (first lis))
#t
(member? = el (rest lis)))))))

Zurck zur Eigenschaft von search-tree-member?: Immer dann, wenn


el nicht Element von els ist, darf auch search-tree-member? el nicht
finden. Diese Implikation wird mit ==> formuliert:

(check-property
(for-all ((els (list-of natural))
(el natural))
(==> (not (member? = el els))
(not (search-tree-member? el (list->search-tree = < els))))))

Die beiden check-property-Tests strken also das Vertrauen in die kor-


rekte Funktionsweise von search-tree-insert und search-tree-member?.
Aber auch hier ist Kontrolle ber einen Beweis der Korrektheit noch bes-
ser: Es lohnt sich, etwas formaler ber die Korrektheit von search-tree-insert
nachzudenken. Zunchst einmal ist es wichtig, zu formulieren, was
der Begriff Korrektheit im Zusammenhang mit search-tree-insert
berhaupt bedeutet:

Satz 13.1 Search-tree-insert erhlt die Suchbaumeigenschaft. Oder mit


anderen Worten: Wenn das search-tree-Argument von search-tree-
insert die Suchbaumeigenschaft erfllt, so erfllt auch der zurckgege-
bene Baum die Suchbaumeigenschaft.
160 Kapitel 13

Beweis Die Korrektheit ist an der Hilfsprozedur insert festgemacht:


Wenn das Argument von insert die Suchbaumeigenschaft erfllt, so
mu auch der Rckgabewert sie erfllen. Der Beweis funktioniert ber
strukturelle Induktion ber den Wert t, der an den Baum gebunden
sei. Im Beweis gibt es vier Flle, die den Zweigen der cond-Formen
entsprechen:

ist der leere Baum. Der dann zurckgegebene Baum der Form
erfllt offensichtlich die Suchbaumeigenschaft.
ist ein Knoten, dessen Markierung mit l bereinstimmt. Dann gibt
insert zurck. Da nach Voraussetzung die Suchbaumeigenschaft
erfllt, ist auch hier die Suchbaumeigenschaft erhalten.
ist ein Knoten, dessen Markierung grer ist als l, sieht also so
aus: wobei sowohl a als auch b selbst die Suchbaumeigenschaft
erfllen. In diesem Fall sieht der entstehende Baum folgendermaen
aus: Per Induktionsannahme erfllt (insert d ae) die Suchbau-
meigenschaft. Da b auch die Suchbaumeigenschaft erfllt, mu nur
noch gezeigt werden, da alle Markierungen in (insert d ae) kleiner
sind als m. Es gibt in insert drei Aufrufe von make-node, die neue
Knoten erzeugen knnen. Alle fgen hchstens l zu der Menge der
Markierungen des Baumes hinzu. Alle anderen Markierungen sind
nach Voraussetzung kleiner als m, ebenso wie l. Das Resultat erfllt
also ebenfalls die Suchbaumeigenschaft.
Im vierten Fall ist ein Knoten, dessen Markierung kleiner ist als l.
Dieser Fall geht analog zum dritten Fall.


Aufgaben

Aufgabe 13.1 Formulieren Sie eine spezielle Schablone fr Prozeduren,


die Binrbume akzeptieren!

Aufgabe 13.2 Schreiben Sie eine Prozedur, die einen Binrbaum akzep-
tiert und eine Liste aller Markierungen in dem Baum zurckgibt.

Aufgabe 13.3 Wie mu search-tree-insert aufgerufen werden, um


den Suchbaum in Abbildung 13.2 zu erzeugen? Wie mu search-tree-
insert aufgerufen werden, um den Suchbaum in Abbildung ?? zu
erzeugen?

Aufgabe 13.4 Schreiben Sie eine Prozedur search-tree-delete, die ein


Element aus einem Suchbaum entfernt. Beweisen Sie, da die Prozedur
die Suchbaumeigenschaft erhlt.

Aufgabe 13.5 Die Implementierung von Suchbumen ist fr viele Such-


probleme nicht mchtig genug, da search-tree-member? nur berprft,
ob ein Element in einem Suchbaum vorhanden ist. Das hilft nicht viel
z.B. beim Suchen von Telefonnummern zu gegebenen Namen. Erwei-
tern Sie die Implementierung so, da sie auch z.B. zum Suchen von
Telefonnummern verwendet werden kann. Realisieren Sie exemplarisch
das Suchen nach Telefonnummern!
Bume 161

Hinweis: Benutzen Sie als Markierungen im Suchbaum sogenann-


te Eintrge, die aus eine Schlssel (z.B. dem Namen) und dem Wert
bestehen. Schreiben Sie dazu parametrische Daten-, Record- und Si-
gnaturdefinitionen. ndern Sie search-tree-insert dahingehend, da
es Schlssel und Element akzeptiert. Schreiben Sie eine Prozedur
search-tree-find, die zu einem Schlssel den zugehrigen Wert findet.

Aufgabe 13.6 Beweisen Sie die Korrektheit von search-tree-member?.


Formulieren Sie zunchst eine geeignete Korrektheitseigenschaft und
beweisen Sie diese mit Hilfe von Induktion!
14 Schrittweise Verfeinerung

In der Praxis gelingt es nur selten, Programme erst vollstndig zu pla-


nen und dann umzusetzen: Manchmal ist die Aufgabenstellung bei
Entwicklungsbeginn noch nicht vollstndig bekannt. Manchmal ist die
Aufgabenstellung zwar bekannt aber unbersichtlich. Manchmal ndert
sich die Aufgabenstellung nachtrglich. Deshalb mu die Entwicklung
oft beginnen, bevor die gesamte Planung steht. Das Programm wird
dann bis zu einem bestimmten Grad entwickelt und dann schrittweise
verfeinert, um die noch vorher unbekannten Aspekte der Aufgaben-
stellung abzudecken. Dabei ist es wichtig, zwischenzeitlich gemachte
Annahmen klar herauszustellen, fehlerhafte Programmteile auch wieder
bereitwillig zu lschen, und Schnittstellen gegen nderungen zu isolie-
ren. In diesem Kapitel werden verschiedene Beispiele und Techniken
fr diese schrittweise Verfeinerung vorgestellt.

14.1 Lschen in Suchbumen

Das Lschen eines Elements in einem Suchbaum ist deutlich schwieri-


ger als das Einfgen. Beim Suchbaum s3 erscheint die Aufgabe noch
einfach. Hier das Bild dazu: Angenommen, die 3 soll ge-
lscht werden. In diesem Fall kann der Suchbaum durch dessen rechten
Teilbaum ersetzt werden: Angenommen, die 17 soll gelscht
werden: Dann kann der Teilbaum mit 17 an der Spitze durch seinen
eigenen linken Teilbaum ersetzt werden: hnlich kann der
Teilbaum mit der 5 an der Spitze durch den leeren Baum ersetzt wer-
den: Wird der Suchbaum nur minimal erweitert, wird die Sache
schon schwieriger: Wie knnte hier die 17 gelscht werden? Es ist
nicht mglich, den Teilbaum mit 17 an der Spitze einfach durch seinen
linken oder rechten Teilbaum zu ersetzen, weil dann der jeweils andere
Teilbaum unter den Tisch fiele. Es ist also notwendig, die Baumstruktur
zu reorganisieren. Mit dieser Beoachtung ist klar, da die Aufgabe nicht
einfach dadurch zu lsen ist, da die Konstruktionsanleitung einmal
befolgt wird: Stattdessen empfiehlt es sich, etwas nachzudenken und
zumindest eine grobe Lsungsstrategie zu entwickeln, bevor mit dem
Programmieren begonnen wird.
Eine Mglichkeit fr eine Lsung wre, den kompletten Suchbaum
neu zu bilden: Dazu knnte die Lsch-Prozedur alle Elemente in ei-
ner Liste aufsammeln, und dann mit search-tree-insert sukzessive
aus allen Elementen bis auf das zu lschende einen neuen Suchbaum
konstruieren. (Siehe Aufgabe 14.1.) Dies wre allerdings eine zeitauf-
wendige Methode, die berproportional lnger dauert, je grer der
164 Kapitel 14

Suchbaum ist.
Besser wre also, wenn beim Lschen soviel wie mglich des rest-
lichen Baums intakt bleibt: das soll fr die berlegungen erst einmal
die Maxime sein. Hypothetisch wre es mglich, da dieser Ansatz
nicht zum Ziel fhrt und diese Annahme wieder rckgngig gemacht
werden mu. Darum ist es sinnvoll, die Annahme deutlich sichtbar zu
dokumentieren:
Annahme: Es ist mglich, ein Element zu lschen, indem nur der Teilbaum verndert
wird, an dessen Wurzel das Element sitzt; der Rest soll gleichbleiben.

Etwas Nachdenken ergibt, da der oben beschriebene Ansatz, nur den


Teilbaum zu ersetzen, an dessen Spitze das zu lschende Element steht
und den restlichen Baum unverndert zu lassen, auch hier funktioniert.
Es gibt zwei Mglichkeiten, dies zu tun: Mit anderen Worten, es
wird ein Element von weiter unten hochgezogen. Es bleibt die Frage,
wie die Prozedur das Element auswhlen knnte, das hochgezogen
werden mu. Offensichtlich kann entweder ein Element aus dem linken
oder aus dem rechten Teilbaum ausgewhlt werden. Ideal wre, wenn
der jeweils andere Teilbaum intakt bliebe. Es mu also ein Element so
hochgezogen werden, da die Suchbaumeigenschaft nicht verletzt wird.
Angenommen, das Element soll aus dem linken Teilbaum genommen
werden: Dann mu es grer sein als alle anderen Elemente des linken
Teilbaums. (Es mu auch kleiner als alle Elemente des rechten Teilbaums
sein, aber dies ist automatisch gegeben, weil die Suchbaumeigenschaft
im ursprnglichen Baum bereits gilt.) Das hochzuziehende Element
des linken Teilbaums wre also das Maximum aller Elemente des linken
Teilbaums. (Umgekehrt wre es genauso mglich, das Minimum der
Elemente des rechten Teilbaums auszuwhlen: reine Geschmacksfrage.)
Zu diesem Zeitpunkt stellen sich mglicherweise bereits Kopfschmer-
zen beim Programmierer ein: Wie wird das hochzuziehende Element
gefunden? Wie funktioniert der Hochziehproze selbst? Da diese Auf-
gaben lsbar sind, ist klar, aber zusammen mit den anderen zu lsenden
Teilaufgaben vom Anfang dieses Abschnitts ist es vielleicht etwas viel
auf einmal.
Darum ist es sinnvoll, erst einmal die schon klaren Teile der Aufga-
benlsung umzusetzen und die noch unklaren Teile per Wunschdenken
auf spter zu verschieben. Ein Teil der Strategie ist bereits vollstndig
erkennbar: Das Lschen funktioniert, indem der Teilbaum, an dessen
Wurzel das zu lschende Element klebt, durch einen anderen Baum
ersetzt wird; der Rest des Baums ist intakt. Es gibt also bereits zwei
erkennbare Teilaufgaben:
1. Den zu ersetzenden Teilbaum finden.
2. Aus einem Baum mit einem zu lschenden Element an der Spitze
einen anderen Baum machen, der das Element nicht mehr enthlt.
Insbesondere mte es leicht mglich sein, die zweite Teilaufgabe soweit
zu lsen, da die Lsch-Prozedur zu zumindest fr s3 funktioniert:
; Element aus Suchbaum lschen
(: search-tree-delete (%a (search-tree %a) -> (search-tree %a)))

(check-expect (search-tree-tree (search-tree-delete 3 s3))


Schrittweise Verfeinerung 165

(make-node 17
(make-node 5 the-empty-tree the-empty-tree)
the-empty-tree))
(check-expect (search-tree-tree (search-tree-delete 5 s3))
(make-node 3
the-empty-tree
(make-node 17 the-empty-tree the-empty-tree)))
(check-expect (search-tree-tree (search-tree-delete 17 s3))
(make-node 3 the-empty-tree (make-node 5 the-empty-tree the-empty-tree)))

(check-expect (search-tree-tree (search-tree-delete 28 s3))


(search-tree-tree s3))

Das Gerst ist das gleiche, das schon fr search-tree-member? und


search-tree-insert entwickelt wurde. Die zwei Hilfsprozeduren wer-
den mit letrec gebunden:

(define search-tree-delete
(lambda (e s)
(let ((=proc (search-tree-label-equal-proc s))
(<proc (search-tree-label-less-than-proc s)))
(letrec
; Element finden und lschen
; (: delete ((tree %a) -> (tree %a))
((delete
(lambda (t)
...))
; Element an Wurzel lschen
; (: delete-top ((node %a (tree %a) (tree %a)) -> (tree %a))
(delete-top
(lambda (n)
...)))
(make-search-tree
=proc <proc
(delete (search-tree-tree s)))))))

Die Hilfsprozedur delete folgt dabei dem gleichen Muster wie die
Hilfsprozeduren in search-tree-member? und search-tree-insert: Die
rekursiven Aufrufe folgen der Struktur des Baums gem der Such-
baumeigenschaft. Wenn des gesuchte Element gefunden ist, kann die
Hilfsprozedur delete-top den Rest der Arbeit bernehmen. Bei delete-top
sind bereits zwei Flle klar: Wenn ein Teilbaum leer ist, so kann der
Baum durch den jeweils anderen Teilbaum ersetzt werden:

(define search-tree-delete
(lambda (e s)
(let ((=proc (search-tree-label-equal-proc s))
(<proc (search-tree-label-less-than-proc s)))
(letrec
; Element finden und lschen
; (: delete ((tree %a) -> (tree %a))
((delete
(lambda (t)
166 Kapitel 14

(cond
((empty-tree? t) t)
((node? t)
(cond
((=proc e (node-label t))
(delete-top t))
((<proc e (node-label t))
(make-node (node-label t)
(delete (node-left-branch t))
(node-right-branch t)))
(else
(make-node (node-label t)
(node-left-branch t)
(delete (node-right-branch t)))))))))
; Element an Wurzel lschen
; (: delete-top ((node %a (tree %a) (tree %a)) -> (tree %a))
(delete-top
(lambda (n)
(cond
((empty-tree? (node-left-branch n))
(node-right-branch n))
((empty-tree? (node-right-branch n))
(node-left-branch n))
(else ...)))))
(make-search-tree
=proc <proc
(delete (search-tree-tree s)))))))

Die Testflle mit s3 funktionieren damit schon einmal. Es ist durchaus


sinnvoll, bewut die einfachen Flle zuerst zu programmieren und auch
zu testen. Gibt es spter Probleme bei den komplizierteren Fllen, so
sind diese einfacher zu lokalisieren.
Der folgende Testfall allerdings beschwert sich noch ber den fehlen-
den Code im else-Zweig:

(define s4
(make-search-tree
= <
(make-node 17
(make-node 5 the-empty-tree the-empty-tree)
(make-node 20 the-empty-tree the-empty-tree))))

(check-expect (search-tree-tree (search-tree-delete 17 s4))


(make-node 5 the-empty-tree
(make-node 20 the-empty-tree the-empty-tree)))

Fr die Ellipse mu nun also das maximale Element des linken Teil-
baums hochgezogen werden; der rechte Teilbaum bleibt unangetastet.
Das knnte etwa so aussehen:

(make-node (find-maximum (node-left-branch n))


...
(node-right-branch n))
Schrittweise Verfeinerung 167

Per Wunschdenken wird hier eine Prozedur find-maximum vorausge-


setzt:
; Maximum von Teilbaum finden
(: find-maximum ((tree %a) -> %a))

Aber auch im obigen Ausdruck fehlt noch etwas, nmlich der lin-
ke Teilbaum des neu konstruierten Baums. Der soll alle Elemente
des ursprnglichen Teilbaums finden, auer dem Maximum. Wie kann
delete-top das Maximum loswerden? Am einfachsten durch die Ver-
wendung der Lsch-Prozedur, die gerade geschrieben wird. Erster
Anlauf:
(make-node (find-maximum (node-left-branch n))
(search-tree-delete
(find-maximum (node-left-branch n))
(node-left-branch n))
(node-right-branch n))

Hier taucht (find-maximum (node-left-branch n)) mehrfach auf, es soll-


te also durch ein let gebunden werden:
(let ((el (find-maximum (node-left-branch n))))
(make-node el
(search-tree-delete
el
(node-left-branch n))
(node-right-branch n)))

Leider ist der Code so nicht richtig, da (node-left-branch n) kein


search-tree-Wert ist, sondern nur ein tree-Wert: es gibt also eine
Vertragsverletzung. Der Baum (node-left-branch n) mu also noch
in ein search-tree-Record eingewickelt werden. Auerdem kommt
bei search-tree-delete wieder ein search-tree-Wert heraus, whrend
make-node einen tree-Wert erwartet, der hinterher wieder ausgewickelt
werden mu:

(let ((el (find-maximum (node-left-branch n))))


(make-node el
(search-tree-tree
(search-tree-delete
el
(make-search-tree =proc <proc
(node-left-branch n))))
(node-right-branch n)))

Alternativ zum umstndlichen Ein- und Auswickeln kann auch die


delete-Hilfsprozedur um einen Parameter fr das zu lschende Element
erweitert werden:
(letrec
; Element finden und lschen
; (: delete (%a (tree %a) -> (tree %a)))
((delete
(lambda (e t)
168 Kapitel 14

(cond
((empty-tree? t) t)
((node? t)
(cond
((=proc e (node-label t))
(delete-top t))
((<proc e (node-label t))
(make-node (node-label t)
(delete e (node-left-branch t))
(node-right-branch t)))
(else
(make-node (node-label t)
(node-left-branch t)
(delete e (node-right-branch t)))))))))

Dann lautet der Code im else-Zweig:

(let ((el (find-maximum (node-left-branch n))))


(make-node el
(delete el (node-left-branch n))
(node-right-branch n)))

Fehlt nur noch die Hilfsprozedur find-maximum. Beim Ermitteln des


Maximums hilft die Suchbaumeigenschaft: Es ist einfach das Element,
das am weitesten rechts im Baum steht. Die Prozedur ist also ganz
einfach zu schreiben. Komplett sieht search-tree-delete dann so aus:

(define search-tree-delete
(lambda (e s)
(let ((=proc (search-tree-label-equal-proc s))
(<proc (search-tree-label-less-than-proc s)))
(letrec
; Element finden und lschen
; (: delete (%a (tree %a) -> (tree %a))
((delete
(lambda (e t)
(cond
((empty-tree? t) t)
((node? t)
(cond
((=proc e (node-label t))
(delete-top t))
((<proc e (node-label t))
(make-node (node-label t)
(delete e (node-left-branch t))
(node-right-branch t)))
(else
(make-node (node-label t)
(node-left-branch t)
(delete e (node-right-branch t)))))))))
; Element an der Wurzel lschen
; (: delete-top ((node %a (tree %a) (tree %a)) -> (tree %a))
(delete-top
Schrittweise Verfeinerung 169

(lambda (n)
(cond
((empty-tree? (node-left-branch n))
(node-right-branch n))
((empty-tree? (node-right-branch n))
(node-left-branch n))
(else
(let ((el (find-maximum (node-left-branch n))))
(make-node el
(delete el (node-left-branch n))
(node-right-branch n)))))))
; Maximum von Teilbaum finden
; (: find-maximum ((tree %a) -> %a))
(find-maximum
(lambda (t)
(cond
((empty-tree? t)
(violation "unerwarteter leerer Baum"))
((node? t)
(let ((right (node-right-branch t)))
(if (empty-tree? right)
(node-label t)
(find-maximum (node-right-branch t)))))))))

(make-search-tree
=proc <proc
(delete e (search-tree-tree s)))))))

Beim Testen fllt auf, da der rekursive Aufruf von find-maximum noch
nicht abgedeckt ist. (Da der Aufruf von violation nicht abgedeckt ist,
ist erwnscht es soll ja nicht passieren.) Es mu also noch ein Testfall
konstruiert werden:
(define s4
(make-search-tree
= <
(make-node 17
(make-node 5 the-empty-tree the-empty-tree)
(make-node 20 the-empty-tree the-empty-tree))))

(check-expect (search-tree-tree (search-tree-delete 17 s4))


(make-node 5 the-empty-tree
(make-node 20 the-empty-tree the-empty-tree)))

14.2 Datenverfeinerung

Bei realen Programmieraufgaben sind selten schon przise Datende-


finitionen vorgegeben: Stattdessen liegen oft nur ein paar vage Anfor-
derungen an die Software vor, die sich unter Umstnden auch noch
mit der Zeit ndern. In solchen Fllen ist es nicht mglich, wie bei den
meisten bungsaufgaben dieses Buches, die Datendefinition direkt an
der Aufgabenstellung abzulesen und alle zu anfallenden Probleme mit
einem Wurf zu lsen.
170 Kapitel 14

Das Beispiel dieses Abschnitts ist, die Verwaltungsdaten von Woh-


nungen zu modellieren. ber eine Wohnung lt sich viel sagen, von
dem nur manches je nach Kontext und Aufgabe wichtig ist. Darum
ist es schwierig, im ersten Anlauf eine vollstndige und przise Da-
tendefinition zu entwerfen. In diesem Abschnitt geht es darum, was
passiert, wenn sich die Anforderungen und damit die Datendefinitionen
ndern. Angenommen, es geht zunchst einmal vage um die Adresse,
die Bewohner, die Zimmer und die Wohnflche. Immerhin sind daran
schon eine Daten- und eine Record-Definition ablesbar:

; Eine Wohnung besteht aus:


; - Adresse
; - Bewohner
; - Zimmern
(define-record-procedures apartment
make-apartment apartment?
(apartment-address apartment-resident apartment-rooms))

(: make-apartment (string string (list room) -> apartment))

Hier ist allerdings bereits eine willkrliche Entscheidung gefallen: Die


Datendefinition enthlt nicht direkt die Grundflche der Wohnung.
Diese soll pro Zimmer angegeben werden. Der Vertrag room fr Zimmer
ist noch offen; da ber die Zimmer auer der Grundflche nichts weiter
bekannt ist, wre eine Mglichkeit die folgende Definition:

(define-contract room rational)

Diese naive Definition hat zwei Nachteile:

Es gibt unmittelbar kein eindeutiges Prdikat fr Zimmer.


Es ist klar, da sich ber ein Zimmer noch mehr Dinge sagen lassen
als die Grundflche: Frher oder spter werden Zimmer zu zusam-
mengesetzten Daten.

Aus diesen Grnden empfiehlt es sich, von vornherein von zusammen-


gesetzten Daten auszugehen:

; Ein Zimmer besteht aus:


; - Flche
(define-record-procedures room
make-room room?
(room-area))

(: make-room (rational -> room))

Hier sind zwei Wohnungen fr Tests:

(define a1
(make-apartment
"Entenhausener Strae 15"
"Sperber"
(list (make-room 30) (make-room 50) (make-room 60))))
Schrittweise Verfeinerung 171

(define a2
(make-apartment
"Froschgasse 17"
"Crestani"
(list (make-room 20) (make-room 40))))

Die Gesamtflche einer Wohnung lt sich einfach durch Addieren der


Flchen der Zimmer berechnen:
; Flche einer Wohnung berechnen
(: apartment-area (apartment -> number))
(check-expect (apartment-area a1) 140)
(check-expect (apartment-area a2) 60)

(define apartment-area
(lambda (a)
(fold 0 +
(map room-area (apartment-rooms a)))))

An dieser Stelle wird eine positive Eigenschaft der bisherigen Daten-


und Prozedurdefinitionen deutlich. Angenommen, die Record-Definition
htte die Grundflche direkt einer Wohnung zugeordnet:
; Eine Wohnung besteht aus:
; - Adresse
; - Bewohner
, - Grundflche
; - Zimmern
(define-record-procedures apartment
make-apartment apartment?
(apartment-address apartment-resident apartment-rooms apartment-area))

(: make-apartment (string string (list room) rational -> apartment))

In diesem Fall wrde der Selektor apartment-area sich nach auen


genauso verhalten wie die Prozedur apartment-area gleicher Vertrag.
gleiche Funktion. Die Tatsache, da der Record-Selektor eine regulre
Prozedur ist, die sich von selbstgeschriebenen Prozeduren nach auen
nicht unterscheidet, isoliert Benutzer der Wohnungs-Funktionalitt von
den Unterschieden zwischen den zwei Varianten.
Aus Sicht eines Wohnungsbesitzers interessieren mglicherweise
noch weitere Daten, zum Beispiel, ob ein Bewohner noch Untermieter
hat. Es ist sogar mglich, da Untermieter ihrerseits selbst Untermieter
haben. Dieser Idee liee sich Rechnung tragen, indem eine Wohnung
statt aus Zimmern aus Abschnitten besteht, und jeder Abschnitt
entweder ein Zimmer oder ein untervermieter Wohnungsteil ist. Die
Definition fr Wohnungen mte also folgendermaen gendert wer-
den:
; Eine Wohnung besteht aus:
; - Adresse
; - Bewohner
; - Abschnitten
(define-record-procedures apartment
172 Kapitel 14

make-apartment apartment?
(apartment-address apartment-resident apartment-sections))

(: make-apartment (string string (list section) -> apartment))

Bei Abschnitten (section) handelt es sich klar um gemischte Daten:


; Ein Abschnitt ist eins der folgenden:
; - ein Zimmer
; - ein untervermieter Teil der Wohnung
(define-contract section (mixed room sublet))

Zimmer sind wie gehabt. Untervermietete Wohnungsteile sind wie


Wohnungen, haben aber keine Adresse:
; Ein unvermieteter Teil des Wohnung besteht aus:
; - Bewohner
; - Abschnitten
(define-record-procedures sublet
make-sublet sublet?
(sublet-resident sublet-sections))

(: make-sublet (string (list section) -> sublet))

Hier ist ein Beispiel fr solche einen untervermieteten Teil sowie ein
Apartment, das es enthlt:
(define s1
(make-sublet "Taschenbier"
(list (make-room 20)
(make-sublet "Sams"
(list (make-room 2)))
(make-room 10))))

(define a3
(make-apartment
"Flugentenschneise 17"
"Rotkohl"
(list (make-room 100)
s1
(make-room 30))))

Es kann nun sein, da die Wohnungs-Funktionalitt schon in ein


greres Programm eingebaut wurde, da dessen Prozeduren benutzt,
insbesondere den Selektor apartment-rooms. Der ist im Zuge der Er-
weiterung um Untermiete unter den Tisch gefallen. Das Konzept der
Zimmer einer Wohnungs ist aber immer noch sinnvoll; entsprechend ist
es mglich, apartment-rooms wieder zur Verfgung zur stellen, diesmal
als normale Prozedur mit einer Hilfsprozedur, um die Zimmer eines
Abschnitts aufzuzhlen:
; Zimmer einer Wohnung aufzhlen
(: apartment-rooms (apartment -> (list room)))

(check-expect (apartment-rooms a1)


Schrittweise Verfeinerung 173

(list (make-room 30) (make-room 50) (make-room 60)))


(check-expect (apartment-rooms a3)
(list (make-room 100)
(make-room 20)
(make-room 2)
(make-room 10)
(make-room 30)))

(define apartment-rooms
(lambda (a)
(fold empty append
(map section-rooms (apartment-sections a)))))

; Zimmer eines Wohnungsabschnitts aufzhlen


(: section-rooms (section -> (list room)))

(check-expect (section-rooms (make-room 30))


(list (make-room 30)))
(check-expect (section-rooms s1)
(list (make-room 20)
(make-room 2)
(make-room 10)))

(define section-rooms
(lambda (s)
(cond
((room? s) (list s))
((sublet? s)
(fold empty append
(map section-rooms (sublet-sections s)))))))

Bei der Abspaltung von Hilfsprozeduren gibt es Ermessensspielraum:


Die Hilfsprozedur section-rooms ist notwendig, weil sie rekursiv ist.
Von auen ist kein Unterschied zwischen dem alten Selektor apartment-rooms
und der neuen Prozedur zur erkennen die Schnittstelle ist gleich ge-
blieben: Dies ist ein wichtiger Beitrag zur Modularitt, die es erlaubt,
nderungen an der Wohnungs-Funktionalitt vorzunehmen, ohne da
andere Programmteile, welche die Funktionalitt benutzen, verndert
werden mssen.
Zu den Programmteilen, die apartment-rooms verwenden, gehrt
auch apartment-area, und tatschlich funktioniert apartment-area ohne
Anpassungen wie vorher. Auerdem funktioniert es auch bei Wohnun-
gen mit Untermietern, wie ein zustzlicher Testfall ermittelt:

(check-expect (apartment-area a3) 162)

Mit den Erweiterungen an den Datendefinitionen sind allerdings auch


neue Funktionalitten mglich. Zum Beispiel knnte eine Prozedur fr
die Bewohner einer Wohnung przise Adressen ausrechnen, etwa so:

; Adresse eines Bewohners berechnen


(: resident-address (string apartment -> string))
174 Kapitel 14

(check-expect (resident-address "Sperber" a1) "Sperber")

(check-expect (resident-address "Taschenbier" a3)


"Taschenbier bei Rotkohl")
(check-expect (resident-address "Sams" a3)
"Sams bei Taschenbier bei Rotkohl")

Die Prozedur mu also gleichzeitig nach dem Bewohner suchen und


die Adreangabe konstruieren. Wenn der gesuchte Bewohner gerade
der Hauptbewohner der Wohnung ist, ist die Aufgabe einfach:
(define resident-address
(lambda (r a)
(if (string=? r (apartment-resident a))
r
...)))

Damit funktioniert bereits der erste Testfall.


Die anderen Adreangaben haben jetzt alle die Form x bei y wobei
y der Bewohner der Wohnung ist:
(define resident-address
(lambda (r a)
(if (string=? r (apartment-resident a))
r
(string-append ... " bei " (apartment-resident a)))))

Der Prfix x hngt vom Wohnabschnitt ab, in dem der gesuchte Bewoh-
ner wohnt. Per Wunschdenken sei eine Prozedur vorausgesetzt, die in
den Abschnitten einer Wohnung nach dem Bewohner sucht und den
entsprechenden Prfix berechnet:
; Adre-Prfix eines Abschnitts-Bewohners berechnen
(: sections-prefix (string (list section) -> string))

Mit Hilfe dieser Prozedur kann resident-address erweitert werden:


(define resident-address
(lambda (r a)
(if (string=? r (apartment-resident a))
r
(string-append (sections-prefix r (apartment-sections a))
" bei "
(apartment-resident a)))))

Nun zur Prozedur sections-prefix. Hier ein geeigneter Testfall:


(check-expect (sections-prefix "Taschenbier" (apartment-sections a3))
"Taschenbier")

Gerst und Schablone:


(define sections-prefix
(lambda (r lis)
(cond
((empty? lis) ...)
((pair? lis)
... (first lis) ...
... (sections-prefix r (rest lis)) ...))))
Schrittweise Verfeinerung 175

Beim Ausfllen der Schablone wird sofort klar, da ein geeigneter


Rckgabewert fr den empty?-Zweig noch fehlt: In diesem Fall ist der
Bewohner gar nicht gefunden worden. Fr diesen Umstand (der in den
berlegungen bisher noch gar nicht auftauchte) mu ein Extra-Wert
eingefhrt werden:

; Wert fr nicht gefundenen Bewohner


(define-record-procedures not-found
make-not-found not-found?
())

(define the-not-found (make-not-found))

Entsprechend mu der Vertrag von sections-prefix erweitert werden:


(: sections-prefix (string (list section) -> (mixed string not-found)))

Da es sich bei (first lis) um gemischte Daten handelt, wird eine


Verzweigung fllig. Wieder wird per Wunschdenken eine Hilfsprozedur
angenommen, diesmal sublet-prefix, die den Adre-Prfix fr eine
Untermiete ausrechnet, mglich:
(define sections-prefix
(lambda (r lis)
(cond
((empty? lis) the-not-found)
((pair? lis)
(let ((f (first lis)))
(cond
((room? f) (sections-prefix r (rest lis)))
((sublet? f)
(let ((prefix (sublet-prefix r f)))
(cond
((not-found? prefix)
(sections-prefix r (rest lis)))
((string? prefix)
prefix))))))))))

Die Hilfsprozedur sublet-prefix soll sich folgendermaen verhalten:


; Adre-Prfix eines Untermieters bewohnen
(: sublet-prefix (string sublet -> (mixed string not-found)))

(check-expect (sublet-prefix "Taschenbier" s1) "Taschenbier")


(check-expect (sublet-prefix "Sams" s1) "Sams bei Taschenbier")
(check-expect (sublet-prefix "Merz" s1) the-not-found)

Bei Untervermietungen ist die Arbeit dann erledigt, wenn der Bewohner
gerade der gesuchte ist:

(define sublet-prefix
(lambda (r s)
(if (string=? r (sublet-resident s))
r
...)))
176 Kapitel 14

Dies sieht hnlich aus wie bei apartment-prefix; auch hier funktio-
niert schon der erste Testfall. Fr die Alternative kann sections-prefix
benutzt werden:
(define sublet-prefix
(lambda (r s)
(if (string=? r (sublet-resident s))
r
(let ((prefix (sections-prefix r (sublet-sections s))))
(cond
((not-found? prefix) the-not-found)
((string? prefix)
(string-append prefix " bei " (sublet-resident s))))))))

Damit sieht das Programm fast fertig aus. Allerdings ist der nde-
rung im Vertrag von sections-prefix noch nicht berall Rechnung
getragen worden, namentlich nicht in resident-address: Einerseits
mu resident-address eine Fallunterscheidung fr den Rckgabewert
von sections-prefix einfhren; dies heit andererseits aber auch, da
resident-address selbst in die Lage kommen kann, da der gesuchte
Bewohner nicht gefunden wird. Damit werden neben der Erweiterung
des Rumpfes der Prozedur auch eine nderung des Vertrags und ein
neuer Testfall fllig:
(: resident-address (string apartment -> (mixed string not-found)))
(check-expect (resident-address "Mller" a1) the-not-found)

(define resident-address
(lambda (r a)
(if (string=? r (apartment-resident a))
r
(let ((prefix (sections-prefix r (apartment-sections a))))
(cond
((not-found? prefix)
the-not-found)
((string? prefix)
(string-append prefix
" bei "
(apartment-resident a))))))))

Solche nderungen an Vertrgen, die im Rahmen der schrittweisen


Verfeinerung auftreten, sind wie Wellen im Teppichboden: Sie mssen
durch die gesamte Ausdehnung des Programms weitergegeben werden,
bis sie schlielich am Rand angekommen sind.
Am Beispiel der Wohnungsverwaltung werden einige typische Ein-
sichten ber die Software-Entwicklung deutlich:
Einmal begonnene Software wird fast immer spter erweitert und
fr Aufgaben eingesetzt, fr die sie ursprnglich nicht gedacht war.
Darum ist es sinnvoll, von vornherein die Mglichkeit spterer Er-
weiterungen zu bercksichtigen
Auch wenn scheinbar das gesamte Problem vor dem Beginn der
Programm-Entwicklung bekannt ist, treten hufig noch beim Pro-
grammieren neue Einsichten zutage, die Anpassungen erfordern.
Schrittweise Verfeinerung 177

nderungen an Vertrgen also Schnittstellen ziehen hufig andere


nderungen in Prozeduren nach sich, die diese benutzen. Es ist
wichtig, diese gewissenhaft durchzufhren.
In Mantra-Form sehen diese Einsichten so aus:

Mantra 11 Programme wachsen.

Mantra 12 Es ist nicht mglich, fr alle Eventualitten im voraus zu


planen.

Mantra 13 nderungen an Schnittstellen mssen vollstndig durch ein


Programm propagiert werden.

Aufgaben

Aufgabe 14.1 Eine alternative Implementierung von search-tree-delete


funktioniert folgendermaen:

1. Alle Elemente des Baums werden in einer Liste aufgezhlt.


2. Das zu lschende Element wird aus der Liste entfernt.
3. Ein vllig neuer Suchbaum aus wird aus der bereinigten Liste kon-
struiert.
Realisieren Sie diese Alternative! Wie unterscheidet sie sich in der
Laufzeit von der im Text prsentierten Implementierung?

Aufgabe 14.2 Zwischen den Record-Definitionen von apartment und


sublet gibt es Gemeinsamkeiten. Ist es mglich, von diesen Gemein-
samkeiten zu profitieren und den Code zu vereinfachen? ndern Sie
den Code entsprechend und beurteilen Sie, ob es sich gelohnt hat.
15 Zuweisungen und Zustand

TBD
16 Der -Kalkl

Nachdem die bisherigen Kapitel den Bogen von der praktischen Kon-
struktion einfacher Programme bis zur objektorientierten Program-
mierung geschlagen haben, beleuchtet dieses Kapitel eine wichtige
theoretische Grundlage der Programmierung.
Fr die Informatik ist der Begriff des logischen Kalkls von zentraler
Bedeutung. Ein Kalkl dient dazu, auf formale Art und Weise wahre
Aussagen abzuleiten, ohne da es dabei ntig wird, ber den Sinn
der Aussagen nachzudenken. Der -Kalkl ist ein logischer Kalkl,
der als die Basis fr eine formale Beschreibung des Verhaltens von
Computerprogrammen dient. Scheme baut direkt auf dem -Kalkl
auf: es ist kein Zufall, da das Schlsselwort fr die Abstraktion lambda
heit. Es gibt noch viele weitere Einsatzgebiete fr den -Kalkl, ins-
besondere bei der Konstruktion von besonders effizienten bersetzern
fr Programmiersprachen, in der Logik und der Linguistik, und bei
der Entwicklung und Sicherheitsberprfung von mobilem Code im
Internet.

16.1 Sprache und Reduktionssemantik

Definition 16.1 (Sprache des -Kalkls L ) Sei V eine abzhlbare Men-


ge von Variablen. Die Sprache des -Kalkls, die Menge der -Terme,
L , ist durch folgende Grammatik definiert:

hL i hV i
| (L L )
| ( hV i.L )

Ein -Term der Form (e0 e1 ) heit Applikation mit Operator e0 und Ope-
rand e1 . Ein Term der Form (x.e) heit Abstraktion, wobei x Parameter
der Abstraktion heit und e Rumpf. In diesem Kapitel steht e immer fr
einen -Term, v und x stehen fr Variablen.

Es ist kein Zufall, da Scheme genau die gleichen Begriffe verwen-


det wie der -Kalkl. Ein Lambda-Ausdruck mit einem Parameter
entspricht einer Abstraktion im -Kalkl, und die Applikationen in
Scheme entsprechen den Applikationen im -Kalkl. Scheme wurde
bewut auf dem -Kalkl aufgebaut.
Die Intuition fr die Bedeutung der -Terme ist hnlich wie in Sche-
me: Eine Abstraktion steht fr eine mathematische Funktion, speziell fr
eine solche Funktion, die sich durch ein Computerprogramm berechnen
182 Kapitel 16

lt.1 Eine Applikation steht gerade fr die Applikation einer Funktion,


und eine Variable bezieht sich auf den Parameter einer umschlieenden
Abstraktion und steht fr den Operanden der Applikation.
Der einfachste -Term ist die Identitt:
(x.x )
Der folgende -Term wendet eine Funktion f auf ein Argument x an:
( f .(x.( f x )))
An diesem Beispiel wird deutlich, da sich im -Kalkl, wie in Sche-
me auch, die Klammern schnell hufen, wenn die Terme grer wer-
den. Darum werden redundante Klammern beim Aufschreiben von
-Termen oft weggelassen. Damit wird aus dem obigen Term der fol-
gende:
f .x. f x
Dieser Ausdruck lt sich unterschiedlich klammern: ( f .((x. f ) x )),
( f .(x.( f x ))) oder (( f .x. f ) x ). Bei solchen Mehrdeutigkeiten er-
streckt sich der Rumpf einer Abstraktion so weit wie mglich nach
rechts. Die richtige Variante ist also ( f .(x.( f x ))).
Die Funktionen im -Kalkl sind auf einen Parameter beschrnkt.
Dies ist keine wirkliche Einschrnkung: Funktionen mit mehreren Para-
metern werden geschnfinkelt, um aus ihnen mehrstufige Funktionen
mit jeweils einem Parameter zu machen, vgl. Abschnitt 8.5.
Wegen der Verwandtschaft zwischen Funktionen mit mehreren Pa-
rametern und ihren geschnfinkelten Pendants gibt es zwei weitere
Abkrzungen in der Notation von -Termen:
x1 . . . xn .e steht fr x1 .(x2 .(. . . xn .e) . . .).
e0 . . . en steht fr (. . . (e0 e1 ) e2 ) . . . en ).
Dementsprechend ist f xy. f x y eine andere Schreibweise fr den Term
( f .(x.(y.(( f x ) y)))) .
Bemerkenswert am -Kalkl ist, da es dort nur Funktionen gibt,
noch nicht einmal Zahlen, boolesche Werte oder Datenstrukturen. Dar-
um erscheint die Sprache des Kalkls auf den ersten Blick noch sparta-
nisch und unintuitiv: So unmittelbar lt sich noch nicht einmal eine
Funktion hinschreiben, die zwei Zahlen addiert schlielich gibt es kei-
ne Zahlen. Wie sich jedoch weiter unten in Abschnitt 16.3 herausstellen
wird, lassen sich all diese Dinge durch Funktionen nachbilden.
Der -Kalkl selbst legt das Verhalten von -Termen fest; er ist ein
Reduktionskalkl, der beschreibt, wie ein -Term in einen anderen,
gleichbedeutenden, berfhrt werden kann. Die Konstruktion dieses
Kalkls erfordert sehr sorgfltigen Umgang mit Variablen, was eine
Hilfsdefinition notwendig macht:
1 Die Wahl des Buchstabens fr die Notation von Abstraktionen war eher ein Unfall: Zur
Zeit der Entstehung des Kalkls war der Ausdruck 2x + 1 eine historische Notation fr
def
eine Funktion f mit f ( x ) = 2x + 1. Alonzo Church, der Erfinder des -Kalkls, hatte

ursprnglich die Notation x.2x + 1 in der ersten Publikation ber den Kalkl vorgesehen.
Der Schriftsetzer konnte allerdings aus technischen Grnden das Htchen nicht ber
dem x positionieren und setzte es deshalb davor, womit aus dem Ausdruck x.2x + 1
wurde. Ein weiterer Setzer machte aus dem einsamen Htchen ein und der -Kalkl
war geboren.
Der -Kalkl 183

Definition 16.2 (Freie und gebundene Variablen) Die Funktionen

free, bound : L P (V )

liefern die Mengen der freien bzw. der gebundenen Variablen eines -
Terms.


{v}
falls e=v
def
free(e) = free(e0 ) free(e1 ) falls e = e0 e1

free(e0 ) \ {v} falls e = v.e0



falls e = v
def
bound(e) = bound(e0 ) bound(e1 ) falls e = e0 e1

bound(e0 ) {v} falls e = v.e0

def
Auerdem ist var(e) = free(e) bound(e) die Menge der Variablen von
e. (Es lt sich leicht zeigen, da diese Menge alle vorkommenden
Variablen eines -Terms enthlt.) Ein -Term e heit abgeschlossen bzw.
Kombinator, falls free(e) = .

Einige Beispiele:

free(x.y) = {y}
bound(x.y) = {x}
free(y.y) =
bound(y.y) = {y}
free(x.y.x.x (z.a y)) = { a}
bound(x.y.x.x (z.a y)) = { x, y, z}

In einem Term kann die gleiche Variable sowohl frei als auch gebunden
vorkommen:

free(x.y (y.y)) = {y}


bound(x.y (y.y)) = { x, y}

Entscheidend ist dabei, da das y einmal innerhalb und einmal auer-


halb einer bindenden Abstraktion auftaucht. Das Frei- und Gebunden-
sein bezieht sich also immer auf bestimmte Vorkommen einer Variablen
in einem -Term.
Im -Kalkl gilt, genau wie Scheme, das Prinzip der lexikalischen
Bindung (siehe Abschnitt ??): das Vorkommen einer Variable v als -
Term gehrt immer zur innersten umschlieenden Abstraktion v.e,
deren Parameter ebenfalls v ist. Bei x.y (y.y)) aus dem Beispiel oben
ist also das erste y das freie, whrend das zweite y durch die zweite
Abstraktion gebunden wird.
Der -Reduktionskalkl ist darauf angewiesen, Variablen durch ande-
re zu ersetzen, ohne dabei die Zugehrigkeit von Variablenvorkommen
und den dazu passenden Abstraktionen zu verndern. Der Mechanis-
mus dafr heit auch hier Substitution:
184 Kapitel 16

Definition 16.3 (Substitution) Fr e, f L ist e[v 7 f ] in e wird v


durch f substituiert induktiv definiert:


f
falls e = v
falls e = x und x 6= v




x
falls e = v.e0

v.e0


def
e[v 7 f ] = x.(e0 [v 7 f ]) falls e = x.e0 und x 6= v, x 6 free( f )
x 0 .(e0 [ x 7 x 0 ][v 7 f ])

falls e = x.e0




und x 6= v, x free( f ), x 0 6 free(e0 ) free( f )





(e0 [v 7 f ]) (e1 [v 7 f ])

falls e = e0 e1

Die Definition der Substitution erscheint auf den ersten Blick kom-
pliziert, folgt aber letztlich nur direkt dem Prinzip der lexikalischen
Bindung. Die erste Regel besagt, da das Vorkommen einer Variable
durch eine Substitution genau dieser Variablen ersetzt wird:

v[v 7 f ] = f

Die zweite Regel besagt, da das Vorkommen einer anderen Variable


durch die Substitution nicht betroffen wird:

x [v 7 f ] = x x 6= v

Die dritte Regel ist auf den ersten Blick etwas berraschend:

(v.e0 )[v 7 f ] = v.e0

Ein -Ausdruck, dessen Parameter gerade die Variable ist, die substi-
tutiert werden soll, bleibt unverndert. Das liegt daran, da mit dem
-Ausdruck die Zugehrigkeit aller Vorkommen von v in e0 bereits
festgelegt ist: ein Vorkommen von v in e0 gehrt entweder zu dieser
Abstraktion oder einer anderen Abstraktion mit v als Parameter, die in
e0 weiter innen steht v ist in (v.e0 ) gebunden und v bound(v.e0 ).
Da die Substitution diese Zugehrigkeiten nicht verndern darf, lt
sie das v in Ruhe.
Anders sieht es aus, wenn die Variable der Abstraktion eine andere
ist die vierte Regel:

(x.e0 )[v 7 f ] = x.(e0 [v 7 f ]) x 6= v, x 6 free( f )

In diesem Fall wird die Substitution auf den Rumpf der Abstraktion
angewendet. Wichtig ist dabei, da x nicht frei in f vorkommt sonst
knnte es vorkommen, da beim Einsetzen von f ein freies Vorkommen
von x pltzlich durch die umschlieende Abstraktion gebunden wird.
Damit wrde auch wieder die durch die lexikalische Bindung definierte
Zugehrigkeitsregel verletzt.
Was passiert, wenn x eben doch frei in f vorkommt, beschreibt die
fnfte Regel:

(x.e0 )[v 7 f ] = x 0 .(e0 [ x 7 x 0 ][v f ]) x 6= v, x free( f )


x 0 6 free(e0 ) free( f )
Der -Kalkl 185

Hier kann es passieren, da die freien x in f durch die Abstraktion


eingefangen werden. Aus diesem Grund wird einfach das x in der
Abstraktion aus dem Weg geschafft und durch ein frisches x 0 ersetzt,
das noch nirgendwo frei vorkommt.
Die letzte Regel beschreibt schlielich, wie die Substitution auf Appli-
kationen wirkt: sie taucht einfach in Operator und Operanden rekursiv
ab:
(e0 e1 )[v 7 f ] = (e0 [v 7 f ])(e1 [v 7 f ])
Hier ist ein etwas umfangreicheres Beispiel fr die Substitution:
(x.y.x (z.z) z)[z 7 x y] = x 0 .((y.x (z.z) z)[ x 7 x 0 ][z 7 x y])
= x 0 .((y.(( x (z.z) z)[ x 7 x 0 ]))[z 7 x y])
= x 0 .((y.( x [ x 7 x 0 ] ((z.z)[ x 7 x 0 ]) z[ x 7 x 0 ]))[z 7 x y])
= x 0 .((y.( x 0 (z.z) z))[z 7 x y])
= x 0 .y0 .(( x 0 (z.z) z)[y 7 y0 ][z 7 x y])
= x 0 .y0 .(( x 0 [y 7 y0 ] ((z.z)[y 7 y0 ]) z[y 7 y0 ])[z 7 x y])
= x 0 .y0 .(( x 0 (z.z) z)[z 7 x y])
= x 0 .y0 .x 0 [z 7 x y] ((z.z)[z 7 x y]) z[z 7 x y]
= x 0 .y0 .x 0 (z.z) ( x y)
Deutlich zu sehen ist, wie die freien Variablen x und y aus der Substitu-
tion z 7 x y auch im Ergebnis frei bleiben, whrend die gebundenen
Variablen x und y aus dem ursprnglichen Term umbenannt werden,
um eine irrtmliche Bindung ihrer hereinsubstitutierten Namensvettern
zu vermeiden.
Mit Hilfe der Definition der Substitution ist es mglich, die Redukti-
onsregeln des -Kalkls zu formulieren.

Definition 16.4 (Reduktionsregeln) Die Reduktionsregeln im -Kalkl


sind die -Reduktion und die -Reduktion :

x.e y.(e[ x 7 y]) y 6 free(e)


(v.e) f e[v 7 f ]

Fr x {, } ist x jeweils der reflexiv-transitive Abschlu der Relati-
on. (Siehe dazu Definition 1.9). Auerdem ist x jeweils der symmetri-

sche Abschlu, und x der reflexiv-transitiv-symmetrische Abschlu.
Die -Reduktion (oft auch -Konversion genannt) benennt eine gebunde-
ne Variable in eine andere um.
Die -Reduktion, die zentrale Regel des -Kalkls, steht fr Funkti-
onsapplikation: eine Abstraktion wird angewendet, indem die Vorkom-
men ihres Parameters durch den Operanden einer Applikation ersetzt
werden.
Wie in anderen Reduktionskalklen auch, also zum Beispiel wie in
RC1 , werden die Regeln auf Subterme fortgesetzt. So gilt zum Beispiel:
x.(y.y) x x.x
Auch der Begriff des Redex ist im -Kalkl analog zu RC1 und be-
zeichnet einen reduzierbaren Subterm. Im obigen Beispiel ist der Redex
gerade (y.y) x.
186 Kapitel 16

Als Reduktionskalkl ist die Hauptaufgabe des -Kalkls der Beweis


von Gleichungen: Zwei Terme gelten als quivalent wenn sie durch
Reduktionen ineinander berfhrt werden knnen.

Definition 16.5 (quivalenz im -Kalkl) Zwei Terme e1 , e2 L hei-



en -quivalent oder einfach nur quivalent, wenn e1 , e2 gilt,
def
wobei , = .
Die Schreibweise dafr ist e1 e2 .

16.2 Normalformen

Im -Kalkl ist es erlaubt, jederzeit beliebige Teilausdrcke zu reduzie-


ren, solange sie nur - oder -Redexe sind. Zum Beispiel gibt es fr den
folgenden -Term zwei verschiedene Mglichkeiten zur -Reduktion.
Der gewhlte Redex ist jeweils unterstrichen:

((x.(y.y) z) a) (x.z) a
((x.(y.y) z) a) (y.y) z

In diesem Beispiel kommt eine weitere -Reduktion sowohl von (x.z) a


als auch von (y.y) z zum gleichen Ergebnis z ein Indiz dafr, da
schlielich alle Abfolgen von -Reduktionen zum gleichen Ergebnis
kommen. Eine solche Eigenschaft eines Kalkls heit Normalformeigen-
schaft. Hier ist die Definition des Normalformbegriffs fr den -Kalkl:

Definition 16.6 (Normalform) Sei e ein -Term. Ein -Term e0 ist eine

Normalform von e, wenn e e0 gilt und kein -Term e00 existiert mit
e0 e00 .

Nun wre es schn, wenn Normalformen dazu benutzt werden knnten,


um den Beweis von Gleichungen im Kalkl zu erleichtern: Der Beweis
von e1 e2 erfordert dann lediglich den Vergleich der Normalformen
von e1 und e2 wenn diese -quivalent sind, dann gilt e1 e2 , sonst
nicht.
Leider haben manche -Terme berhaupt keine Normalform. Hier
ein Beispiel:

(x.x x )(x.x x ) (x.x x ) (x.x x )

Solche Terme ohne Normalformen lassen sich endlos weiterreduzie-


ren, ohne da der Proze jemals zum Schlu kommt. Sie entsprechen
damit Programmen, die endlos weiterrechnen. Dies ist kein spezieller
Defekt des -Kalkls: Jeder Kalkl, der mchtig genug ist, um beliebige
Computerprogramme zu modellieren, hat diese Eigenschaft.
Eine wichtige Eigenschaft auf dem Weg zur Eindeutigkeit von Nor-
malformen ist der Satz von Church/Rosser:

Satz 16.7 (Church/Rosser-Eigenschaft) Die -Reduktionsregel hat die



Church/Rosser-Eigenschaft: Fr beliebige -Terme e1 und e2 mit e1 e2 ,

gibt es immer einen -Term e0 mit e1 e0 und e2 e0 .
Der -Kalkl 187


e1 e2

Abbildung 16.1. Die Church/Rosser-Eigenschaft

Abbildung 16.1 stellt die Aussage des Satzes von Church/Rosser gra-
fisch dar. Der Beweis des Satzes ist leider recht umfangreich und tech-
nisch. Die einschlgige Literatur ber den -Kalkl hat ihn vorrtig [?].
Die Church/Rosser-Eigenschaft ebnet den Weg fr Benutzung von
Normalformen zum Finden von Beweisen im -Kalkl:

Satz 16.8 (Eindeutigkeit der Normalform) Ein -Term e hat hchstens


eine Normalform modulo -Reduktion.
Beweis Angenommen, es gebe zwei unterschiedliche Normalformen
e1 und e2 von e. Nach Satz 16.7 mu es dann aber einen weiteren -Term

e0 geben mit e1 e0 und e2 e0 . Entweder sind e1 und e2 also nicht
unterschiedlich, oder zumindest einer von beiden ist keine Normalform
im Widerspruch zur Annahme. 
Satz 16.8 besttigt, da der -Kalkl ein sinnvoller Mechanismus fr die
Beschreibung des Verhaltens von Computerprogrammen ist: Bei einem
-Term ist es gleichgltig, in welcher Reihenfolge die Reduktionen
angewendet werden: Jede Reduktionsfolge, die zu einer Normalform
fhrt, fhrt immer zur gleichen Normalform.

16.3 Der -Kalkl als Programmiersprache

Mit dem Normalformsatz ist geklrt, da Terme im -Kalkl, die eine


Normalform besitzen, so etwas wie einen Sinn haben, der unabhngig
von der Reihenfolge der Reduktionsschritte ist. Bleibt die Frage, ob der
-Kalkl gro genug ist, um Computerprogramme abzubilden.
Auf den ersten Blick erscheint das etwas unwahrscheinlich: In der
Welt des -Kalkls gibt es direkt keine eingebauten booleschen Werte
oder Zahlen. Diese lassen sich jedoch durch Funktionen nachbilden.
Das heit, da der -Kalkl ebenso mchtig wie eine ausgewachsene
Programmiersprache ist. Dadurch, da er aber nur eine zentrale Reduk-
tionsregel besitzt, eignet er sich aber viel besser als eine komplizierte
Programmiersprache fr die formale Manipulation.
Dieser Abschnitt zeigt, wie sich die wichtigsten Elemente einer Pro-
grammiersprache im Kalkl nachbilden lassen:
Verzweigungen und boolesche Werte
Zahlen
Rekursion

16.3.1 Verzweigungen

Verzweigungen haben ihre primre Daseinsberechtigung in Verbindung


mit booleschen Werten und umgekehrt. Die binre Verzweigung in
188 Kapitel 16

Scheme (if t k a) whlt, abhngig vom Wert von t, entweder die Kon-
sequente k oder die Alternative a aus. Die Nachbildung im -Kalkl
stellt dieses Prinzip auf den Kopf: die Maschinerie fr die Auswahl
zwischen Konsequente und Alternative wird in die booleschen Werte
selbst gesteckt. true ist ein -Term, der das erste von zwei Argumen-
ten auswhlt und das zweite verwirft; false selektiert das zweite und
verwirft das erste:
def
true = xy.x
def
false = xy.y

Damit hat die Verzweigung selbst nicht mehr viel zu tun; sie wendet ein-
fach den Test, der einen booleschen Wert ergeben mu, auf Konsequente
und Alternative an:
def
if = txy.t x y
Da if tatschlich so funktioniert wie angenommen, lt sich an einem
Beispiel leicht sehen:

if true e1 e2 = (txy.t x y) true e1 e2


(xy.true x y) e1 e2
2 true e1 e2
= (xy.x ) e1 e2
(y.e1 ) e2
e1

Fr false geht der Beweis analog.

16.3.2 Natrliche Zahlen

Die Nachbildung von Zahlen ist etwas komplizierter als die der boo-
leschen Werte. Eine Methode dafr ist die Verwendung von Church-
Numeralen. Das Church-Numeral dne einer natrlichen Zahl n ist eine
Funktion, die eine n-fache Applikation vornimmt.

def
dne = f x. f n ( x )

Fr einen -Term f ist f n : L L folgendermaen induktiv defi-


niert: (
n def e falls n = 0
f (e) =
f ( f n1 (e)) sonst
d0e ist nach dieser Definition f .x.x, d1e ist f .x. f x, d2e ist f .x. f ( f x ),
usw.
Die Nachfolgeroperation hngt eine zustzliche Applikation an:

def
succ = n. f .x.n f ( f x )

Der folgende Term bildet die Vorgngerfunktion ab:

def
pred = x.y.z.x (p.q.q ( p y)) ((x.y.x ) z) (x.x )
Der -Kalkl 189

Der Beweis dafr, da sich pred in bezug auf succ wie die Vorgnger-
funktion verhlt, ist bungsaufgabe 16.1.
In Verbindung mit den booleschen Werten lt sich eine Zahl darauf-
hin testen, ob sie 0 ist:
def
zerop = n.n (x.false) true

Die Funktionsweise von zerop lt sich am einfachsten an einem Beispiel


erkennen:
zerop d0e = (n.n (x.false) true) d0e
d0e (x.false) true
= ( f .x.x ) (x.false) true
(x.x ) true
true

16.3.3 Rekursion und Fixpunktsatz

Schlielich fehlt noch die Rekursion. Das Hauptproblem dabei ist, da


es im -Kalkl kein Pendant zu define oder letrec gibt: Es gibt keine
direkte Mglichkeit, eine rekursive Bindung herzustellen. Zur Reali-
sierung von Rekursion ist deshalb ein Kunstgriff notwendig, der sich
an der rekursiven Definition der Fakultt zeigen lt. Schn wre eine
Definition wie folgt, wobei Zahlen ohne d e fr ihre Church-Numerale
stehen:
def
fac = x.if (zerop x ) 1 ( x (fac (pred x )))
= und stehen dabei fr -Terme, die Church-Numerale verglei-
chen bzw. multiplizieren. (Ihre Formulierung ist Teil der bungsaufga-
be 16.2.)
Leider ist diese Formulierung von fac keine richtige Definition: fac
taucht sowohl auf der linken als auch auf der rechten Seite auf. Wenn
fac aus der rechten Seite entfernt wird, bleibt folgender Term brig:

x.if (zerop x 1) 1 ( x (? (pred x )))

Immerhin ist zu sehen, da dieser Term korrekt die Fakultt von 0


ausrechnet, nmlich 1. Fr alle Zahlen grer als 0 ist es allerdings
schlecht bestellt, da der Term ? noch unbekannt ist. Weil der obige
Term nur fr 0 taugt, sei er mit fac0 benannt:
def
fac0 = x.if (zerop x ) 1 ( x (? (pred x )))

Nun wre es schn, einen Term zu haben, der zumindest auch die Fa-
kultt von 1 ausrechnen kann. Dazu wird fac0 in seine eigene Definition
anstelle des ? eingesetzt. Das Ergebnis sieht so aus:

x.if (zerop x ) 1 ( x (fac0 (pred x )))

Da fac0 keinen Selbstbezug enthlt, lt sich seine Definition einsetzen;


das Ergebnis soll der Funktion entsprechend fac1 heien:
def
fac1 = x.if (zerop x ) 1 ( x ((x.if (zerop x ) 1 ( x (? (pred x )))) (pred x )))
190 Kapitel 16

Auf die gleiche Art und Weise lt sich ein Term konstruieren, der alle
Fakultten bis 2 ausrechnen kann:

def
fac2 = x.if (zerop x ) 1 ( x (fac1 (pred x )))

Dieses Muster lt sich immer so weiter fortsetzen. Leider entsteht


dabei trotzdem nie ein Term, der die Fakultten aller natrlichen Zahlen
berechnen kann, da die Terme immer endlich gro bleiben.
Immerhin aber enthalten alle facn -Terme das gleiche Muster und
unterscheiden sich nur durch Aufruf von facn1 . Also ist es sinnvoll,
Abstraktion das Problem anzuwenden:

fac.x.if (zerop x ) 1 ( x (fac (pred x )))

Dieser Term soll FAC heien. Nun lassen sich die facn -Funktionen mit
Hilfe von FAC einfacher beschreiben:

def
fac0 = x.if (zerop x ) 1 ( x (? (pred x )))
def
fac1 = FAC fac0
def
fac2 = FAC fac1
def
fac3 = FAC fac2
...

FAC ist also eine Fabrik fr Fakulttsfunktionen und teilt mit allen faci
die Eigenschaft, da ihre Definition nicht rekursiv ist.
Damit ist zwar die Notation weniger schreibintensiv geworden, aber
das fundamentale Problem ist noch nicht gelst: Eine korrekte Defini-
tion von fac mte eine unendliche Kette von Applikationen von FAC
enthalten. Da sich ein Term mit einer unendlichen Kette von Applikatio-
nen nicht aufschreiben lt, hilft im Moment nur Wunschdenken weiter.
Dafr sei angenommen, fac wre bereits gefunden. Dann gilt folgende
Gleichung:
fac FAC fac

Die eine zustzliche Applikation, die FAC vornimmt, landet auf ei-
nem ohnehin schon unendlichen Stapel, macht diesen also auch nicht
grer. Damit ist aber fac ein sogenannter Fixpunkt von FAC: Wenn fac
hineingeht, kommt es auch genauso wieder heraus. Wenn es nun eine
Mglichkeit gbe, fr einen -Term einen Fixpunkt zu finden, wre das
Problem gelst. Der folgende Satz zeigt, da dies tatschlich mglich
ist:

Satz 16.9 (Fixpunktsatz) Fr jeden -Term F gibt es einen -Term X


mit F X X.
def
Beweis Whle X = Y F, wobei

def
Y = f .(x. f ( x x )) (x. f ( x x )).
Der -Kalkl 191

fac 3 = Y FAC 3

(Satz 16.9) FAC (Y FAC) 3
(x.if (zerop x ) 1 ( x ((Y FAC) (pred x )))) 3
if (zerop 3) 1 ( 3 ((Y FAC) (pred 3)))

if false 1 ( 3 ((Y FAC) 2))

3 ((Y FAC) 2)

(Satz 16.9) 3 (FAC (Y FAC) 2)
3 ((x.if (zerop x ) 1 ( x ((Y FAC) (pred x )))) 2)
3 (if (zerop 2) 1 ( 2 ((Y FAC) (pred 2))))

3 (if false 1 ( 2 ((Y FAC) 1)))

3 ( 2 ((Y FAC) 1))

(Satz 16.9) 3 ( 2 (FAC (Y FAC) 1))
3 ( 2 ((x.if (zerop x ) 1 ( x ((Y FAC) (pred x )))) 1))
3 ( 2 (if (zerop 1) 1 ( 1 ((Y FAC) (pred 1)))))

3 ( 2 (if false 1 ( 1 ((Y FAC) 0))))

3 ( 2 ( 1 ((Y FAC) 0)))

(Satz 16.9) 3 ( 2 ( 1 (FAC (Y FAC) 0)))
3 ( 2 ( 1 ((x.if (zerop x ) 1 ( x ((Y FAC) (pred x )))) 0)))
3 ( 2 ( 1 (if (zerop 0) 1 ( 1 ((Y FAC) (pred 0))))))

3 ( 2 ( 1 (if true 1 ( 1 ((Y FAC) (pred 0))))))

3 ( 2 ( 1 1))

6

Abbildung 16.2. Berechnung der Fakultt von 3 im -Kalkl

Dann gilt:

Y F = ( f .(x. f ( x x )) (x. f ( x x ))) F


(x.F ( x x )) (x.F ( x x ))
F ((x.F ( x x )) (x.F ( x x )))
F (( f .(x. f ( x x )) (x. f ( x x ))) F )
= F (Y F )


Der -Term Y, der Fixpunkte berechnet, heit Fixpunktkombinator. Mit
seiner Hilfe lt sich die Fakultt definieren:
def
fac = Y FAC

Abbildung 16.2 zeigt, wie die Berechnung der Fakultt von 3 mit
dieser Definition funktioniert.

16.4 Auswertungsstrategien

Die Definitionen des vorangegangenen Abschnitts zusammen mit dem


Satz von Church/Rosser sind wichtige Meilensteine auf dem Weg zur
Verwendung des -Kalkls als Basis fr reale Programmiersprachen.
192 Kapitel 16

Leider hat die Anwendung des Satzes von Church/Rosser noch einen
Haken in der Praxis: Er besagt zwar, da sich die quivalenz von zwei
Termen dadurch beweisen lt, da ihre Normalformen verglichen wer-
den. Leider sagt er nichts darber, wie diese Normalformen gefunden
werden.
Zum systematischen Finden von Normalformen gehrt eine Aus-
wertungsstrategie. Eine solche Strategie ist dafr zustndig, von den
-Redexen innerhalb eines -Terms denjenigen auszusuchen, der tat-
schlich reduziert wird. Fr den -Kalkl gibt es mehrere populre
Auswertungsstrategien, die jeweils ihre eigenen Vor- und Nachteile
haben, was das effektive Finden von Normalformen betrifft.
Eine populre Auswertungsstrategie ist die Linksauen-Reduktion,
auch normal-order reduction oder leftmost-outermost reduction genannt:

Definition 16.10 (Linksauen-Reduktion) Die Relation o , die Linksauen-


Reduktion, ist durch die gleiche Regel wie die -Reduktion definiert:

(v.e) f o e[v 7 f ]

Diese Regel darf nur auf bestimmte Subterme angewendet werden,


nmlich solche -Redexe, die mglichst weit links auen stehen.

Die Linksauen-Reduktion hat folgende uerst angenehme Eigen-


schaft:

Satz 16.11 Wenn e0 eine Normalform von e ist, so gilt e o e0 .

Falls es also eine Normalform gibt, so findet die Linksauen-Reduktion


sie auch.
Es gibt allerdings noch weitere Auswertungsstrategien. Die sogenann-
te Call-by-Name-Auswertung basiert auf dem Konzept der schwachen
Kopfnormalform:

Definition 16.12 (Schwache Kopfnormalform) Unter den -Termen hei-


en die Abstraktionen auch Werte oder schwache Kopfnormalformen. Ein
-Term, der kein Wert ist, heit Nichtwert.

Definition 16.13 (Call-by-Name-Auswertung) Die Relation n , die Call-


by-Name-Reduktion, ist durch folgende Regel definiert, die wiederum
identisch zur normalen Regel fr -Reduktion ist:

(v.e) f n e[v 7 f ]

Diese Regel darf nur in einem Gesamtterm angewendet werden, wenn


dieser noch nicht in schwacher Kopfnormalform ist, und auch dann
nur auf Subterme, die -Redexe sind, die mglichst weit links auen
stehen.

Die Call-by-Name-Auswertung ist damit hnlich zur Linksauen-Aus-


wertung, aber nicht ganz so aggressiv: sie gibt sich schon mit einer
schwachen Kopfnormalform zufrieden anstatt einer richtigen Nor-
malform. Dies ist bei der Verwendung als Auswertungsstrategie in
Programmiersprachen allerdings schon genug: die weitere Auswertung
Der -Kalkl 193

des Rumpfes einer schwachen Kopfnormalform wird einfach verscho-


ben auf die Zeit der Applikation.
Linksauen- und Call-by-Name-Auswertung finden zwar immer eine
Normalform bzw. eine schwache Kopfnormalform, wenn es eine solche
gibt; gelegentlich aber geschieht dies nicht auf die effektivste Art und
Weise. Im folgendem Term wird bei Linksauen- und Call-by-Name-
Reduktion zuerst der uere Redex reduziert:
(x.x x ) ((y.y) z) o ((y.y) z) ((y.y) z)
o z ((y.y) z)
o zz

Bei dieser Reduktionsfolge wurde der Subterm ((y.y) z) zunchst


verdoppelt und mute demnach auch zweimal reduziert werden.
Eine andere Auswertungsstrategie verspricht die Vermeidung solcher
doppelter Arbeit: Die meisten Programmiersprachen verwenden eine
Strategie, die von der sogenannten Linksinnen-Reduktion, auch genannt
applicative-order reduction oder leftmost-innermost reduction abgeleitet ist:

Definition 16.14 (Linksinnen-Reduktion) In dieser Definition steht w


immmer fr einen Wert. Die Relation i , die Linksinnen-Reduktion, ist
definiert durch die folgende Regel:

(v.e) w i e[v 7 w].

i ist dabei nur anwendbar auf Subterme, die mglichst weit links
innen stehen.

Die Linksinnen-Reduktion ist beim obigen Beispiel effektiver, da zu-


nchst das Argument der ueren Applikation ausgewertet wird:

(x.x x ) ((y.y) (z.z)) i (x.x x ) (z.z)


i (z.z) (z.z)
i (z.z)

Leider fhrt die Linksinnen-Reduktion nicht immer zu einer Normal-


form, selbst wenn es die Linksauen-Reduktion tut. Der Term

(x.y.y) ((z.z z) (z.z z))

zum Beispiel hat zwei Redexe, einmal den ganzen Term und dann noch

(z.z z) (z.z z).

Die Linksinnen-Strategie whlt den inneren Subterm als ersten Redex


aus:
(z.z z) (z.z z) i (z.z z) (z.z z).
Damit luft die Linksinnen-Reduktion unendlich im Kreis, whrend
die Linksauen-Reduktion sofort den gesamten Term reduziert und die
Normalform y.y liefert.
Eine Ableitung der Linksinnen-Reduktion, die in den meisten Pro-
grammiersprachen Anwendung findet, ist die Call-by-Value-Reduktion:
194 Kapitel 16

Definition 16.15 (Call-by-Value-Reduktion) In dieser Definition steht w


immmer fr einen Wert und e fr einen Nichtwert. Die Relation v ,
die Call-by-Value-Reduktion, ist definiert durch die folgende Regel:

(v.e) w v e[v 7 w].

v darf nur in einem Gesamtterm angewendet werden, wenn dieser


keine schwache Kopfnormalform ist, und dann nur auf einen Subterm,
der mglichst weit links innen steht.

16.5 Die Auswertungsstrategie von Scheme

Mit der Call-by-Value-Reduktion ist die Grundlage fr die Auswer-


tungsstrategie von Scheme gelegt. Tatschlich definiert das Substituti-
onsmodell eine Variante der Call-by-Value-Auswertung. Zur Erinnerung
ist hier noch einmal die Definition der wichtigsten Regel des Substitu-
tionsmodells, nmlich der fr Prozeduranwendungen der Form ( p o1
. . . on ):

[. . . ] Zunchst werden Operator p und Operanden o1 , . . . , on ausgewertet. Der Wert


von p mu eine Prozedur sein. [. . . ]

Der entscheidende Satz ist dabei der letzte: Er bedeutet, da innen


zuerst ausgewertet wird; treten bei der Auswertung von Operator und
Operanden weitere Prozeduranwendungen auf, wird das gleiche Prin-
zip rekursiv angewendet. Damit ist das Substitutionsmodell fr Scheme
eng verwandt mit der Call-by-Value-Auswertung im Lambda-Kalkl.
Der einzige Unterschied zwischen der offiziellen Definition der Call-
by-Value-Auswertung im -Kalkl und Scheme ist, da in Scheme nicht
notwendigerweise von links nach rechts reduziert wird: Der Scheme-
Standard [?, ?] schreibt nicht vor, in welcher Reihenfolge Operator und
Operanden ausgewertet werden. Es kann im Prinzip sogar passieren,
da bei jedem Prozeduraufruf eine andere Auswertungsreihenfolge
benutzt wird.
Trotzdem ist es blich, bei Programmiersprachen, die von innen nach
auen auswerten, von Call-by-Value-Sprachen oder strikten Sprachen zu
sprechen. Neben Scheme gehren auch C, Java, Pascal und ML und
viele andere zu den strikten Sprachen.
Es gibt auch nicht-strikte Sprachen wie z.B. Haskell, die auf der so-
genannten lazy evaluation beruhen. Ihre Auswertungsstrategie ist eng
mit der Call-by-Name-Auswertung im -Kalkl verwandt. Allerdings
vermeiden diese Sprachen die mehrfache berflssige Auswertung von
Ausdrcken dadurch, da sie den Wert beim ersten Mal abspeichern
und danach wiederverwenden.

bungsaufgaben

Aufgabe 16.1 Beweise, da pred den Vorgnger eines positiven Church-


Numerals berechnet!

Aufgabe 16.2 Beweise, da es Lambda-Terme fr die folgenden arith-


Der -Kalkl 195

metischen Operationen auf Church-Numeralen gibt:

adddmedne = dm + ne
multdmedne = dmne
expdmedne = dmn e fr m > 0
(
true falls m = n
=dmedne =
false sonst

Benutze dazu die folgenden Definitionen:

def
add = x.y.p.q.xp(ypq)
def
mult = x.y.z.x (yz)
def
exp = x.y.yx

und gibt eine eigene Definition fr = an. Dabei lt sich die Korrektheit
von add direkt beweisen. Fr mult und exp beweise und benutze dazu
folgende Hilfslemmata:

(dne x )m y x nm y

dnem x dnm e fr m > 0

Aufgabe 16.3 Der Y-Kombinator liee sich auch in Scheme schreiben


als:
(define y
(lambda (f)
((lambda (x) (f (x x)))
(lambda (x) (f (x x))))))

Zeige durch Ausprobieren, da y mit dieser Definition in Scheme nicht


funktioniert. Warum ist das so? Benutze fr die Erklrung das Substitu-
tionsmodell! Zeige, da die folgende Variante von y ein Fixpunktkom-
binator ist, der in Scheme funktioniert:
(define y
(lambda (f)
((lambda (x)
(f (lambda (y) ((x x) y))))
(lambda (x)
(f (lambda (y) ((x x) y)))))))

Aufgabe 16.4 (Quelle: Ralf Hinze, Bonn) Zeige, da F mit der folgenden
Definition ebenfalls ein Fixpunktkombinator ist:

def
F = G [26]
def
G = abcde f ghijklmnopqstuvwxyzr.r (dasistein f ixpunktkombinator )

Dabei steht G [26] fr den Lambda-Term, der durch 26faches Hinterein-


anderschreiben von G entsteht, also GG . . . G = (. . . (( GG ) G ) . . . G ).
17 Die SECD-Maschine

Der -Kalkl ist als theoretisches Modell fr berechenbare Funktionen


lange vor der Erfindung des Computers entwickelt worden. Die Re-
duktionsregeln dienen dabei der Entwicklung von Beweisen ber die
quivalenz von -Termen. Damit der -Kalkl auch als Modell fr die
tatschliche Ausfhrung von Programmen, auf dem Computer geeignet
ist, fehlen noch zwei Zutaten: die direkte Definition von eingebauten
Werten und Operationen wie Zahlen und booleschen Werten sowie
ein formales Auswertungsmodell. Dieses Kapitel stellt zunchst den
angewandten -Kalkl vor, der den normalen -Kalkl um primitive
Werte und Operationen erweitert, und dann die SECD-Maschine, ein
klassisches Auswertungsmodell fr die Call-by-Value-Reduktion. Ange-
nehmerweise lt sich die SECD-Maschine auch als Scheme-Programm
implementieren, was ebenfalls in diesem Kapitel geschieht. Die SECD-
Maschine kennt keine Zuweisungen; es folgt darum noch die Darstel-
lung der SECDH-Maschine, die auch einen Speicher kennt und damit
Zuweisungen korrekt modelliert.

17.1 Der angewandte -Kalkl

Abschnitt 16.3 zeigte bereits, da sich auch boolesche Werte und Zahlen
im -Kalkl durch -Terme darstellen lassen. Das ist zwar aus theo-
retischer Sicht gut zu wissen, auf Dauer aber etwas mhsam: Darum
ist es sinnvoll, mit einer erweiterten Version des -Kalkls zu arbeiten,
die solche primitiven Werte direkt kennt. Abschnitt 16.3 hat gezeigt,
da eine solche Erweiterung nur syntaktischer Zucker ist, also die Aus-
druckskraft des Kalkls nicht wirklich erhht. Alle Erkenntnisse aus
dem normalen -Kalkl bleiben also erhalten.
Ein solcher erweiterter -Kalkl heit auch angewandter -Kalkl:

Definition 17.1 (Sprache des angewandten -Kalkls LA ) Sei V eine


abzhlbare Menge von Variablen. Sei B eine Menge von Basiswerten.
Sei fr eine natrliche Zahl n und i {1, . . . , n} jeweils i eine Menge
von i-stelligen Primitiva die Namen von eingebauten Operationen.
Jedem Fi i ist eine i-stellige Funktion FBi : B . . . B B ihre
Operation zugordnet. Die Sprache des angewandten -Kalkls, die
Menge der angewandten -Terme, LA , ist durch folgende Grammatik
definiert:
hLA i hV i
| (hLA i hLA i)
| (hV i.hLA i)
198 Kapitel 17

| h Bi
| (h1 i hLA i)
| (h2 i hLA i hLA i)
...
| (hn i hLA i ... hLA i) (n-mal)
Die Grammatik ist abgekrzt notiert: Die letzen Klauseln besagen, da
es fr jede Stelligkeit i eine Klausel mit hi i gibt, bei der jeweils i
Wiederholungen von hLA i entsprechend der Stelligkeit der Primi-
tiva in i . Dabei heien Terme der Form ( F k e1 . . . ek ) auch primitive
Applikationen.
In diesem Kapitel dienen normalerweise die Zahlen als Basiswerte mit
den blichen Operationen wie +, , , / etc. Damit sind Terme wie
zum Beispiel (+ ( 5 3) 17) mglich.
Im angewandten -Kalkl kommen zu den Werten aus Definiti-
on 16.12 die Basiswerte dazu:

Definition 17.2 (Werte im angewandten -Kalkl) Im angewandten -


Kalkl heien die Abstraktionen und Basiswerte kollektiv Werte. Ein
-Term, der kein Wert ist, heit Nichtwert.
Damit die primitiven Operationen auch tatschlich eine Bedeutung
bekommen, mu eine spezielle Reduktionsregel fr sie eingefhrt wer-
den:

Definition 17.3 (-Reduktion)


( F k e1 . . . ek ) FBk (e1 , . . . , ek ) e1 , . . . , ek B
Diese Regel besagt, da eine primitive Applikation, wenn alle Ope-
randen Werte sind, durch Anwendung der entsprechenden Operation
reduziert werden kann. Damit wird z.B. der obige Beispielterm folgen-
dermaen reduziert:
(+ ( 5 3) 17) (+ 2 17) 19
17.2 Die einfache SECD-Maschine

Wie schon in Abschnitt 16.5 erwhnt, ist der Call-by-Value--Kalkl ein


Modell fr die Auswertung von Scheme und viele andere Programmier-
sprachen . Allerdings ist Definition 16.15 strenggenommen etwas vage:
Es wird immer nur der Subterm reduziert, der mglichst weit links
innen steht, aber was das heit, ist nicht genau definiert. Auerdem ist
Reduktion zwar ein mchtiges formales Modell, entspricht aber nicht
der Ausfhrungsmethode tatschlicher Scheme-Implementierungen auf
echten Prozessoren. Ein przises und echten Maschinen deutlich nhe-
res Modell ist die SECD-Maschine, erfunden schon in den 60er Jahren
von Peter Landin [?], und seitdem die Grundlage fr zahllose Imple-
mentierungen von Call-by-Value-Sprachen. (Die Darstellung hier ist
gegenber Landins ursprnglicher Formulierung etwas modernisiert.)
Damit ein Programm aus dem angewandten -Kalkl mit der SECD-
Maschine ausgewertet werden kann, mu es erst einmal in einen spezi-
ellen Maschinencode bersetzt oder compiliert werden. Der Maschinen-
code besteht, anders als der -Kalkl, nicht aus geschachtelten Termen,
sondern aus einer Folge von Instruktionen.
Die SECD-Maschine 199

Definition 17.4 (Maschinencode) In der folgenden Definition ist I die


Menge der Instruktionen:
hIi hBi
| hVi
| ap
| primFi fr alle Fi i
| (hVi, hCi)
Ein Maschinencode-Programm ist eine Folge von Instruktionen:

C = I

Ein Term aus dem angewandten -Kalkl wird mit Hilfe folgender
Funktion in Maschinencode bersetzt:

: L C
A
JK


b falls e =bB
v falls e =vV



def
JeK = Je0 K Je1 K ap falls e = ( e0 e1 )

1 K . . . Jek K prim F k falls e = ( F e1 . . . e k )



Je

(v, Je K)
0 falls e = v.e0

Die bersetzungsfunktion linearisiert einen -Term. Zum Beispiel


bedeutet die bersetzung Je0 K Je1 K ap fr einen Term (e0 e1 ), da zuerst
e0 ausgewertet wird, danach wird e1 ausgewertet, und schlielich wird
die eigentliche Applikation ausgefhrt: Entsprechend steht ap fr Ap-
plikation ausfhren und primFk fr Primitiv F ausfhren. Basiswerte
und Variablen werden im Maschinencode belassen. Ein -Term wird
bersetzt in ein Tupel aus seiner Variable und dem Maschinencode fr
seinen Rumpf.
Durch die Linearisierung sind die Instruktionen schon in einer Liste
in der Reihenfolge ihrer Ausfhrung aufgereiht. Insbesondere hat die
Linearisierung den Begriff links innen formalisiert: der jeweils am
weitesten links innen stehende Redex steht in der Liste der Instruktionen
vorn.
Beispiel:

J f .x.y. f (+ x ( y 2))K = ( f , Jx.y. f (+ x ( y 2))K)


= ( f , ( x, Jy. f (+ x ( y2))K))
= ( f , ( x, (y, J f (+ x ( y2))K)))
= ( f , ( x, (y, J f KJ(+ x ( y2))Kap)))
= ( f , ( x, (y, f J(+ x ( y2))Kap)))
= ( f , ( x, (y, f JxKJ( y 2)Kprim+ ap)))
= ( f , ( x, (y, f xJ( y 2)Kprim+ ap)))
= ( f , ( x, (y, f xJyKJ2Kprim prim+ ap)))
= ( f , ( x, (y, f x yJ2Kprim prim+ ap)))
= ( f , ( x, (y, f x y 2 prim prim+ ap)))
200 Kapitel 17

Das Beispiel zeigt deutlich, wie der Rumpf der innersten Abstraktion
in eine lineare Folge von Instruktionen bersetzt wird, die genau der
Call-by-Value-Reduktionsstrategie entspricht: erst f auswerten, dann
x, dann y, dann das Primitiv anwenden, dann +, und schlielich die
Applikation durchfhren.
Nun zur eigentlichen SECD-Maschine sie funktioniert hnlich wie
ein Reduktionskalkl, operiert aber auf sogenannten Maschinenzustn-
den: die Maschine berfhrt also einen Maschinenzustand durch einen
Auswertungsschritt in einen neuen Maschinenzustand. Ein Maschinen-
zustand ist dabei ein 4-Tupel aus der Menge S E C D (daher der
Name der Maschine). Die Buchstaben sind deshalb so gewhlt, weil S
der sogenannte Stack, E die sogenannte Umgebung bzw. auf englisch
das Environment, C der schon bekannte Maschinencode bzw. Code und
D der sogenannte Dump ist. Die formalen Definitionen dieser Mengen
sind wie folgt; dabei ist W die Menge der Werte:

= W
S
E = P (V W )
D = (S E C )
W = B (V C E )

Der Stack ist dabei eine Folge von Werten. In der Maschine sind dies
die Werte der zuletzt ausgewerteten Terme, wobei der zuletzt ausge-
wertete Term vorn bzw. oben steht. Die Umgebung ist eine partielle
Abbildung von Variablen auf Werte: sie ersetzt die Substitution in der
Reduktionsrelation des -Kalkls. Anstatt da Werte fr Variablen ein-
gesetzt werden, merkt sich die Umgebung einfach, an welche Werte die
Variablen gebunden sind. Erst wenn der Wert einer Variablen bentigt
wird, holt ihn die Maschine aus der Umgebung. Der Dump schlie-
lich ist eine Liste frherer Zustnde der Maschine: er entspricht dem
Kontext im Substitutionsmodell.
Die Menge W schlielich entspricht dem Wertebegriff aus Definiti-
on 17.2: Die Basiswerte gehren dazu, auerdem Tripel aus (V C E).
Ein solches Tripel, genannt Closure reprsentiert den Wert einer Ab-
straktion es besteht aus der Variable einer Abstraktion, dem Maschi-
nencode ihres Rumpfs und der Umgebung, die notwendig ist, um die
Abstraktion anzuwenden: Die Umgebung wird bentigt, damit die frei-
en Variablen der Abstraktion entsprechend der lexikalischen Bindung
ausgewertet werden knnen. Dies ist anders als im Substitutionsmodell,
wo Variablen bei der Applikation direkt ersetzt werden und damit
verschwinden. Eine Closure ist also einfach die Reprsentation einer
Funktion.
Im Verlauf der Auswertung werden Umgebungen hufig um neue
Bindungen von einer Variable an einen Wert erweitert. Dazu ist die
Notation e[v 7 w] ntzlich. e[v 7 w] konstruiert aus einer Umgebung
e eine neue Umgebung, in der die Variable v an den Wert w gebunden
ist. Hier ist die Definition:
def
e[v 7 w] = (e \ {(v, w0 )|(v, w0 ) e}) {(v, w)}

Es wird also zunchst eine eventuell vorhandene alte Bindung entfernt


und dann eine neue hinzugefgt.
Die SECD-Maschine 201

Um einen -Term e in die SECD-Maschine zu injizieren, wird


er in einen Anfangszustand (e, , JeK, e) bersetzt. Dann wird dieser
Zustand wiederholt in die Zustandsbergangsrelation , gefttert. In
der folgenden Definition von , sind Bezeichner mit einem Unterstrich
versehen, wenn es sich um Folgen handelt, also z.B. s fr einen Stack:

, P ((S E C D ) (S E C D ))
(s, e, bc, d) , (bs, e, c, d) (17.1)
(s, e, vc, d) , (e(v)s, e, c, d) (17.2)
(bk . . . b1 s, e, primFk c, d) , (bs, e, c, d) (17.3)
wobei F k k und FBk (b1 , . . . , bk ) = b
0
(s, e, (v, c )c, d) , ((v, c0 , e)s, e, c, d) (17.4)
0
(w(v, c , e0 )s, e, ap c, d) , (e, e0 [v 7 w], c0 , (s, e, c)d) (17.5)
(w, e, e, (s0 , e0 , c0 )d) , (ws0 , e0 , c0 , d) (17.6)

Die Regeln definieren eine Fallunterscheidung nach der ersten Instruk-


tion der Code-Komponente des Zustands, bzw. greift die letzte Regel,
wenn der Code leer ist. Der Reihe nach arbeiten die Regeln wie folgt:

Regel 17.1 (die Literalregel) schiebt einen Basiswert direkt auf den
Stack.
Regel 17.2 (die Variablenregel) ermittelt den Wert einer Variable aus
der Umgebung und schiebt diesen auf den Stack.
Regel 17.3 ist die Primitivregel. Bei einer primitiven Applikation ms-
sen soviele Basiswerte oben auf dem Stack liegen wie die Stelligkeit
des Primitivs. Dann ermittelt die Primitivregel das Ergebnis der
primitiven Applikation und schiebt es oben auf den Stack.
Regel 17.4 ist die Abstraktionsregel: Das Tupel (v, c0 ) ist bei der ber-
setzung aus einer Abstraktion entstanden. Die Regel ergnzt v und c0
mit e zu einer Closure, die auf den Stack geschoben wird.
Regel 17.5 ist die Applikationsregel: Bei einer Applikation mssen oben
auf dem Stack ein Wert sowie eine Closure liegen. (Zur Erinnerung:
Eine Applikation kann nur ausgewertet werden, wenn eine Abstrak-
tion vorliegt. Abstraktionen werden zu Closures ausgewertet.) In
einem solchen Fall sichert die Applikation den aktuellen Zustand
auf den Dump, und die Auswertung fhrt mit einem leeren Stack,
der Umgebung aus der Closure erweitert um eine Bindung fr die
Variable und dem Code aus der Closure fort.
Regel 17.6 ist die Rckkehrregel: Sie ist anwendbar, wenn das Ende
des Codes erreicht ist. Das heit, da gerade die Auswertung einer
Applikation fertig ist. Auf dem Dump liegt aber noch ein gesicherter
Zustand, der jetzt zurckgeholt wird.

Hier ein Beispiel fr den Ablauf der SECD-Maschine fr den Term


202 Kapitel 17

(((x.y.(+ x y)) 1) 2):

(e, , ( x, (y, x y prim+ )) 1 ap 2 ap, e)


, (( x, (y, x y prim+ ), ), , 1 ap 2 ap, e)
, (1 ( x, (y, x y prim+ ), ), , ap 2 ap, e)
, (e, {( x, 1)}, (y, x y prim+ ), (e, , 2 ap))
, ((y, x y prim+ , {( x, 1)}), {( x, 1)}, e, (e, , 2 ap))
, ((y, x y prim+ , {( x, 1)}), , 2 ap, e)
, (2 (y, x y prim+ , {( x, 1)}), , ap, e)
, (e, {( x, 1), (y, 2)}, x y prim+ , (e, , e))
, (1, {( x, 1), (y, 2)}, y prim+ , (e, , e))
, (2 1, {( x, 1), (y, 2)}, prim+ , (e, , e))
, (3, {( x, 1), (y, 2)}, e, (e, , e))
, (3, , e, e)

Die Zustandsbergangsrelation , ist nun die Grundlage fr die Aus-


wertungsfunktion der SECD-Maschine, die fr einen -Term dessen
Bedeutung ausrechnet. Dies ist scheinbar ganz einfach:

evalSECD LA B
:
evalSECD (e) = x wenn (e, , JeK, e) , ( x, e, e, e)

Diese Definition hat jedoch zwei Haken:

Die Auswertung von -Termen terminiert nicht immer (wie zum


Beispiel fr den Endlos-Term (x.( x x )) (x.( x x ))), es kommt
also nicht immer dazu, da die Zustandsbergangsrelation bei einem
Zustand der Form (e, , JeK, e) terminiert.
Das x aus dieser Definition ist nicht immer ein Basiswert es kann
auch eine Closure sein.

Der erste Haken sorgt dafr, da die Auswertungsfunktion nur eine


Relation im Sinne einer partiellen Funktion ist. Meist wird trotz-
dem von einer Auswertungsfunktion gesprochen. Beim zweiten Haken,
wenn x eine Closure ist, lt sich mit dem Resultat nicht viel anfangen:
Um die genaue Bedeutung der Closure herauszubekommen, mte sie
angewendet werden das Programm ist aber schon fertig gelaufen. Es
ist also gar nicht sinnvoll, zwischen verschiedenen Closures zu unter-
scheiden. Darum wird fr die Zwecke der Auswertungsfunktion eine
Menge Z der Antworten definiert, die einen designierten Spezialwert
fr Closures enthlt:
Z = B {function}
Damit lt sich die Evaluationsfunktion wie folgt definieren:

evalSECD L Z
(A
b falls (e, , JeK, e) , (b, e, e, e)
evalSECD (e) =
function falls (e, , JeK, e ) , ((v, c, e0 ), e, e, e )

17.3 Quote und Symbole

Dieses Kapitel wird ab hier Gebrauch von einer weiteren Sprachebene in


DrRacket machen, nmlich Die Macht der Abstraktion - fortgeschritten.
Die SECD-Maschine 203

Diese Ebene mu mit dem DrRacket-Men Sprache unter Sprache


auswhlen aktiviert sein, damit die Programme dieses Kapitels funktio-
nieren.
Die entscheidende nderung gegenber den frheren Sprachebenen
ist die Art, mit der die REPL Werte ausdruckt. (Diese neue Schreibweise,
ermglicht, die Programme des Interpreters, die als Werte reprsentiert
sind, korrekt auszudrucken.) Bei Zahlen, Zeichenketten und booleschen
Werten bleibt alles beim alten:

5
, 5
"Mike ist doof"
, "Mike ist doof"
#t
, #t

Bei Listen sieht es allerdings anders aus:

(list 1 2 3 4 5 6)
, (1 2 3 4 5 6)

Die REPL druckt also eine Liste aus, indem sie zuerst eine ffnen-
de Klammer ausdruckt, dann die Listenelemente (durch Leerzeichen
getrennt) und dann eine schlieende Klammer.
Das funktioniert auch fr die leere Liste:

empty
, ()

Mit der neuen Sprachebene bekommt auerdem der Apostroph, der


dem Literal fr die leere Liste voransteht, eine erweiterte Bedeutung.
Unter anderem kann der Apostroph benutzt werden, um Literale fr
Listen zu formulieren:

(1 2 3 4 5 6)
, (1 2 3 4 5 6)
(1 #t "Mike" (2 3) "doof" 4 #f 17)
, (1 #t "Mike" (2 3) "doof" 4 #f 17)
()
, ()

In der neuen Sprachebene benutzen die Literale und die ausgedruckten


externen Reprsentationen fr Listen also die gleiche Notation. Sie
unterscheiden sich nur dadurch, da beim Literal der Apostroph voran-
steht. Der Apostroph funktioniert auch bei Zahlen, Zeichenketten und
booleschen Werten:

5
, 5
"Mike ist doof"
, "Mike ist doof"
#t
, #t
204 Kapitel 17

Der Apostroph am Anfang eines Ausdrucks kennzeichnet diesen also


als Literal. Der Wert des Literals wird genauso ausgedruckt, wie es
im Programm steht. (Abgesehen von Leerzeichen und Zeilenumbr-
chen.) Der Apostroph heit auf englisch quote, und deshalb ist diese
Literalschreibweise auch unter diesem Namen bekannt. Bei Zahlen, Zei-
chenketten und booleschen Literalen ist auch ohne Quote klar, da es
sich um Literale handelt. Das Quote ist darum bei ihnen rein optional;
sie heien selbstquotierend. Bei Listen hingegen sind Miverstndnisse
mit anderen zusammengesetzten Formen mglich, die ja auch mit einer
ffnenden Klammer beginnen: 1
(1 2 3 4 5 6)
, procedure application: expected procedure, given: 1;
arguments were: 2 3 4 5 6

Mit der Einfhrung von Quote kommt noch eine vllig neue Sorte Wer-
te hinzu: die Symbole. Symbole sind Werte hnlich wie Zeichenketten
und bestehen aus Text. Sie unterscheiden sich allerdings dadurch, da
sie als Literal mit Quote geschrieben und in der REPL ohne Anfh-
rungszeichen ausgedruckt werden:
mike
, mike
doof
, doof

Symbole lassen sich mit dem Prdikat symbol? von anderen Werten
unterscheiden:
(symbol? mike)
, #t
(symbol? 5)
, #f
(symbol? "Mike")
, #f
Vergleichen lassen sich Symbole mit equal? (siehe Abbildung ??):

(equal? mike herb)


, #f
(equal? mike mike)
, #t

Symbole knnen nicht aus beliebigem Text bestehen. Leerzeichen


sind zum Beispiel verboten. Tatschlich entsprechen die Namen der
zulssigen Symbole genau den Namen von Variablen:
karl-otto
, karl-otto
mehrwertsteuer
, mehrwertsteuer
1 Tatschlich ist die neue Schreibweise fr externe Reprsentationen die Standard-
Reprsentation in Scheme. Die frheren Sprachebenen benutzten die alternative Schreib-
weise, um die Verwirrung zwischen Listenliteralen und zusammengesetzten Formen zu
vermeiden.
Die SECD-Maschine 205

duftmarke
, duftmarke
lambda
, lambda
+
, +
*
, *

Diese Entsprechung wird in diesem Kapitel noch eine entscheidene


Rolle spielen. Symbole knnen natrlich auch in Listen und damit auch
in Listenliteralen vorkommen:

(karl-otto mehrwertsteuer duftmarke)


, (karl-otto mehrwertsteuer duftmarke)
Mit Hilfe von Symbolen knnen Werte konstruiert werden, die in der
REPL ausgedruckt wie Scheme-Ausdrcke aussehen:

(+ 1 2)
, (+ 1 2)
(lambda (n) (+ n 1))
, (lambda (n) (+ n 1))
Auch wenn diese Werte wie Ausdrcke so aussehen, sind sie doch
ganz normale Listen: der Wert von (+ 1 2) ist eine Liste mit drei
Elementen: das Symbol +, die Zahl 1 und die Zahl 2. Der Wert von
(lambda (n) (+ n 1)) ist ebenfalls eine Liste mit drei Elementen: das
Symbol lambda, eine Liste mit einem einzelnen Element, nmlich dem
Symbol n, und einer weiteren Liste mit drei Elementen: dem Symbol +,
dem Symbol n und der Zahl 1.
Quote hat noch eine weitere verwirrende Eigenheit:

()
, ()
Dieses Literal bezeichnet nicht die leere Liste (dann wrde nur ()
ausgedruckt, ohne Quote), sondern etwas anderes:

(pair? ())
, #t
(first ())
, quote
(rest ())
, (())
Der Wert des Ausdrucks () ist also eine Liste mit zwei Elementen:
das erste Element ist das Symbol quote und das zweite Element ist die
leere Liste. t ist selbst also nur syntaktischer Zucker, und zwar fr
(quote t):

(equal? (quote ()) ())


, #t
(equal? (quote (quote ())) ())
, #t
206 Kapitel 17

Quote erlaubt die Konstruktion von Literalen fr viele Werte, aber nicht
fr alle. Ein Wert, fr den Quote ein Literal konstruieren kann, heit
reprsentierbarer Wert. Die folgende induktive Definition spezifiziert, was
ein reprsentierbarer Wert ist:

Zahlen, boolesche Werte, Zeichenketten und Symbole sind reprsen-


tierbare Werte.
Eine Liste aus reprsentierbaren Werten ist ihrerseits ein reprsentier-
barer Wert.
Nichts sonst ist ein reprsentierbarer Wert.

17.4 Implementierung der SECD-Maschine

Die SECD-Maschine ist ein Modell fr die Implementierung des -


Kalkls. Eine solche Implementierung lt sich in Scheme einfach
bauen dieser Abschnitt zeigt, wie. Der grobe Fahrplan ergibt sich dabei
aus der Struktur der SECD-Maschine selbst: Nach den obligatorischen
Datendefinitionen mssen zunchst Terme in Maschinencode bersetzt
werden. Dann kommt die Zustandsbergangsfunktion und schlielich
die Auswertungsfunktion an die Reihe.

17.4.1 Datenanalyse

Die erste Aufgabe ist dabei zunchst, wie immer, die Datenanalyse: Am
Anfang stehen die Terme des angewandten -Kalkls. Eine geeignete
Reprsentation mit Listen und Symbolen lt dabei die Terme in der
fortgeschrittenen Sprachebene genau wie entsprechenden Scheme-
Terme aussehen:
(+ 1 2) steht fr (+ 1 2)
(lambda (x) x) steht fr x.x
((lambda (x) (x x)) (lambda (x) (x x))) steht fr (x.( x x )) (x.( x x ))
etc.
Die Datendefinition dafr orientiert sich direkt an Definition 17.1:

Ein Lambda-Term ist eins der folgenden:


; - ein Symbol (fr eine Variable)
; - eine zweielementige Liste (fr eine regulre Applikation)
; - eine Liste der Form (lambda (x) e) (fr eine Abstraktion)
; - ein Basiswert
; - eine Liste mit einem Primitiv als erstem Element
; (fr eine primitive Applikation)

Hier die dazu passende Signaturdefinition:

(define term
(signature
(mixed symbol
application
abstraction
base
primitive-application)))

Die Signaturen fr application etc. mssen noch definiert werden.


Die SECD-Maschine 207

Um Verzweigungen ber die Sorte term zu ermglichen, mssen Pr-


dikate fr die einzelnen Teilsorten geschrieben werden. Diese knnen
dann fr die Definition der entsprechenden Signaturen benutzt werden.

(: application? (%a -> boolean))


(define application?
(lambda (t)
(and (pair? t)
(not (equal? lambda (first t)))
(not (primitive? (first t))))))

(define application (signature (predicate application?)))

; Prdikat fr Abstraktionen
(: abstraction? (%a -> boolean))
(define abstraction?
(lambda (t)
(and (pair? t)
(equal? lambda (first t)))))

(define abstraction (signature (predicate abstraction?)))

; Prdikat fr primitive Applikationen


(: primitive-application? (%a -> boolean))
(define primitive-application?
(lambda (t)
(and (pair? t)
(primitive? (first t)))))

(define primitive-application (signature (predicate primitive-application?)))

Die Definition lt noch offen, was genau ein Basiswert und was
ein Primitiv ist. Auch hierfr werden noch Datendefinitionen ben-
tigt, zuerst fr Basiswerte. Der Einfachheit halber beschrnkt sich die
Implementierung erst einmal auf boolesche Werte und Zahlen:

; Ein Basiswert ist ein boolescher Wert oder eine Zahl

Damit Basiswerte in Fallunterscheidungen von den anderen Arten von


Termen unterschieden werden knnen, wird ein Prdikat bentigt:

; Prdikat fr Basiswerte
(: base? (%a -> boolean))
(define base?
(lambda (v)
(or (boolean? v) (number? v))))

(define base (signature (predicate base?)))

Als nchstes sind Primitive gefragt: Am obigen Beispiel ist zu erkennen,


da z.B. + ein Primitiv sein sollte. Die Datendefinition fr eine kleine
beispielhafte Menge von Primitiven ist wie folgt:

; Ein Primitiv ist eins der Symbole +, -, *, /, =


208 Kapitel 17

Da die Primitive genau wie die Variablen Symbole sind, stehen die
Primitive als Variablen nicht mehr zur Verfgung: Alle Symbole, die
keine Primitive sind, sind also Variablen. Das dazugehrige Prdikat
ist das folgende:
; Prdikat fr Primitive
(: primitive? (%a -> boolean))
(define primitive?
(lambda (s)
(or (equal? + s)
(equal? - s)
(equal? * s)
(equal? / s)
(equal? = s))))

(define primitive (signature (predicate primitive?)))

Bevor nun ein die SECD-Maschine einen Term verarbeiten kann, mu


dieser erst in Maschinencode bersetzt werden. Dabei entsteht aus
Definition 17.4 direkt Daten- und Signaturdefinitionen fr Instruktionen
und Maschinencode:
; Eine Instruktion ist eins der folgenden:
; - ein Basiswert
; - eine Variable
; - eine Applikations-Instruktion
; - eine Instruktion fr eine primitive Applikation
; - eine Abstraktion
(define instruction
(signature
(mixed base
symbol
ap
tailap
prim
abs))

; Eine Maschinencode-Programm ist eine Liste von Instruktionen.


(define machine-code (signature (list-of instruction)))

Bei der Definition von Instruktionen ist wieder einiges Wunschdenken


im Spiel. Basiswerte und Variablen sind wie bei den Termen. Die rest-
lichen Flle werden durch eigene Datendefinitionen abgebildet. Wie
schon bei den leeren Bumen sind Record-Definitionen ohne Felder im
Spiel, die Fallunterscheidungen mglich machen:
; Eine Applikations-Instruktion ist ein Wert
; (make-ap)
(define-record-procedures ap
make-ap ap?
())
(: make-ap (-> ap))

; Die Instruktion fr eine primitive Applikation


Die SECD-Maschine 209

; ist ein Wert


; (real-make-prim op arity)
; wobei op ein Symbol und arity die Stelligkeit
; ist
(define-record-procedures prim
real-make-prim prim?
(prim-operator prim-arity))
(: make-prim (symbol natural -> prim))

; Eine Abstraktions-Instruktion ist ein Wert


; (make-abs v c)
; wobei v ein Symbol (fr eine Variable) und c
; Maschinencode ist
(define-record-procedures abs
make-abs abs?
(abs-variable abs-code))
(: make-abs (symbol machine-code -> abs))

Da die Stelligkeit eines Primitivs dem Primitiv fest zugeordnet ist, ist
eine Hilfsprozedur ntzlich, die bei der Erzeugung eines Werts der
Sorte prim die Stelligkeit ergnzt. Glcklicherweise haben alle oben
eingefhrten Primitive die gleiche Stelligkeit:
; Primitiv erzeugen
(: make-prim (symbol -> prim))
(define make-prim
(lambda (s)
(real-make-prim s 2)))

Die Einfhrung von Primitive mit anderen Stelligkeiten ist Gegenstand


von Aufgabe 17.6.

17.4.2 bersetzung in Maschinencode

Nun, da sowohl Terme als auch der Maschinencode Datendefinitionen


haben, ist es mglich, die bersetzung zu programmieren. Hier sind
Kurzbeschreibung, Signatur und Gerst:
; Term in Maschinencode bersetzen
(: term->machine-code (term -> machine-code))
(define term->machine-code
(lambda (e)
...))

Da es sich bei term um gemischte Daten handelt, mu wie immer


eine Verzweigung den Rumpf der Prozedur bilden:
(define term->machine-code
(lambda (e)
(cond
((symbol? e) ...)
((application? e) ...)
((abstraction? e) ...)
((base? e) ...)
((primitive-application? e) ...))))
210 Kapitel 17

Die Implementierung entspricht in den einzelnen Fllen genau der


bersetzungsfunktion J K. Die Flle fr Variablen und Basiswerte sind,
genau wie dort, trivial:

(define term->machine-code
(lambda (e)
(cond
((symbol? e) (list e))
((base? e) (list e))
...)))

Bei regulren Applikationen werden Operator und Operand bersetzt,


und das ganze zusammen mit einer ap-Instruktion zu einer Liste zu-
sammengesetzt:

(define term->machine-code
(lambda (e)
(cond
...
((application? e)
(append (term->machine-code (first e))
(append (term->machine-code (first (rest e)))
(list (make-ap)))))
...)))

Bei den primitiven Applikationen werden erst einmal die Operanden


in Maschinencode bersetzt, die Resultate aneinandergehngt, und
schlielich kommt noch eine prim-Instruktion ans Ende:

(define term->machine-code
(lambda (e)
(cond
...
((primitive-application? e)
(append
(append-lists
(map term->machine-code (rest e)))
(list (make-prim (first e)))))
...)))

Dieses Stck Code benutzt die Hilfsprozedur append-lists, die aus


einer Liste von Listen eine einzelne Liste macht, indem die Elemente
aneinandergehngt werden:

; die Elemente einer Liste von Listen aneinanderhngen


(: append-lists ((list-of (list-of %a)) -> (list-of %a)))
(define append-lists
(lambda (l)
(fold () append l)))

Zurck zur bersetzung: Eine Abstraktionen wird direkt in eine abs-


Instruktion bersetzt, wobei der Rumpf selbst noch in Maschinencode
bersetzt wird:
Die SECD-Maschine 211

(define term->machine-code
(lambda (e)
(cond
...
((abstraction? e)
(list
(make-abs (first (first (rest e)))
(term->machine-code
(first (rest (rest e))))))))))

17.4.3 Zustandsbergang und Auswertung

Da nun alle -Terme in Maschinencode-Programme bersetzt werden


knnen, ist jetzt die eigentliche SECD-Maschine an der Reihe. Hier sind
erst einmal einige neue Datendefinitionen fllig. Zunchst einmal die
Menge S der Stacks:

; Ein Stack ist eine Liste von Werten


(define stack (signature (list-of value)))

Die Definition von Werten W kommt etwas spter an die Reihe.


Umgebungen aus der Menge E sind mathematisch gesehen Mengen
aus Tupeln. In der Implementierung werden sie dargestellt aus Listen
von Bindungen, wobei jede Bindung einem Tupel aus der mathemati-
schen Definition entspricht:

; Eine Umgebung ist eine Liste von Bindungen.


; Dabei gibt es fr jede Variable nur eine Bindung.
(define environment (signature (list-of binding)))

; Eine Bindung (Name: binding) ist ein Wert


; (make-binding v x)
; wobei v der Name einer Variablen und x der dazugehrige Wert ist.

(define-record-procedures binding
make-binding binding?
(binding-variable binding-value))
(: make-binding (symbol value -> binding))

Die leere Umgebung wird fter bentigt und wird darum schon vorde-
finiert:

; die leere Umgebung


(define the-empty-environment empty)

Zwei Operationen gibt es fr eine Umgebung e: die Erweiterung um


eine Bindung e[v 7 w] und das Nachschauen einer Bindung e(v).
Zunchst die Erweiterung: die Implementierung entspricht genau der
mathematischen Definition: zunchst wird eine eventuell vorhandene
Bindung fr v entfernt, dann eine neue Bindung hinzugefgt:

; eine Umgebung um eine Bindung erweitern


(: extend-environment (environment symbol value -> environment))
(define extend-environment
212 Kapitel 17

(lambda (e v w)
(make-pair (make-binding v w)
(remove-environment-binding e v))))

Fr das Entfernen der alten Bindung ist die Hilfsprozedur remove-environment-binding


zustndig. Sie folgt einmal mehr strikt der Konstruktionsanleitung fr
Prozeduren, die Listen akzeptieren:
; die Bindung fr eine Variable aus einer Umgebung entfernen
(: remove-environment-binding (environment symbol -> environment))
(define remove-environment-binding
(lambda (e v)
(cond
((empty? e) empty)
((pair? e)
(if (equal? v (binding-variable (first e)))
(rest e)
(make-pair (first e)
(remove-environment-binding (rest e) v)))))))

Auch die zweite Operation, das Nachschauen einer Bindung in der


Umgebung, folgt der Konstruktionsanleitung:
; die Bindung fr eine Variable in einer Umgebung finden
(: lookup-environment (environment symbol -> value))
(define lookup-environment
(lambda (e v)
(cond
((empty? e) (violation "unbound variable"))
((pair? e)
(if (equal? v (binding-variable (first e)))
(binding-value (first e))
(lookup-environment (rest e) v))))))

Damit sind die Operationen auf Umgebungen abgeschlossen. Als nch-


stes sind Dumps an der Reihe: D ist als Folge von Tupeln S E C
definiert, auch genannt Frames. Hier sind Daten- und Record-Definition:
; Ein Dump ist eine Liste von Frames

; Ein Frame ist ein Wert


; (make-frame s e c)
; wobei s ein Stack, e eine Umgebung und c Maschinencode ist.
(define-record-procedures frame
make-frame frame?
(frame-stack frame-environment frame-code))
(: make-frame (stack environment machine-code -> frame))

Schlielich fehlt noch eine Reprsentation fr die Menge W der Werte:


Ein Wert ist entweder ein Basiswert oder eine Closure. Basiswerte
wurden bereits in Abschnitt 17.4.1 definiert; es fehlen noch Closures, die
Tupel aus V C E sind. Hier sind die entsprechenden Definitionen:
; Ein SECD-Wert ist ein Basiswert oder eine Closure
(define value (signature (mixed base closure)))
Die SECD-Maschine 213

; Eine Closure ist ein Wert


; (make-closure v c e)
; wobei v die Variable der Lambda-Abstraktion,
; c der Code der Lambda-Abstraktion
; und e ein Environment ist.
(define-record-procedures closure
make-closure closure?
(closure-variable closure-code closure-environment))
(: make-closure (symbol machine-code environment -> closure))

Mit Hilfe dieser Definitionen ist es mglich, eine Daten- und eine
Record-Definition fr die Zustnde der SECD-Maschine anzugeben,
also die Tupel aus S E C D:
; Ein SECD-Zustand ist ein Wert
; (make-secd s e c d)
; wobei s ein Stack, e eine Umgebung, c Maschinencode
; und d ein Dump ist
(define-record-procedures secd
make-secd secd?
(secd-stack secd-environment secd-code secd-dump))
(: make-secd (stack environment machine-code dump -> secd))

Damit kann es an die Zustandsbergangsfunktion gehen. Sie wird


als Prozedur realisiert, die einen SECD-Zustand akzeptiert und einen
neuen liefert. Hier sind Kurzbeschreibung, Signatur und Gerst:
; Zustandsbergang berechnen
(: secd-step (secd -> secd))
(define secd-step
(lambda (state)
...))

Entsprechend den Regeln der SECD-Maschine mu der Rumpf der


Prozedur eine Verzeigung zwischen den verschiedenen Fllen bei der
Code-Komponente von state sein. Diese folgen den Konstruktionsan-
leitungen fr Listen und fr gemischte Daten. Es ist bereits an den
Regeln abzulesen, da alle Regeln Zugriff auf die Komponenten von
state bentigen. Fr diese werden gleich am Anfang lokale Variablen
angelegt:
(define secd-step
(lambda (state)
(let ((stack (secd-stack state))
(environment (secd-environment state))
(code (secd-code state))
(dump (secd-dump state)))
(cond
((pair? code)
(cond
((base? (first code)) ...)
((symbol? (first code)) ...)
((prim? (first code)) ...)
214 Kapitel 17

((abs? (first code)) ...)


((ap? (first code)) ...)))
((empty? code) ...)))))

In diesem Gerst werden nun die Regeln direkt abgebildet. Hier zur
Erinnerung noch einmal die erste Regel fr Basiswerte:

(s, e, bc, d) , (bs, e, c, d)

Hier der passende Code dafr:


(define secd-step
(lambda (state)
...
(cond
((base? (first code))
(make-secd (make-pair (first code) stack)
environment
(rest code)
dump))
...)
...))

Hier die Regel fr Variablen:

(s, e, vc, d) , (e(v)s, e, c, d)

Hier der entsprechende Code:


(define secd-step
(lambda (state)
...
(cond
((symbol? (first code))
(make-secd (make-pair
(lookup-environment environment (first code))
stack)
environment
(rest code)
dump))
...)
...))

Die Regel fr primitive Applikationen ist etwas aufwendiger:

(bk . . . b1 s, e, primFk c, d) , (bs, e, c, d)


wobei F k k und FB (b1 , . . . , bk ) = b

Fr die Implementierung werden Hilfsprozeduren gebraucht, welche


die Argumente vom Stack holen und in der Reihenfolge umdrehen,
die Argumente vom Stack entfernen und schlielich die eigentliche
-Transition berechnen:
(define secd-step
(lambda (state)
Die SECD-Maschine 215

...
(cond
...
((prim? (first code))
(make-secd (make-pair
(apply-primitive
(prim-operator (first code))
(take-reverse (prim-arity (first code)) stack))
(drop (prim-arity (first code)) stack))
environment
(rest code)
dump))
...)
...))

Die Prozedur drop ist gerade die in Aufgabe ?? geforderte Prozedur:

; die ersten Elemente einer Liste weglassen


(: drop (natural (list-of %a) -> (list-of %a)))

Die take-reverse-Prozedur ist das Pendant zu drop, das die ersten n


Elemente einer Liste in umgekehrter Reihenfolge liefert. Dies ist am
einfachsten ber eine endrekursive Hilfsprozedur zu erledigen aus
Kapitel 12 ist ja bekannt, da bei endrekursiver Konstruktion von Listen
gerade immer die Reihenfolge umgedreht wird:
; die ersten Elemente einer Liste in umgekehrter Reihenfolge berechnen
(: take-reverse (natural (list-of %a) -> (list-of %a)))
(define take-reverse
(lambda (n l)
;; (: loop (natural (list-of %a) (list-of %a) -> (list-of %a)))
(letrec ((loop (lambda (n l r)
(if (= n 0)
r
(loop (- n 1) (rest l) (make-pair (first l) r))))))
(loop n l ()))))

Aus einem Primitiv und einer Liste von Argumenten berechnet apply-primitive
das Resultat der primitiven Applikation. Dabei handelt es sich bei
primitive um eine Fallunterscheidung, der Rumpf der Prozedur ist also
eine entsprechende Verzweigung:
; Delta-Transition berechnen
(: apply-primitive (primitive (list-of value) -> value))
(define apply-primitive
(lambda (p args)
(cond
((equal? p +)
(+ (first args) (first (rest args))))
((equal? p -)
(- (first args) (first (rest args))))
((equal? p =)
(= (first args) (first (rest args))))
((equal? p *)
216 Kapitel 17

(* (first args) (first (rest args))))


((equal? p /)
(/ (first args) (first (rest args)))))))

Die Regel fr Abstraktionen macht aus einer Abstraktion eine Closure:


(s, e, (v, c0 )c, d) , ((v, c0 , e)s, e, c, d)
Der Code macht dies genauso:
(define secd-step
(lambda (state)
...
(cond
...
((abs? (first code))
(make-secd (make-pair
(make-closure (abs-variable (first code))
(abs-code (first code))
environment)
stack)
environment
(rest code)
dump)))
...)))

Hier die Regel fr die Applikation:


(w(v, c0 , e0 )s, e, ap c, d) , (e, e0 [v 7 w], c0 , (s, e, c)d)
Hier der Code dazu:
(define secd-step
(lambda (state)
...
(cond
...
((ap? (first code))
(let ((closure (first (rest stack))))
(make-secd empty
(extend-environment
(closure-environment closure)
(closure-variable closure)
(first stack))
(closure-code closure)
(make-pair
(make-frame (rest (rest stack))
environment (rest code))
dump))))
...)
...))

Schlielich bleibt noch der Code fr die Rckgabe eines Wertes von
einer Prozedur. Hier ist die Regel:
(w, e, e, (s0 , e0 , c0 )d) , (ws0 , e0 , c0 , d)
Hier ist der Code dazu:
Die SECD-Maschine 217

(define secd-step
(lambda (state)
...
(cond
...
((empty? code)
(let ((f (first dump)))
(make-secd
(make-pair (first stack)
(frame-stack f))
(frame-environment f)
(frame-code f)
(rest dump)))))
...))

Damit die SECD-Maschine in Betrieb genommen werden kann, mu


ein Term e noch in einen Anfangszustand (e, , JeK, e) bersetzt werden.
Das erledigt folgende Hilfsprozedur:
; Aus Term SECD-Anfangszustand machen
(: inject-secd (term -> secd))
(define inject-secd
(lambda (e)
(make-secd empty
the-empty-environment
(term->machine-code e)
empty)))

Damit lt sich die Maschine schon ausprobieren:


(secd-step (inject-secd (+ 1 2)))
,#<record:secd (1) () (2 #<record:prim + 2>) ()>
(secd-step (secd-step (inject-secd (+ 1 2))))
,#<record:secd (2 1) () (#<record:prim + 2>) ()>
(secd-step (secd-step (secd-step (inject-secd (+ 1 2)))))
,#<record:secd (3) () () ()>
Es fehlt noch die Auswertungsfunktion evalSECD , die eine Hilfsprozedur
bentigt, um die reflexiv-transitive Hlle des Zustandsbergangs ,
bentigt:
; bis zum Ende Zustandsbergnge berechnen
(: secd-step* (secd -> secd))
(define secd-step*
(lambda (state)
(if (and (empty? (secd-code state))
(empty? (secd-dump state)))
state
(secd-step* (secd-step state)))))

Die Auswertungsfunktion orientiert sich direkt an der mathematischen


Definition:
; Evaluationsfunktion zur SECD-Maschine berechnen
(: eval-secd (term -> (mixed value (one-of function))))
218 Kapitel 17

(define eval-secd
(lambda (e)
(let ((val (first
(secd-stack
(secd-step*
(inject-secd e))))))
(if (base? val)
val
proc))))

Damit luft die SECD-Maschine:

(eval-secd (((lambda (x) (lambda (y) (+ x y))) 1) 2))


,3

17.5 Die endrekursive SECD-Maschine

Die SECD-Maschine hat einen Schnheitsfehler: Bei endkursiven Ap-


plikationen sollte sie eigentlich, wie in Scheme, keinerlei zustzlichen
Platz verbrauchen, da kein Kontext anfllt. Folgende Beispielauswer-
tung fr den Term (x.x x ) (x.x x ) zeigt aber, da der Zustand mit
fortschreitender Auswertung immer grer wird:
(e, , ( x, x x ap) ( x, x x ap) ap, e)
, (( x, x x ap, ), , ( x, x x ap) ap, e)
, (( x, x x ap, ) ( x, x x ap, ), , ap, e)
, (e, {( x, ( x, x x ap, ))}, x x ap, (e, , e))
, (( x, x x ap, ), {( x, ( x, x x ap, ))}, x ap, (e, , e))
, (( x, x x ap, ) ( x, x x ap, ), {( x, ( x, x x ap, ))}, ap, (e, , e))
, (e, {( x, ( x, x x ap, ))}, x x ap, (e, {( x, ( x, x x ap, ))}, e) (e, , e))
, (( x, x x ap, ), {( x, ( x, x x ap, ))}, x ap, (e, {( x, ( x, x x ap, ))}, e) (e, , e))
, (( x, x x ap, ) ( x, x x ap, ), {( x, ( x, x x ap, ))}, ap, (e, {( x, ( x, x x ap, ))}, e) (e, , e))
, (e, {( x, ( x, x x ap, ))}, x x ap, (e, {( x, ( x, x x ap, ))}, e) (e, {( x, ( x, x x ap, ))}, e) (e, , e))
, (( x, x x ap, ), {( x, ( x, x x ap, ))}, x ap, (e, {( x, ( x, x x ap, ))}, e) (e, {( x, ( x, x x ap, ))}, e) (e, , e))
...

Damit ist die SECD-Maschine, so wie ist, als Ausfhrungsmodell fr


Scheme ungeeignet. Dieses Manko lt sich zum Glck reparieren:
Die SECD-Maschine mu endrekursive und normale Applikationen
unterschiedlich behandeln. Dazu wird eine neue Instruktion namens
tailap eingefhrt, die wie ap eine Applikation durchfhrt, aber eine
endrekursive Applikation signalisiert:

I = ...
{tailap}

Als nchstes mu die bersetzungsfunktion von Termen in Maschi-


nencode gendert werden: Applikationen, die Kontext um sich herum
haben, sollen mit ap bersetzt werden, solche ohne Kontext mit tailap.
Da der Applikation allein der Kontext nicht anzusehen ist, sondern nur
dem Term drumherum, wird die bersetzungsfunktion J K in zwei
Teile aufgespalten: fr einen Term e wird die Auswertungsfunktion J K
immer dann benutzt, wenn um e Kontext steht. Eine weitere Funktion
J K0 wird immer dann aufgerufen, wenn kein Kontext drumherum steht.
Kontext entsteht seinerseits immer durch Funktionsapplikationen.
Bei der Auswertung eines Terms (e0 e1 ) mu nach e0 noch e1 ausgewer-
tet werden, und nach Auswertung von e1 mu noch die Applikation
durchgefhrt werden. Sowohl e0 als auch e1 stehen in Kontext. hnlich
ist es bei den Argumenten von primitiven Applikationen.
Die SECD-Maschine 219

Auf der anderen Seite schneiden Abstraktionen fr ihren Rumpf den


Kontext erst einmal ab: Der Rumpf einer Abstraktion kommt schlielich
bei der Auswertung der Abstraktion noch gar nicht zum Zug. Ob
er Kontext hat oder nicht, entscheidet sich erst bei der Applikation.
Dementsprechend schalten Applikationen und Abstraktionen zwischen
den beiden Funktionen J K und J K0 hin und her:

: L C
A
JK


b falls e =bB
v falls e =vV



def
JeK = Je0 K Je1 K ap falls e = ( e0 e1 )

Je1 K . . . Jek K primFk falls e = ( F e1 . . . e k )




(v, Je K0 )

falls e = v.e0
0

J K0 : LA C



b falls e =bB
v falls e =vV



def
JeK0 = Je0 K Je1 K tailap falls e = ( e0 e1 )

1 K . . . Jek K primFk falls e = ( F e1 . . . e k )


Je
(v, Je K0 )

falls e = v.e0
0

Die bersetzungsfunktion hat die eigentliche Arbeit geleistet: Jetzt mu


nur noch eine Zustandsbergangsregel her, die tailap verarbeitet. Die-
se ergibt sich direkt aus den Regeln fr ap und die Rckgabe eines
Wertes: tailap funktioniert so, wie ap direkt gefolgt von der Rckgabe-
regel. Hier sind die beiden Regeln noch einmal zur Erinnerung:

(w(v, c0 , e0 )s, e, ap c, d) , (e, e0 [v 7 w], c0 , (s, e, c)d)


(w, e, e, (s0 , e0 , c0 )d) , (ws0 , e0 , c0 , d)

Da die erste Regel ein neues Dump-Frame erzeugt und die zweite ein
Dump-Frame vernichtet, entfllt diese Arbeit in der Regel fr tailap:

(w(v, c0 , e0 )s, e, tailap c, d) , (s, e0 [v 7 w], c0 , d)

Damit luft das Beispiel zwar immer noch endlos, aber immerhin, ohne
immer mehr Platz zu verbrauchen:
, (e, , ( x, x x tailap) ( x, x x tailap) ap, e)
, (( x, x x tailap, ), , ( x, x x tailap) ap, e)
, (( x, x x tailap, ) ( x, x x tailap, ), , ap, e)
, (e, {( x, ( x, x x tailap, ))}, x x tailap, (e, , e))
, (( x, x x tailap, ), {( x, ( x, x x tailap, ))}, x tailap, (e, , e))
, (( x, x x tailap, ) ( x, x x tailap, ), {( x, ( x, x x tailap, ))}, tailap, (e, , e))
, (e, {( x, ( x, x x tailap, ))}, x x tailap, (e, , e))
, (( x, x x tailap, ), {( x, ( x, x x tailap, ))}, x tailap, (e, , e))
, (( x, x x tailap, ) ( x, x x tailap, ), {( x, ( x, x x tailap, ))}, tailap, (e, , e))
, (e, {( x, ( x, x x tailap, ))}, x x tailap, (e, , e))
, (( x, x x tailap, ), {( x, ( x, x x tailap, ))}, x tailap, (e, , e))
, (( x, x x tailap, ) ( x, x x tailap, ), {( x, ( x, x x tailap, ))}, tailap, (e, , e))
, (e, {( x, ( x, x x tailap, ))}, x x tailap, (e, , e))
, (( x, x x tailap, ), {( x, ( x, x x tailap, ))}, x tailap, (e, , e))

Die Implementierung der endrekursiven SECD-Maschine ist Gegen-


stand von bungsaufgabe 17.3.
220 Kapitel 17

17.6 Der -Kalkl mit Zustand

Der bisher vorgestellte -Kalkl liefert keinerlei Erklrung fr das


Verhalten von Zuweisungen. Tatschlich hat sich schon in Abschnitt ??
angedeutet, da Zuweisungen die Formalisierung deutlich erschweren.
Mglich ist es trotzdem, und dieser Abschnitt zeigt, wie es geht.
Als erstes mu wieder einmal die Sprache des -Kalkls erweitert
werden, diesmal um set!-Ausdrcke:

Definition 17.5 (Sprache des angewandten -Kalkls mit Zustand LS )


Sei V eine abzhlbare Menge von Variablen. Sei B eine Menge von Ba-
siswerten mit void B. Sei fr eine natrliche Zahl n und i {1, . . . , n}
jeweils i eine Menge von i-stelligen Primitiva. Jedem Fi i ist eine
i-stellige Funktion FBi : B . . . B B ihre Operation zugordnet.
Seit A eine abzhlbare Menge von Adressen mit V A = ..
Die Sprache des angewandten -Kalkls mit Zustand, die Menge der
angewandten -Terme mit Zustand, LS , ist durch folgende Grammatik
definiert:

hLS i hV i
| (hLS i hLS i)
| (hV i.hLS i)
| h Bi
| (h1 i hLS i)
| (h2 i hLS i hLS i)
...
| (h n i hLS i ... hLS i) (n-mal)
| (set! hVi hLS i)

Der void-Wert wird als Rckgabewert von set!-Ausdrcken dienen.


Um Reduktionsregeln fr Zuweisungen zu bilden, ist es notwendig,
den Begriff des Speichers in den -Kalkl einzufhren: Im -Kalkl mit
Zustand stehen Variablen nicht mehr fr Werte, die fr sie eingesetzt
werden knnen, sondern fr Speicherzellen. Eine Speicherzelle ist ein Ort
im Speicher, der einen Wert aufnimmt, der auch wieder verndert wer-
den kann. Dabei wird jede Speicherzelle durch eine Adresse identifiziert.
Eine Adresse ist eine abstrakte Gre, es kommt also gar nicht darauf
an, um was fr eine Art Wert es sich handelt im realen Computer ist
eine Adresse in der Regel einfach eine Zahl. In diesem Abschnitt steht
A fr die Menge der Adressen, die abzhlbar sein sollte.
Ein Speicher aus der Menge M ist eine Zuordnung zwischen Adres-
sen aus A und Werten. Die Werte sind wie schon im normalen -Kalkl
die Basiswerte und die Abstraktionen hier bekommen sie, weil sie
eine Rolle in den Reduktionsregeln spielen, den Namen X:

M = P ( A X)
X = B {v.e|v.e LS }

Um Reduktionsregeln fr den -Kalkl mit Zustand zu formulieren,


mu LS noch erweitert werden, damit die Adressen ins Spiel kommen:
Adressen werden Terme und sind auf der linken Seite von Zuweisungen
zulssig:
Die SECD-Maschine 221

hLS i ...
| hAi
| (set! hAi hLS i)
Adressen tauchen dabei nur als Zwischenschritte bei der Reduktion
auf; sie sind nicht dafr gedacht, da sie der Programmierer in ein
Programm schreibt.
Da das bisherige Substitutionsprinzip bei Zuweisungen nicht mehr
funktioniert, reicht es nicht, die Reduktionsregeln fr den -Kalkl
mit Zustand einfach nur auf Termen auszudrcken: Ein Term, der ja
Adressen enthalten kann, ergibt nur Sinn, wenn er mit einem Speicher
kombiniert wird. Die Reduktionsregeln berfhren somit immer ein
Paar, bestehend aus einem Term und einem Speicher in ein ebensol-
ches Paar. Hierbei wird der Einfachheit halber kein Unterschied mehr
zwischen den verschiedenen Arten der Reduktion gemacht:

b, m a, m[ a 7 b] wobei a frisch
v.e, m a, m[ a 7 v.e] wobei a frisch
( a0 a1 ), m e[v 7 a], m[ a 7 m( a1 )] wobei m( a0 ) = v.e und a frisch
(set! a0 a1 ), m void, m[ a0 7 m( a1 )]
k
( F a1 . . . ak ), m a, m[ a 7 FB (b1 , . . . , bk )] wobei bi = m( ai ) B, a frisch
Die Formulierung a frisch bedeutet dabei, da a eine Adresse sein
sollte, die in m bisher noch nicht benutzt wurde. Die Operation m[ a 7
x ] ist hnlich wie bei Umgebungen definiert: der alte Speicherinhalt bei
a wird zunchst entfernt, und dann eine neue Zuordnung fr a nach x
hinzugefgt:
def
m[ a 7 x ] = (e \ {( a, x 0 )|( a, x 0 ) m) {( a, x )}

Die Regeln sind immer noch ber Substitution definiert, allerdings


werden fr Variablen jetzt nicht mehr Werte sondern Adressen einge-
setzt. Sie werden, wie beim normalen Call-by-Value-Kalkl auch, auf
Subterme fortgesetzt, die mglichst weit links innen stehen.
Im folgenden Beispiel stehen fettgedruckte Zahlen 0, 1 fr Adressen.
Die Redexe sind jeweils unterstrichen:
((x.((y.x )(set! x (+ x 1)))) 12),
(0 12), {(0, x.((y.x )(set! x (+ x 1))))}
(0 1), {(0, x.((y.x )(set! x (+ x 1)))), (1, 12)}
((y.2)(set! 2 (+ 2 1))), {(0, x.((y.x )(set! x (+ x 1)))), (1, 12), (2, 12)}
(3 (set! 2 (+ 2 1))),
{(0, x.((y.x )(set! x (+ x 1)))), (1, 12), (2, 12), (3, (y.2)}
(3 (set! 2 (+ 2 4))),
{(0, x.((y.x )(set! x (+ x 1)))), (1, 12), (2, 12), (3, (y.2), (4, 1)}
(3(set! 2 5)),
{(0, x.((y.x )(set! x (+ x 1)))), (1, 12), (2, 12), (3, (y.2), (4, 1), (5, 13)}
(3 void),
{(0, x.((y.x )(set! x (+ x 1)))), (1, 12), (2, 13), (3, (y.2), (4, 1), (5, 13)}
( 3 6 ),
{(0, x.((y.x )(set! x (+ x 1)))), (1, 12), (2, 13), (3, (y.2), (4, 1), (5, 13), (6, void)}
2,
{(0, x.((y.x )(set! x (+ x 1)))), (1, 12), (2, 13), (3, (y.2), (4, 1), (5, 13), (6, void)}
222 Kapitel 17

Der Endausdruck steht fr die Speicherzelle an Adresse 2, wo der Wert


13 steht. Es ist sichtbar, da die Auswertungsmaschinerie durch die
Einfhrung von Zustand deutlich komplizierter wird.

17.7 Die SECDH-Maschine

Die SECD-Maschine ist nicht mchtig genug, um den -Kalkl mit Zu-
stand zu modellieren: Es fehlt ein Speicher. Darum mu das Maschinen-
Pendant zum -Kalkl mit Zustand um eine Speicher-Komponente
erweitert werden: Heraus kommt die SECDH-Maschine, um die es in
diesem Abschnitt geht.
Der Maschinencode fr die SECDH-Maschine ist dabei genau wie
bei der SECD-Maschine, nur da eine spezielle Zuweisungsoperation
hinzukommt:

hIi ...
| :=

Die bersetzungsfunktion produziert diese neue Instruktion bei set!-


Ausdrcken:
(
def ...
=
v Je0 K := falls e = (set! v e0 )
JeK

Der Begriff der Adresse aus der Menge A wird direkt aus dem Kalkl
bernommen. hnlich wie im Kalkl landen Zwischenergebnisse nicht
mehr direkt auf dem Stack, sondern stattdessen landen ihre Adressen
im Speicher. Dementsprechend bilden nun Umgebungen Variablen auf
Adressen ab. Die neue Komponente H ist gerade der Speicher, auch
genannt Heap, der die Adressen auf Werte abbildet:

S = A
E = P (V A )
D = (S E C )
H = P (A W)
W = B (V C E )

Die Regeln fr die SECDH-Maschine sind analog zu den Regeln fr die


SECD-Maschine. Zwei Hauptunterschiede gibt es dabei:

Der Heap aus H gehrt nun zum Zustand dazu. Anders als die
Umgebung wird er nicht bei der Bildung von Closures eingepackt:
Stattdessen wird der Heap stets linear von links nach rechts durch
Regeln durchgefdelt.
Zwischenergebnisse nehmen stets den Umweg ber den Heap: Immer,
wenn ein neues Zwischenergebnis entsteht, wird es bei einer neuen
Adresse im Heap abgelegt. Auf dem Stack landen die Adressen der
Zwischenergebnisse.
Die SECD-Maschine 223

, P ((S E C D H ) (S E C D H ))
(s, e, bc, d, h) , ( as, e, c, d, h[ a 7 b])
wobei a frisch
(s, e, vc, d, h) , (e(v)s, e, c, d, h)
( ak . . . a1 s, e, primFk c, d, h) , ( as, e, c, d, h[ a 7 b])
wobei a frisch, bi = h( ai ) und F k k und FBk (b1 , . . . , bk ) = b
( a1 a0 s, e, :=c, d, h) , ( as, e, c, d, h[ a0 7 h( a1 )][ a 7 void])
wobei a frisch
(s, e, (v, c )c, d, h) , ( as, e, c, d, h[ a 7 (v, c0 , e)])
0

wobei a frisch
( a1 a0 s, e, apc, d, h) , (e, e0 [v 7 a], c0 , (s, e, c)d, h[ a 7 h( a1 )])
wobei a frisch und h( a0 ) = (v, c0 , e0 )
( a, e, e, (s0 , e0 , c0 )d, h) , ( as0 , e0 , c0 , d, h)

Entsprechend mu die Auswertungsfunktion das Endergebnis im


Heap nachschauen:

evalSECD L Z
(S
h( a) falls (e, , JeK, e, ) , ( a, e, e, e, h), h( a) B
evalSECD (e) =
proc falls (e, , JeK, e, ) , ( a, e, e, e, h ), h ( a ) = ( v, c, e0 )

17.8 Implementierung der SECDH-Maschine

Fr die Implementierung der SECDH-Maschine werden einige der Pro-


zeduren wiederverwendet, die fr die SECD-Maschine programmiert
wurden. Zunchst einmal mu genau wie bei der SECD-Maschine
erst einmal die bersetzung von Termen in Maschinencode realisiert
werden. Zuweisungsterme haben wie in Scheme die folgende Form:
(set! v e)
Das dazu passende Prdikat ist das folgende:
; Prdikat fr Zuweisungen
(: assignment? (%a -> boolean))
(define assignment?
(lambda (t)
(and (pair? t)
(equal? set! (first t)))))

(define assignment (signature (predicate assignment?)))

Mit Hilfe dieser Definition kann die Signaturdefinition von term erwei-
tert werden:
(define term
(signature
(mixed symbol
224 Kapitel 17

application
abstraction
base
primitive-application
assignment)))

Um zu vermeiden, da Zuweisungen mit regulren Applikationen


verwechselt werden, mu das Prdikat application? erweitert werden:
(define application?
(lambda (t)
(and (pair? t)
(not (equal? set! (first t)))
(not (equal? lambda (first t)))
(not (primitive? (first t))))))

Als nchstes wird die zustzliche :=-Instruktion reprsentiert. Hier sind


Daten- und Record-Definition:
; Eine Zuweisungs-Instruktion ist ein Wert
; (make-:=)
(define-record-procedures :=
make-:= :=?
())
(: make-:= (-> :=))

Die Signaturdefinition fr Maschinen-Instruktionen kann um := erwei-


tert werden:
(define instruction
(signature
(mixed base
symbol
ap
tailap
prim
abs
:=)))

Bei der bersetzung in Maschinencode kommt in term->machine-code


ein weiterer Zweig hinzu:
; Term in Maschinencode bersetzen
(: term->machine-code (term -> machine-code))
(define term->machine-code
(lambda (e)
(cond
...
((assignment? e)
(make-pair (first (rest e))
(append (term->machine-code (first (rest (rest e))))
(list (make-:=))))))))

Wie bei der SECD-Maschine werden die verschiedenen Mengendefini-


tionen erst einmal in Daten- und Record-Definitionen bersetzt. Das ist
fr Stacks, Umgebungen und Speicheradressen ganz einfach:
Die SECD-Maschine 225

; Ein Stack ist eine Liste aus Adressen.


(define stackh (signature (list-of address)))

; Eine Umgebung bildet Variablen auf Adressen ab.

; Eine Adresse ist eine ganze Zahl.


(define address (signature natural))

Die nderung in der Definition von Umgebungen bedingt eine nde-


rung der Signatur von make-binding:

(: make-binding (symbol address -> binding))

Bei der Reprsentation des Heaps ist wichtig, da eine Operation


zur Beschaffung frischer Adressen eingebaut wird. Aus diesem Grund
enthlt der Heap zustzlich zu den Zellen auch noch einen Zhler mit
der nchsten frischen Adresse:

; Ein Heap ist ein Wert


; (make-heap s n)
; wobei n die nchste freie Adresse ist und s eine Liste
; von Zellen.
(define-record-procedures heap
make-heap heap?
(heap-cells heap-next))
(: make-heap ((list-of cell) natural -> heap))

Der leere Heap wird schon einmal vorfabriziert:

(define the-empty-heap (make-heap empty 0))

Jede Zelle ordnet einer Adresse einen Wert zu:

; Eine Zelle ist ein Wert


; (make-cell a w)
; wobei a eine Adresse und w ein Wert ist
(define-record-procedures cell
make-cell cell?
(cell-address cell-value))
(: make-cell (address value -> cell))

Die Prozedur heap-store, erweitert den Heap um eine Zelle entspre-


chend der mathematischen Definition:

; Wert im Speicher ablegen


(: heap-store (heap address value -> heap))
(define heap-store
(lambda (h a w)
(make-heap (make-pair (make-cell a w)
(remove-cell a (heap-cells h)))
...)))

Die Ellipse steht fr die nchste frische Adresse: Wenn die bisherige
frische Adresse in heap-store belegt wird, so mu eine neue frische
Adresse gewhlt werden:
226 Kapitel 17

(define heap-store
(lambda (h a w)
(make-heap (make-pair (make-cell a w)
(remove-cell a (heap-cells h)))
(let ((next (heap-next h)))
(if (= a next)
(+ next 1)
next)))))

Es fehlt noch die Hilfsprozedur remove-cell:


; Zelle zu einer Adresse entfernen
(: remove-cell (address (list-of cell) -> (list-of cell)))
(define remove-cell
(lambda (a c)
(cond
((empty? c) empty)
((pair? c)
(if (= a (cell-address (first c)))
(rest c)
(make-pair (first c)
(remove-cell a (rest c))))))))

Als nchstes ist die Operation an der Reihe, die den Wert, der an einer
Adresse im Heap gespeichert ist. Die Prozedur heap-lookup benutzt
eine Hilfsprozedur cells-lookup, um in der Liste von Zellen nach der
richtigen zu suchen:
; den Wert an einer Adresse im Heap nachschauen
(: heap-lookup (heap address -> value))
(define heap-lookup
(lambda (h a)
(cells-lookup (heap-cells h) a)))

; den Wert an einer Adresse in einer Liste von Zellen nachschauen


(: cells-lookup ((list-of cell) address -> value))
(define cells-lookup
(lambda (c a)
(cond
((empty? c) (violation "unassigned address"))
((pair? c)
(if (= a (cell-address (first c)))
(cell-value (first c))
(cells-lookup (rest c) a))))))

Schlielich fehlt noch eine Reprsentation fr den void-Wert:


; Ein void-Wert ist ein Wert
; (make-void)
(define-record-procedures void
make-void void?
())
(: make-void (-> void))

Auch hier wird nur ein void-Wert bentigt, der vorfabriziert wird:
Die SECD-Maschine 227

(define the-void (make-void))

Der Zustand fr die SECDH-Maschine wird genau wie bei der SECD-
Maschine reprsentiert, ergnzt um die Komponente fr den Heap:

; Ein SECDH-Zustand ist ein Wert


; (make-secd s e c d h)
; wobei s ein Stack, e eine Umgebung, c Maschinencode,
; d ein Dump und h ein Speicher ist.
(define-record-procedures secdh
make-secdh secdh?
(secdh-stack secdh-environment secdh-code secdh-dump secdh-heap))
(: make-secdh (stackh environment machine-code dump heap -> secdh))

Die Implementierung der Zustandsbergangsfunktion hat exakt die


gleiche Struktur wie die Implementierung der SECD-Maschine und hlt
sich eng an die mathematische Definition der Regeln:

; eine Zustandstransition berechnen


(: secdh-step (secdh -> secdh))
(define secdh-step
(lambda (state)
(let ((stack (secdh-stack state))
(environment (secdh-environment state))
(code (secdh-code state))
(dump (secdh-dump state))
(heap (secdh-heap state)))
(cond
((pair? code)
(cond
((base? (first code))
(let ((a (heap-next heap)))
(make-secdh
(make-pair a stack)
environment
(rest code)
dump
(heap-store heap a (first code)))))
((symbol? (first code))
(make-secdh
(make-pair (lookup-environment environment (first code))
stack)
environment
(rest code)
dump
heap))
((prim? (first code))
(let ((a (heap-next heap)))
(make-secdh
(make-pair a
(drop (prim-arity (first code)) stack))
environment
(rest code)
228 Kapitel 17

dump
(heap-store heap a
(apply-primitive
(prim-operator (first code))
(map (lambda (address)
(heap-lookup heap address))
(take-reverse (prim-arity (first code)) stack)))))))
((:=? (first code))
(let ((a (heap-next heap)))
(make-secdh
(make-pair a (rest (rest stack)))
environment
(rest code)
dump
(heap-store
(heap-store heap
(first (rest stack))
(heap-lookup heap (first stack)))
a the-void))))
((abs? (first code))
(let ((a (heap-next heap)))
(make-secdh
(make-pair a stack)
environment
(rest code)
dump
(heap-store heap a
(make-closure (abs-variable (first code))
(abs-code (first code))
environment)))))
((ap? (first code))
(let ((closure (heap-lookup heap (first (rest stack))))
(a (heap-next heap)))
(make-secdh empty
(extend-environment
(closure-environment closure)
(closure-variable closure)
a)
(closure-code closure)
(make-pair
(make-frame (rest (rest stack)) environment (rest code))
dump)
(heap-store heap a (heap-lookup heap (first stack))))))
((tailap? (first code))
(let ((closure (heap-lookup heap (first (rest stack))))
(a (heap-next heap)))
(make-secdh (rest (rest stack))
(extend-environment
(closure-environment closure)
(closure-variable closure)
a)
(closure-code closure)
Die SECD-Maschine 229

dump
(heap-store heap a
(heap-lookup heap (first stack))))))))
((empty? code)
(let ((f (first dump)))
(make-secdh
(make-pair (first stack)
(frame-stack f))
(frame-environment f)
(frame-code f)
(rest dump)
heap)))))))

Es bleibt die Auswertungsfunktion, die ebenfalls genau wie bei der


SECD-Maschine realisiert wird:

; aus Term SECDH-Anfangszustand machen


(: inject-secdh (term -> secdh))
(define inject-secdh
(lambda (e)
(make-secdh empty
the-empty-environment
(term->machine-code e)
empty
the-empty-heap)))

; bis zum Ende Zustandsbergnge berechnen


(: secdh-step* (secdh -> secdh))
(define secdh-step*
(lambda (state)
(if (and (empty? (secdh-code state))
(empty? (secdh-dump state)))
state
(secdh-step* (secdh-step state)))))

; Evaluationsfunktion zur SECD-Maschine berechnen


(: eval-secdh (term -> (mixed value (one-of function))))
(define eval-secdh
(lambda (e)
(let ((final (secdh-step* (inject-secdh e))))
(let ((val (heap-lookup (secdh-heap final)
(first (secdh-stack final)))))
(if (base? val)
val
proc)))))

bungsaufgaben

Aufgabe 17.1 bersetzen Sie folgende Lambda-Terme in die Zwischen-


reprsentation der SECD-Maschine:

1. (xy.(+ x y)) ( 5 6) 23
230 Kapitel 17

2. (x.(! x )) (xy.(&& x y)) ((xy.(> x y)) 23 42) true


3. (xy. y x x ) (z. z) (yz. (y y) (y z))

Dabei steht ! fr das boolesche not und && fr das boolesche and.

Aufgabe 17.2 Betrachten Sie folgendes SECD-Programm:

( f , ( x, (y, f x ap y ap))) ( a, (b, a b prim+ )) ap 23 ap 42 ap

1. bersetzen Sie das SECD-Programm in den entsprechenden LA -


Term.
2. Werten Sie das SECD-Programm aus und geben Sie die einzelnen
Auswertungsschritte an!

Aufgabe 17.3 Erweitern Sie die Implementierung der SECD-Maschine


um korrekte Behandlung der Endrekursion! Erweitern Sie dazu zu-
nchst die Datendefinition fr Maschinencode. Implementieren Sie dann
die bersetzung von -Termen fr die endrekursive SECD-Maschine.
Erweitern Sie schlielich die Zustandsbergangsfunktion um einen Fall
fr die tailap-Instruktion.

Aufgabe 17.4 Die um Endrekursion erweiterte SECD-Maschine fhrt


eine neue Maschinencode-Instruktion tailap ein. Dies ist aber nicht
unbedingt ntig. Formulieren Sie die Zustandsbergangsregeln der
SECD-Maschine mit Endrekursion so um, da die Funktionalitt, also
insbesondere die richtige Behandlung endrekursiver Applikationen,
auch ohne das das zustzliche Schlsselwort tailap erhalten bleibt.

Aufgabe 17.5 Zeigen Sie in der um Endrekursion erweiterten SECD-


Maschine, da tailap immer am Ende steht, also tatschlich keinen
Kontext besitzt.

Aufgabe 17.6 Erweitern Sie die SECD-Maschine um Primitive anderer


Stelligkeiten, z.B. abs oder odd?.

Aufgabe 17.7 ndern Sie die Implementierung der SECDH-Maschine


dahingehend, da sie Endrekursion korrekt behandelt.

Aufgabe 17.8 Abstrahieren Sie ber remove-environment-binding und


remove-cell.

Aufgabe 17.9 Erweitern Sie den angewandten -Kalkl um Abstraktio-


nen und Applikationen mit mehr als einem Parameter. Erweitern Sie
die SECD-Maschine und ihre Implementierung entsprechend.

Aufgabe 17.10 Erweitern Sie den angewandten -Kalkl um binre


Verzweigungen analog zu if. Erweitern Sie entsprechend die SECD-
Maschine und ihre Implementierung.

Aufgabe 17.11 Begin lt sich im angewandten -Kalkl als syntakti-


scher Zucker auffassen: Wie mten begin-Ausdrcke in die Sprache
des Kalkls bersetzt werden?
Die SECD-Maschine 231

Aufgabe 17.12 Anstatt Umgebungen durch Listen von Bindungen zu


reprsentieren, ist es auch mglich, Prozeduren zu verwenden, so da
lookup-environment folgendermaen aussieht:

(define lookup-environment
(lambda (e v)
(e v)))

Ergnzen Sie eine passende Definition fr extend-environment.

Aufgabe 17.13 Auf den ersten Blick erscheint es etwas aufwendig, je-
desmal bei der Auswertung einer Abstraktion die gesamte Umgebung
in die Closure einzupacken. Was wrde sich ndern, wenn dieser Schritt
weggelassen wrde, Closures also nur Variable und Maschinencode
fr den Rumpf enthalten wrden? Formulieren Sie die entsprechenden
Regeln fr die SECD-Maschine und ndern Sie die Implementierung
entsprechend. Funktioniert die SECD-Maschine nach der nderung
noch korrekt?
A Mathematische Grundlagen

Dieser Anhang erlutert die in diesem Buch verwendeten Begriffe und


Notationen aus der Mathematik.

1.1 Aussagenlogik

Eine Aussage ist ein Satz, der prinzipiell einen Wahrheitswert W (fr
wahr) oder F (fr falsch) besitzt.
Aus primitiven (elementaren) Aussagen werden mit Hilfe sogenannter
aussagenlogischer Junktoren zusammengesetzte Aussagen aufgebaut.
Es gibt zwei vordefinierte primitive Aussagen > und mit den Wahr-
heitswerten W bzw. F. Die wichtigsten Junktoren sind:
und (): a b hat den Wahrheitswert W genau dann, wenn a und b
beide den Wert W haben.
oder (): a b hat den Wahrheitswert W genau dann, wenn von a
und b mindestens eins den Wert W hat.
nicht (): a hat den Wahrheitswert W genau dann, wenn a den Wert
F hat.
In der Aussagenlogik gilt demnach das Prinzip, da der Wahrheits-
wert einer zusammengesetzten Aussage durch die Wahrheitswerte sei-
ner Bestandteile bestimmt ist.
Statt a wird gelegentlich auch a geschrieben.
Meistens werden logische Junktoren durch sogenannte Wahrheitstafeln
definiert:
W F W F
W W F W W W W F
F F F F W F F W
Andere Junktoren, die ebenfalls hufig verwendet werden, sind:
W F
impliziert (): W W F
F W W
a b spricht sich als wenn a, dann b oder aus a folgt b. F
W besitzt ebenso wie F F den Wahrheitswert W! In der formalen
Aussagenlogik folgt aus einer falschen Voraussetzung jede Folgerung.
W F
genau-dann-wenn (): W W F
F F W
234 Anhang A

Hufig werden Wahrheitstafeln auch in einer etwas ausfhrlicheren


Form notiert, wie im folgenden gezeigt. Dabei sind die Wahrheitstafeln
fr alle vorgestellten Junktoren in einer Tabelle zusammengefat:
a b ab ab a ab ab
W W W W F W W
W F F W F F F
F W F W W W F
F F F F W W W
Zur Einsparung von Klammern wird vereinbart, da am strksten
bindet, gefolgt von , dann , dann und zum Schlu .
Eine zusammengesetzte Aussage heit allgemeingltig oder eine Tau-
tologie, wenn sie stets den Wahrheitswert W besitzt, unabhngig vom
Wahrheitswert ihrer elementaren Aussagen. Beispiele fr Tautologien
sind etwa a a (Satz vom ausgeschlossenen Dritten) und a a (Satz
vom Widerspruch). Zwei Aussagen a und b heien quivalent, wenn
a b eine Tautologie ist.
Es ist mglich, jede aussagenlogische Aussage durch Wahrheitstafeln
auf ihre Allgemeingltigkeit hin zu berprfen. Auch die quivalenz
von Ausdrcken lt sich durch Wahrheitstafeln berprfen. In der
Regel ist es jedoch einfacher, mit diesen Aussagen formal zu rechnen.
Die folgenden Tautologien stellen Rechenregeln fr die Aussagenlogik
dar:

Lemma 1.1 Fr Aussagen a, b, c gilt:


aa a aa a Idempotenzgesetze
( a b) c a (b c) ( a b) c a (b c) Assoziativgesetze
ab ba ab ba Kommutativgesetze
a ( a b) a a ( a b) a Absorptivgesetze
a (b c) ( a b) ( a c) a (b c) ( a b) ( a c) Distributivgesetze
ab ab ab ab DeMorgansche Gesetze
aa

1.2 Mengen

Die ursprngliche Definition des Begriffs Menge lautet:


Unter einer Menge verstehen wir eine Zusammenfassung von bestimmten wohl-
unterschiedenen Objekten unserer Anschauung oder unseres Denkens zu einem
Ganzen. (G. Cantor)

Die Objekte einer Menge M heien Elemente von M. Die Notation


x M bedeutet, da x ein Element von M ist, x 6 M, da x kein
Element von M ist.
In der Informatik wie in der Mathematik werden hufig Mengen
von Zahlen gebraucht. Fr die wichtigsten Zahlenmengen gibt es feste
Bezeichnungen. So bezeichnet N die Menge der natrlichen Zahlen; in
diesem Buch gilt 0 N. Z bezeichnet die Menge der ganzen Zahlen und
R die Menge der reellen Zahlen.
Endliche Mengen, also Mengen mit endlich vielen Elementen knnen
als Aufreihung ihrer Elemente aufgeschrieben werden:
M = {11, 13, 17, 19}.
Mathematische Grundlagen 235

Hufig werden Mengen jedoch auch durch eine bestimmte Eigenschaft


definiert, die ihre Elementen haben:

M = { x | x ist Primzahl, 10 x 20}.

Die leere Menge ist die Menge, die keine Elemente besitzt und wird
durch bezeichnet.
A heit Teilmenge von B, in Zeichen A B, wenn jedes Element von
A auch Element von B ist:
def
A B a A a B.

Zwei Mengen sind gleich, wenn sie die gleichen Elemente besitzen; dies
lt sich mit Hilfe der Teilmengenbeziehung auch so ausdrcken:

def
A = B A B und B A.

Hieraus ergibt sich fr die oben erwhnte Darstellung endlicher Mengen


z.B.
{11, 13, 17, 19} = {17, 13, 19, 11},
d.h. die Reihenfolge der Elemente ist unerheblich (bzw. es gibt gar keine
ausgezeichnete Reihenfolge) und

{11, 13, 17, 19} = {11, 13, 11, 17, 17, 11, 13, 19},

d.h. es spielt keine Rolle, wie oft ein bestimmtes Element erwhnt wird;
es ist trotzdem nur einmal in der Menge enthalten.
Die Notation A 6 B bedeutet, da A B nicht gilt, A 6= B, da
A = B nicht gilt. A heit echte Teilmenge von B, wenn A B, aber
A 6= B. Die Notation dafr ist A B. Es bedeutet B A, da A B
gilt, ebenso fr B A.
Die Vereinigung A B zweier Mengen A und B ist definiert durch

def
AB = { a | a A a B }.

Der Durchschnitt A B zweier Mengen A und B ist definiert durch

def
AB = { a | a A a B }.

Die Differenz A \ B zweier Mengen A und B ist definiert durch

def
A\B = { a | a A a 6 B }.

Definition 1.2 Das kartesische Produkt A B zweier Mengen A und B


ist definiert durch
def
AB = {( a, b) | a A, b B}.

Fr n 2 Mengen A1 , . . . , An ist definiert:

def
A1 A n = {( a1 , . . . , an ) | ai Ai }.
236 Anhang A

Fr eine Menge A und eine natrliche Zahl n 2 ist


n
def _
An = A A.

Um die Flle n = 0 und n = 1 nicht immer ausschlieen zu mssen,


wird auerdem definiert:
def
A1 = A
0 def
A = {()} .

A0 ist also eine einelementige Menge, deren einziges Element in ber-


einstimmung mit der Tupelschreibweise ( a1 , . . . , an ) mit () bezeichnet
wird.

Fr eine Menge A wird die Anzahl ihrer Elemente, ihre Mchtigkeit,


als | A| geschrieben. Fr unendliche Mengen wird | A| = definiert.
Fr eine Menge A heit

def
P ( A) = { T | T A}

die Potenzmenge von A. Fr endliche Mengen gilt |P ( A)| = 2| A| .

Definition 1.3 Fr T P ( A) ist das Komplement von T in A definiert


durch
def
T = A\T .

Lemma 1.4 Fr A, B P ( M ) gelten die sogenannten DeMorganschen


Gesetze:
AB = AB

AB = AB

Es ist blich, Teilmengen T P ( A) durch sog. charakteristische Funk-


tionen darzustellen:

Definition 1.5 Sei A eine Menge, T P ( A). Die charakteristische Funk-


tion von T ist definiert durch

T : A {0, 1}


def 1 falls x T
T (x) =
0 falls x 6 T

Ist umgekehrt f : A {0, 1} eine (totale) Abbildung, so lt sich


hieraus eine Menge T f P ( A) ableiten durch

def
T f = { x A | f ( x ) = 1}.

Die Zuordnung T 7 T ist bijektiv.


Mathematische Grundlagen 237

1.3 Prdikatenlogik

Viele Aussagen haben die Form es gibt (mindestens) ein Objekt (In-
dividuum) mit der Eigenschaft. . . oder fr alle Objekte aus einem
bestimmten Bereich gilt. . . . Solche Aussagen heien Prdikate und
fr ihre mathematische Behandlung werden sogenannte Quantoren
eingefhrt, und zwar der Allquantor (Universalquantor) und der Exi-
stenzquantor . Im folgenden werden Grobuchstaben zur Bezeichnung
von Prdikaten und Kleinbuchstaben fr Individuen verwendet. Die
Stelligkeit eines Prdikats ist die Anzahl der Individuen, ber die hier
eine Aussage gemacht wird.
Ist Q ein nstelliges Prdikat und sind x1 , . . . , xn Individuen eines
Individuenbereichs, so ist die Behauptung, da Q auf x1 , . . . , xn zutrifft
(abgekrzt Qx1 . . . xn ) eine prdikatenlogische Aussage. Mathematische
Ausdrcke wie x M oder n < m oder aussagenlogische Aussagen sind
ebenfalls prdikatenlogische Aussagen, nur in anderer Schreibweise.
Statt der Individuen selbst kommen in den Quantorenausdrcken
Individuenvariablen vor; diese sind Platzhalter fr die Individuen selbst.
Die prdikatenlogische Aussage x : Qx bedeutet: Fr alle Indivi-
duen a gilt die Aussage (das Prdikat) Q, wobei fr die Variable x
das Individuum a eingesetzt wird. Dementsprechend heit x : Qx:
Es gibt mindestens ein Individuum a, so da die Aussage Qa wahr
ist. Die Variable x heit in beiden Fllen eine gebundene Variable des
Prdikatsausdrucks. Variablen, die nicht gebunden sind, heien freie
Variablen.
Eine wichtige Eigenschaft der Quantoren beschreibt der folgende
Satz:

Satz 1.6 Ist Q ein einstelliges Prdikat, so ist die Aussage x : Qx


genau dann wahr, wenn x : Qx wahr ist. Umgekehrt ist die Aussage
( x : Qx ) genau dann wahr, wenn x : Qx wahr ist.
Im Prinzip wre es also mglich, mit nur einem der beiden Quantoren
auszukommen.
Hufig werden Einschrnkungen an den Individuenbereich gemacht,
z.B. in der Form
x M : y N : Pxy .
Dies dient als Abkrzung fr die kompliziertere Aussage

x : ( x M y N : Pxy)

oder die noch kompliziertere Aussage

x : ( x M y : (y N Pxy)) .

1.4 Multimengen

Mengen sind so definiert, da sie jedes Element nur einmal enthalten.


In einer Multimenge, kann jedes Element mit einer bestimmten Multi-
plizitt (Vielfachheit) vorkommen. Technisch werden Multimengen als
Kreuzprodukte M G (N \ {0}) beschrieben, wobei G die Grund-
menge heit und die natrliche Zahl die Multiplizitt jedes Elements
der Grundmenge angibt.
238 Anhang A

Die Schreibweise x M ist eine Abkrzung fr die Tatsache ( x, n)


M. Die Multiplizitt M( M, x ) eines Elements bzw. Nicht-Elements x
ist so definiert: (
def 0 x 6 M
M( M, x ) =
n ( x, n) M
Die mengentheoretischen Operationen sind wie folgt definiert:

def
PQ = {( x, m) | x P x Q, m = max(M( P, x ), M( Q, x ))}
def
PQ = {( x, m) | ( x, p) P, ( x, q) Q, m = min( p, q), m > 0}
def
P\Q = {( x, m) | ( x, p) P, m = p M( Q, x ), m > 0}

Dabei ist die positive Differenz mit dem Wert 0 fr q > p.

1.5 Relationen und Abbildungen

Definition 1.7 Eine (binre) Relation ist eine Teilmenge A B. Eine


alternative Notation fr ( a, b) ist ab. Fr eine Relation heit
def
1 = {(b, a) | ( a, b) }

die Umkehrrelation von .

Definition 1.8 Eine Relation A A heit


def
reflexiv fr alle a A gilt aa,
def
symmetrisch aus ab folgt ba,
def
antisymmetrisch aus ab und ba folgt a = b,
def
transitiv aus ab und bc folgt ac,
def
quivalenzrelation ist reflexiv, symmetrisch und transitiv.
quivalenzrelationen werden oft dazu verwendet, Elemente einer Men-
ge in quivalenzklassen einzuteilen. Die quivalenzklasse [ a] eines Ele-
ments a A bezglich einer quivalenzrelation = ist die Menge aller
Elemente b A mit der Eigenschaft b = a.
Fr eine Relation A A wird hufig eine verwandte Relation
0 betrachtet, die eine oder mehrere der oberen Eigenschaften
zustzlich zu besitzt. Eine solche Relation heit jeweils Abschlu:

Definition 1.9 (Abschlsse ber Mengen) Gegeben sei eine Relation


A A. Eine Relation 0 A A heit
def
reflexiver Abschlu von 0 ist die kleinste reflexive Relation mit
0 ,
def
symmetrischer Abschlu von 0 ist die kleinste symmetrische
Relation mit 0 ,
def
transitive Abschlu von 0 ist die kleinste transitive Relation mit
0 ,
Mathematische Grundlagen 239

def
reflexiv-transitiver Abschlu von 0 ist die kleinste reflexive und
transitive Relation mit 0 .
Dabei heit kleinste Relation jeweils, da fr jede andere Relation 00 ,
welche die jeweilige Eigenschaft erfllt, gilt 0 00 .
+
Die transitive Abschlu von wird geschrieben, der reflexiv-

transitive Abschlu .

Definition 1.10 Eine Abbildung ist ein Tripel f = ( A, f , B), wobei A


und B Mengen sind und f A B eine Relation, so da fr jedes
a A genau ein b B existiert mit a f b. A heit Vorbereich von f , B
heit Nachbereich und f der Graph von f . Fr f = ( A, f , B) steht auch
f
f : A B oder A B. Fr ( a, b) f steht normalerweise f ( a) = b.

Nach dieser Definition sind zwei Abbildungen f = ( A, f , B) und


g = (C, g , D ) gleich genau dann, wenn A = C, B = D und f = g .

def
Definition 1.11 Fr eine Menge A ist die Identittsabbildung durch id A =
def
( A, id A , A) mit id A = {( a, a) | a A} definiert.

f
Definition 1.12 Eine Abbildung (auch: Funktion) A B heit
def
surjektiv fr alle b B gibt es ein a A, so da f ( a) = b,
def
injektiv aus f ( a1 ) = f ( a2 ) folgt a1 = a2 fr beliebige a1 , a2 A,
def f
bijektiv A B ist injektiv und surjektiv.
f
Ist A B bijektiv, so heien A und B isomorph, in Zeichen A
= B.

f g
Definition 1.13 Fr zwei Abbildungen A B und B C definiere
durch
def
g f = ( A, g f , C )
die Komposition von g und f mit
def
g f = {( a, c) | b B : ( a, b) f (b, c) g }.

Lemma 1.14 Die Komposition von Abbildungen ist assoziativ, das heit
f g h
fr A B, B C und C D gilt

(h g) f = h ( g f ) .

Es ist deshalb mglich, die Klammern ganz wegzulassen und h g f


zu schreiben.
f
Lemma 1.15 Fr eine bijektive Abbildung A B mit f = ( A, f , B)
f 1 def
existiert eine Umkehrabbildung B A mit f 1 = ( B, f 1 , A), wobei
def
f 1 = f 1 . Es gilt f 1 f = id A und f f 1 = idB .
240 Anhang A

Definition 1.16 Sei A eine Menge, f : A A eine Abbildung. Dann ist


definiert:
def
f0 = id A
def
f n +1 = f fn

1.6 Ordnungen

Definition 1.17 Sei M eine Menge. Eine Relation M M ist eine


def
partielle Ordnung (auch Halbordnung genannt) auf M ist reflexiv,
transitiv und antisymmetrisch (vgl. Definition 1.8).

Ein Beispiel fr eine partielle Ordnung ist etwa die Relation


zwischen Mengen. Eine partiell geordnete Menge ( M; ) ist eine Menge
M mit einer partiellen Ordnung . In einer partiell geordneten Menge
kann es unvergleichbare Elemente geben, d.h. Elemente x, y M, fr die
weder xy noch yx gilt. In der halbgeordneten Menge ( M; ) mit

M = {{1, 2}, {2, 3}, {1, 2, 3}}

sind z.B. die Elemente {1, 2} und {2, 3} unvergleichbar.

Definition 1.18 Eine totale Ordnung auf einer Menge M ist eine partielle
Ordnung , bei der zustzlich gilt

x, y M : xy yx .

Definition 1.19 Sei ( M; ) eine halbgeordnete Menge. Ein Element x


M heit
def
minimales Element: y M : yx y = x
def
kleinstes Element: y M : xy
def
maximales Element: y M : xy y = x
def
grtes Element: y M : yx

Die Begriffe minimales und kleinstes Element werden gern ver-


wechselt. Das liegt vielleicht daran, da die Relation auf Zahlen, die
oft als Modell fr eine partielle Ordnung herhalten mu, in Wirklich-
keit eine totale Ordnung ist. Bei totalen Ordnungen fallen die Begriffe
minimales und kleinstes Element jedoch zusammen. Deshalb hier
noch einmal eine verbale Definition:

minimales Element heit, da es kein Element gibt, das kleiner ist. Es


kann aber Elemente geben, die unvergleichbar mit einem minima-
len Element sind. (In der oben angegebenen Menge M sind {1, 2}
und {2, 3} beide minimal.)
kleinstes Element heit, da alle anderen Elemente grer sind. Damit
ist auch die Vergleichbarkeit gegeben. Es gibt in einer Menge
hchstens ein kleinstes Element. (In der oben angegebenen Menge
M gibt es kein kleinstes Element.)
Mathematische Grundlagen 241

Definition 1.20 Eine totale Ordnung heit wohlfundierte Ordnung, wenn


es keine unendlich langen absteigenden Ketten gibt, d. h. keine unend-
liche Folge ( ai | i N) mit der Eigenschaft

i N : a i 1 a i +1 .

In einer wohlgeordneten Menge besitzt jede nichtleere Teilmenge ein


kleinstes Element.

Aufgaben

f g
Aufgabe 1.1 A B C sei injektiv (surjektiv). Welche Aussagen ber
f g
A B bzw. B C gelten dann mit Bestimmtheit?

f 1
Aufgabe 1.2 Zeige, da die Umkehrfunktion B A einer bijektiven
f
Funktion A B stets injektiv ist.

Aufgabe 1.3 Fr eine endliche Menge A beweise

|P ( A)| = 2| A|

Aufgabe 1.4 Sei A eine endliche Menge. Beweise, da fr T P ( A)


gilt
| T | + | T | = | A|

Aufgabe 1.5 Beweise anhand der Wahrheitstafeln fr die Aussagenlo-


gik:
Die DeMorganschen Gesetze:

( A B) = A B
( A B) = A B

Das erste Distributivgesetz:

A ( B C ) = ( A B) ( A C )

Die Kontraposition:

( A B) ( B A)

Folgenden Satz ber die Implikation:

( A ( B C )) (( A B) ( A C ))

Aufgabe 1.6 In einem an Schulen populren Lehrbuch der Mathematik


wird folgende Aussage gemacht:
Fr eine partielle Ordnung v auf einer Menge D und d1 , d2 D mit d1 6v d2 gilt
d2 v d1 .

Gib eine partielle Ordnung an, fr welche die Aussage nicht gilt!