racket/collects/deinprogramm/scribblings/ka.scrbl
Mike Sperber 34d365d3a8 Contracts for accumulating helper procedures.
... now that we have proper tail recursion for these procedures.

svn: r16034
2009-09-17 12:02:17 +00:00

356 lines
11 KiB
Racket

#lang scribble/doc
@(require scribble/manual
scribble/basic
scribble/extract
scheme/class
scheme/contract)
@title{Konstruktionsanleitungen 1 bis 10}
This documents the design recipes of the German textbook @italic{Die
Macht der Abstraktion}.
@table-of-contents[]
@section{Konstruktion von Prozeduren}
Gehen Sie bei der Konstruktion einer Prozedur in folgender Reihenfolge
vor:
@itemize[
@item{@bold{Kurzbeschreibung} Schreiben Sie eine einzeilige Kurzbeschreibung.}
@item{@bold{Datenanalyse} Führen Sie eine Analyse der beteiligten Daten
durch. Stellen Sie dabei fest, zu welcher Sorte die Daten gehören, ob
Daten mit Fallunterscheidung vorliegen und ob zusammengesetzte
oder gemischte Daten vorliegen.}
@item{@bold{Vertrag} Wählen Sie einen Namen und schreiben Sie einen Vertrag für die Prozedur.}
@item{@bold{Testfälle} Schreiben Sie einige Testfälle.}
@item{@bold{Gerüst} Leiten Sie direkt aus dem Vertrag das Gerüst der Prozedur her.}
@item{@bold{Schablone} Leiten Sie aus dem Vertrag und der Datenanalyse mit
Hilfe der Konstruktionsanleitungen eine Schablone her.}
@item{@bold{Rumpf} Vervollständigen Sie den Rumpf der Prozedur.}
@item{@bold{Test} Vergewissern Sie sich, daß die Tests erfolgreich laufen.}
]
@section{Fallunterscheidung}
Wenn ein Argument einer Prozedur zu einer Fallunterscheidung gehört,
die möglichen Werte also in feste Kategorien sortiert werden können,
steht im Rumpf eine Verzweigung. Die Anzahl der Zweige entspricht
der Anzahl der Kategorien.
Die Schablone für eine Prozedur @scheme[proc], deren Argument zu einer Sorte gehört,
die @italic{n} Kategorien hat, sieht folgendermaßen aus:
@schemeblock[
(: proc (ctr -> ...))
(define proc
(lambda (a)
(cond
(#,(elem (scheme test) (subscript "1")) ...)
...
(#,(elem (scheme test) (subscript "n")) ...))))
]
Dabei ist @scheme[ctr] der Vertrag, den die Elemente der Sorte erfüllen müssen.
Die @elem[(scheme test) (subscript "i")] müssen Tests sein, welche die einzelnen Kategorien
erkennen. Sie sollten alle Kategorien abdecken.
Der letzte Zweig kann auch ein @scheme[else]-Zweig sein, falls
klar ist, daß @scheme[a] zum letzten Fall gehört, wenn alle vorherigen
@elem[(scheme test) (subscript "i")] @scheme[#f] ergeben haben.
Anschließend werden die Zweige vervollständigt.
Bei Fallunterscheidungen mit zwei Kategorien kann auch @scheme[if]
statt @scheme[cond] verwendet werden.
@section{zusammengesetzte Daten}
Wenn bei der Datenanalyse zusammengesetzte Daten vorkommen, stellen
Sie zunächst fest, welche Komponenten zu welchen Sorten gehören.
Schreiben Sie dann eine Datendefinition, die mit folgenden Worten
anfängt:
@schemeblock[
(code:comment @#,t{Ein @scheme[x] besteht aus / hat:})
(code:comment @#,t{- @scheme[#,(elem (scheme Feld) (subscript "1"))] @scheme[(#,(elem (scheme ctr) (subscript "1")))]})
(code:comment @#,t{...})
(code:comment @#,t{- @scheme[#,(elem (scheme Feld) (subscript "n"))] @scheme[(#,(elem (scheme ctr) (subscript "n")))]})
]
Dabei ist @scheme[x] ein umgangssprachlicher Name für die Sorte
(``Schokokeks''), die @elem[(scheme Feld) (subscript "i")] sind
umgangssprachliche Namen und kurze Beschreibungen der Komponenten
und die @elem[(scheme ctr) (subscript "i")] die dazugehörigen Verträge.
Übersetzen Sie die Datendefinition in eine Record-Definition, indem Sie
auch Namen für den Record-Vertrag @scheme[ctr], Konstruktor @scheme[constr],
Prädikat @scheme[pred?] und die Selektoren @elem[(scheme select) (subscript "i")]
wählen:
@schemeblock[
(define-record-procedures ctr
constr pred?
(#,(elem (scheme select) (subscript "1")) ... #,(elem (scheme select) (subscript "n"))))
]
Schreiben Sie außerdem einen Vertrag für den Konstruktor der
Form:
@schemeblock[
(: constr (#,(elem (scheme ctr) (subscript "1")) ... #,(elem (scheme ctr) (subscript "n")) -> ctr))
]
Ggf. schreiben Sie außerdem Verträge für das Prädikat und die Selektoren:
@schemeblock[
(: pred? (%a -> boolean))
(: #,(elem (scheme select) (subscript "1")) (ctr -> #,(elem (scheme ctr) (subscript "1"))))
...
(: #,(elem (scheme select) (subscript "n")) (ctr -> #,(elem (scheme ctr) (subscript "n"))))
]
@section{zusammengesetzte Daten als Argumente}
Wenn ein Argument einer Prozedur zusammengesetzt ist, stellen Sie
zunächst fest, von welchen Komponenten des Records das Ergebnis der
Prozeduren abhängt.
Schreiben Sie dann für jede Komponente @scheme[(select a)] in die
Schablone, wobei @scheme[select] der Selektor der Komponente und @scheme[a] der Name
des Parameters der Prozedur ist.
Vervollständigen Sie die Schablone, indem Sie einen Ausdruck
konstruieren, in dem die Selektor-Anwendungen vorkommen.
@section{zusammengesetzte Daten als Ausgabe}
Eine Prozedur, die einen neuen zusammengesetzten Wert zurückgibt,
enthält einen Aufruf des Konstruktors des zugehörigen Record-Typs.
@section{gemischte Daten}
Wenn bei der Datenanalyse gemischte Daten auftauchen, schreiben Sie
eine Datendefinition der Form:
@schemeblock[
(code:comment @#,t{Ein @scheme[x] ist eins der Folgenden:})
(code:comment @#,t{- @elem[(scheme Sorte) (subscript "1")] (@elem[(scheme ctr) (subscript "1")])})
(code:comment @#,t{...})
(code:comment @#,t{- @elem[(scheme Sorte) (subscript "n")] (@elem[(scheme ctr) (subscript "n")])})
(code:comment @#,t{Name: @scheme[ctr]})
]
Dabei sind die @elem[(scheme Sorte) (subscript "i")] umgangssprachliche Namen
für die möglichen Sorten, die ein Wert aus diesen gemischten Daten
annehmen kann. Die @elem[(scheme ctr) (subscript "i")] sind die zu den Sorten
gehörenden Verträge. Der Name @scheme[ctr] ist für die Verwendung als
Vertrag.
Aus der Datendefinition entsteht eine Vertragsdefinition folgender Form:
@schemeblock[
(define ctr
(contract
(mixed #,(elem (scheme ctr) (subscript "1"))
...
#,(elem (scheme ctr) (subscript "n")))))
]
Wenn die Prädikate für die einzelnen Sorten @elem[(scheme pred?)
(subscript "1")] ... @elem[(scheme pred?) (subscript "n")] heißen, hat die
Schablone für eine Prozedur, die gemischte Daten konsumiert, die
folgende Form:
@schemeblock[
(: proc (ctr -> ...))
(define proc
(lambda (a)
(cond
((#,(elem (scheme pred?) (subscript "1")) a) ...)
...
((#,(elem (scheme pred?) (subscript "n")) a) ...))))
]
Die rechten Seiten der Zweige werden dann nach den
Konstruktionsanleitungen der einzelnen Sorten ausgefüllt.
@section{Listen}
Eine Prozedur, die eine Liste konsumiert, hat die folgende
Schablone:
@schemeblock[
(: proc ((list elem) -> ...))
(define proc
(lambda (lis)
(cond
((empty? lis) ...)
((pair? lis)
... (first lis)
... (proc (rest lis)) ...))))
]
Dabei ist @scheme[elem] der Vertrag für die Elemente der Liste. Dies
kann eine Vertragsvariable (@scheme[%a], @scheme[%b], ...) sein, falls
die Prozedur unabhängig vom Vertrag der Listenelemente ist.
Füllen Sie in der Schablone zuerst den @scheme[empty?]-Zweig aus.
Vervollständigen Sie dann den anderen Zweig unter der Annahme, daß
der rekursive Aufruf @scheme[(proc (rest lis))] das gewünschte
Ergebnis für den Rest der Liste liefert.
Beispiel:
@schemeblock[
(: list-sum ((list number) -> number))
(define list-sum
(lambda (lis)
(cond
((empty? lis) 0)
((pair? lis)
(+ (first lis)
(list-sum (rest lis)))))))
]
@section{natürliche Zahlen}
Eine Prozedur, die natürliche Zahlen konsumiert, hat die folgende
Schablone:
@schemeblock[
(: proc (natural -> ...))
(define proc
(lambda (n)
(if (= n 0)
...
... (proc (- n 1)) ...)))
]
Füllen Sie in der Schablone zuerst den 0-Zweig aus. Vervollständigen
Sie dann den anderen Zweig unter der Annahme, daß der rekursive Aufruf
@scheme[(proc (- n 1))] das gewünschte Ergebnis für @scheme[n]-1
liefert.
Beispiel:
@schemeblock[
(: factorial (natural -> natural))
(define factorial
(lambda (n)
(if (= n 0)
1
(* n (factorial (- n 1))))))
]
@section{Prozeduren mit Akkumulatoren}
Eine Prozedur mit Akkumulator, die Listen konsumiert, hat die
folgende Schablone:
@schemeblock[
(: proc ((list elem) -> ...))
(define proc
(lambda (lis)
(proc-helper lis z)))
(: proc ((list elem) ctr -> ...))
(define proc-helper
(lambda (lis acc)
(cond
((empty? lis) acc)
((pair? lis)
(proc-helper (rest lis)
(... (first lis) ... acc ...))))))
]
Hier ist @scheme[proc] der Name der zu definierenden Prozedur und
@scheme[proc-helper] der Name der Hilfsprozedur mit Akkumulator. Der
Anfangswert für den Akkumulator ist der Wert von @scheme[z]. Der Vertrag @scheme[ctr]
ist der Vertrag für den Akkumulator. Der
Ausdruck @scheme[(... (first lis) ... acc ...)]
macht aus dem alten Zwischenergebnis @scheme[acc] das neue
Zwischenergebnis.
Beispiel:
@schemeblock[
(: invert ((list %a) -> (list %a)))
(define invert
(lambda (lis)
(invert-helper lis empty)))
(: invert ((list %a) (list %a) -> (list %a)))
(define invert-helper
(lambda (lis acc)
(cond
((empty? lis) acc)
((pair? lis)
(invert-helper (rest lis)
(make-pair (first lis) acc))))))
]
Eine Prozedur mit Akkumulator, die natürliche Zahlen konsumiert, hat die
folgende Schablone:
@schemeblock[
(: proc (natural -> ...))
(define proc
(lambda (n)
(proc-helper n z)))
(define proc-helper
(lambda (n acc)
(if (= n 0)
acc
(proc-helper (- n 1) (... acc ...)))))
]
Dabei ist @scheme[z] das gewünschte Ergebnis für @scheme[n] = 0. Der
Ausdruck @scheme[(... acc ...)] muß den neuen Wert für den
Akkumulator berechnen.
Beispiel:
@schemeblock[
(: ! (natural -> natural))
(define !
(lambda (n)
(!-helper n 1)))
(define !-helper
(lambda (n acc)
(if (= n 0)
acc
(!-helper (- n 1) (* n acc)))))
]
@section{gekapselter Zustand}
Falls ein Wert Zustand enthalten soll, schreiben Sie eine
Datendefinition wie bei zusammengesetzten Daten.
Schreiben Sie dann eine Record-Definition mit
@scheme[define-record-procedures-2] und legen Sie dabei fest, welche
Bestandteile veränderbar sein sollen. Geben Sie Mutatoren für die
betroffenen Felder an. Wenn der Selektor für das Feld @scheme[select]
heißt, sollte der Mutator i.d.R. @scheme[set-select!] heißen. Die Form
sieht folgendermaßen aus, wobei an der Stelle @scheme[k] ein
veränderbares Feld steht:
@schemeblock[
(define-record-procedures-2 ctr
constr pred?
(#,(elem (scheme select) (subscript "1")) ... (#,(elem (scheme s) (subscript "k")) #,(elem (scheme mutate) (subscript "k"))) ... #,(elem (scheme s) (subscript "n"))))
]
In der Schablone für Prozeduren, die den Zustand eines
Record-Arguments @scheme[r] ändern, benutzen Sie den dazugehörigen Mutator
@elem[(scheme mutate) (subscript "k")] Wenn @scheme[a] der Ausdruck für den neuen Wert der Komponente ist,
sieht der Aufruf folgendermaßen aus: @scheme[(#,(elem (scheme mutate) (subscript "k")) r a)].
Um mehrere Komponenten in einer Prozedur zu verändern, oder um einen
sinnvollen Rückgabewert nach einer Mutation zu liefern, benutzen Sie
@scheme[begin].