April 10, 2003
Problemanalyse
Verstehe die Aufgabe informell.
Schreibe einen Vertrag für die Funktion, der spezifiziert, was für Informationen hineingehen und was für Informationen herauskommen.
wird später ergänzt
Schreibe Beispiele für die Benutzung der Funktion, zusammen mit erwarteten Ausgaben.
Funktionsdefinition
Gerüst
wird später ergänzt
Rumpf
Korrektheitsprüfung
wird später ergänzt
wird später ergänzt
Test
Entwickle eine klare Vorstellung davon, was die Funktion tun soll. Gehe dabei sicher, daß Du
die Aufgabe der Funktion einem einfachen Satz beschreiben kannst (höchstens 25 Wörter, besser 12),
-- falls jemand anders das Programm geschrieben hätte -- Du falsche von richtigen Ergebnissen unterscheiden könntest.
Denke darüber nach, wie Du das Problem lösen würdest, wenn es keine Computer gebe. Falls Du gerade am Computer sitzt, kannst Du die Aufgabenbeschreibung in einem Kommentar festhalten:
; Funktion cube: Berechne die dritte Potenz einer Zahl
Überlege Dir, was für Informationen die Funktion benötigt, um ihre Aufgabe zu erledigen, und was für Informationen sie zurückliefern soll. Stelle Dir die folgenden Fragen:
Wie viele Eingaben bzw. Parameter benötigt die Funktion, damit sie ihre Aufgabe erledigen kann?
Was für Typen Information stellen die einzelnen EIngaben dar? (Anfangs sind das immer Zahlen.)
Was für einen Typ Information gibt die Funktion zurück?
Wenn Du am Computer sitzt, kannst du auch diese Angaben in einem Kommentar festhalten:
; cube: number -> number
Dieser Kommentar besagt, daß cube ein Zahl als Eingabe akzeptiert und eine andere Zahl zurückliefert.
; area-of-ring: number[Innenradius] x number[Außenradius] -> number
Dieser Kommentar besagt, daß die Funktion area-of-ring zwei Zahlen als Eingaben akzeptiert -- einen Innenradius und einen Außenradius -- und eine Zahl zurückliefert.
Schreibe mindestens einen, besser mehrere Ausdrücke auf, welche diese Funktion benutzen, jeweils zusammen mit der erwarteten "`korrekten Antwort"'. Dieser Schritt ist aus drei Gründen wichtig:
Es vermittelt Dir eine Vorstellung davon, ob es angemessen einfach ist, die Funktion zu benutzen, oder ob zum Beispiel die Parameter in einer ungünstigen Reihenfolge angeordnet sind. Ggf. kannst Du den Vertrag in dieser Beziehung noch verbessern.
Möglicherweise gibt es noch weitere Probleme mit dem Vertrag, zum Beispiel mit den Typen der Eingaben. Auch in diesem Fall kannst Du Vertrag und Beispiele noch korrigieren.
Du hast schon vorher einige Testbeispiele -- es gibt also keinen guten Grund, den späteren Testschritt auszulassen.
Beginne mit möglichst einfachen Beispielen und arbeite Dich zu komplizierteren Beispiel vor. Wenn Du am Rechner sitzt, schreibe die Beispiele als Kommentare auf:
; (cube 0) => 0 ; (cube -3/5) => -27/125 ; (cube (cube 3)) => 19683
Eine Funktionsdefinition meldet für eine Funktion an:
wie sie heißt,
was für Eingaben sie akzeptiert, und
wie sie ihre Ausgabe berechnet.
Das Gerüst einer Funktionsdefinition ist ihr erster Entwurf, in der nur die Punkte 1 und 2 enthalten sind. Ein Gerüst für eine Funktion mit einem Parameter hat die folgende Form:
(define (name parameter) ... parameter ...)
Eine Funktion mit mehreren Parametern hat die folgende Form:
(define (name parametersb1 parametersb2 ... parametersbn) ... parametersb1 parametersb2 ... parametersbn ...)
Damit hat das Gerüst folgende Bestandteile
(define (
Der Name der Funktion.
Die Namen der Parameter mit Leerzeichen dazwischen.
Eine Klammer zu )
(für die Klammer nach define)
Der Rumpf der Funktion, in dem die Parameter vorkommen werden -- deshalb können sie schon einmal dort auftauschen.
Eine Klammer zu )
(für die Klammer vor define)
1-4 heißen zusammen der Kopf der Funktion, danach kommt der Rumpf. Beispiel:
(define (cube a-number) ... a-number ... )
Der Funktionsname und die Namen der Parameter sind nahezu beliebig, solange sie aus Buchstaben (und, bei Komposita, aus Bindestrichen) bestehen. Der Funktionsname sollte die Aufgabe der Funktion suggerieren, und die Parameter entsprechend der Aufgaben der Eingaben.
Ersetze den Rumpf
... parametersb1 parametersb2 ... parametersbn ...
in dem Gerüst mit einem Ausdruck, in dem die Parameter auftauchen, und dessen Wert das korrekte Ergebnis des Funktionsaufrufs ist. Für das cube-Beispiel ist das Gerüst:
(define (cube number) ... number ... )
Sie wird ergänzt zu
(define (cube number) (* number number number) )
weil die dritte Potenz einer Zahl gerade das Ergebnis der Multiplikation von drei Kopien der Zahl ist.
Der Testschritt ist wichtig, wenn Du Dein Vertrauen rechtfertigen möchtest, daß Dein Programm funktioniert.
Dafür solltest du jedes Beispiel, das Du im Schritt ``Beispiele'' entwickelt hast (angefangen mit dem einfachsten):
in das Interaktionsfenster eintippen (nur den Ausdruck selbst, nicht den Kommentar danach oder das erwartete Ergebnis, und
feststellen, ob das erwartete Ergebnis herauskommt.
Falls nicht erwartete Ergebnis herauskommt, hast Du entweder ein falsches Ergebnis erwartet, oder in Deinem Programm steckt ein Fehler. Stelle fest, auf welche Weise das Ergebnis falsch ist, und wie das falsche Ergebnis zustandegekommen sein könnte. Korrigiere den Fehler und versuche es noch einmal. Wenn Du einen Fehler behebst, führe alle Tests noch einmal durch, um sicherzugehen, daß sie noch funktionieren.
Problemanalyse
Verstehe die Aufgabe informell.
Schreibe einen Vertrag für die Funktion, der spezifiziert, was für Informationen hineingehen und was für Informationen herauskommen.
Analysiere die Typen der Eingaben und Ausgaben.
Schreibe Beispiele für die Benutzung der Funktion, zusammen mit erwarteten Ausgaben.
Funktionsdefinition
Gerüst
Schablone
Rumpf
Korrektheitsprüfung
Korrekturlesen
Syntaxüberprüfung
Test
Dieser Schritt beschäftigt sich mit den Typen Information, auf denen eine Funktion arbeitet -- für Ein- und Ausgabe. Es gibt mehrere Möglichkeiten: einfache Daten, Fallunterscheidung, gemischte Daten, zusammengesetzte Daten und selbstbezügliche Daten:
Zahlen, Wahrheitswerte, Zeichenketten und Symbole sind sämtlich in Scheme eingebaute Typen. Falls also alle Eingaben und die Ausgabe (und alle Zwischenergebnisse) einfache Daten sind, ist keine weitere Analyse notwendig.
Falls die Eingabe einer Funktion eine Zahl ist, kann diese möglicherweise in unterschiedliche Kategorien fallen: Zum Beispiel könnte ein Bankkonto für ein Guthaben unter 500 keine Zinsen abwerfen, für Beträge zwischen 500 und 999,9 3% Zinsen, und für alle Beträge darüber 4% Zinsen. Damit werden die Zahlen in drei unterschiedlich zu behandelnde Teilmengen aufgeteilt: weniger als 500, mindestens 500 aber weniger als 1000, und mindestens 1000.
Falls die Eingabe oder Ausgabe immer eine von mehreren Möglichkeiten
ist (z.B. 'A
, 'B
, 'C
, 'D
, 'F
),
dann bestimmt die Funktion einen Datentyp, der nur eine Teilmenge
aller Symbole umfaßt.
Gemischte Daten sind eine Variante der Fallunterscheidung: eine Eingabe oder die Ausgabe können zu einem von mehreren unterschiedlichen Typen gehören.
Zum Beispiel könnte eine geometrische Form ein Quadrat, ein Rechteck, oder ein Kreis sein. Eine Liste ist entweder empty oder ein cons. Einen solchen Umstand solltest Du in einem Kommentar festhalten:
;; Eine Form ist entweder ein Quadrat, ein Rechteck oder ein Kreis
Falls einer der Parameter, das Ergebnis oder ein Zwischenwert aus mehreren einfacheren Bestandteilen besteht, mußt Du einen zusammengesetzten Datentyp (genannt "`Struktur"') definieren.
Ein Name besteht z.B. aus Vor- und Nachname. Solche Typen werden in Scheme mit define-struct definiert. Schreibe dazu einen Kommentar mit den Namen und Typen jeden Teils des zusammengesetzten Typs auf, und schreibe eine dazu passende define-struct-Form. Schreibe dann Verträge für alle Funktionen, die für den neuen Typ von define-struct automatisch definiert werden:
;; Ein Name besteht aus zwei Symbolen - Vor- und Nachname (define-struct name (personal family)) ;; make-name: symbol, symbol -> name ;; name?: object -> boolean ;; name-personal: name -> symbol ;; name-family: name -> symbol
Selbst-referentielle oder rekursive Datenstrukturen entstehen meist durch die Kombination eines gemischten Typs mit mindestens einem zusammengesetzten Typ. Zum Beispiel ist eine Liste entweder empty oder ein cons. Ein cons besteht aus einem Objekt (dem ersten Element, zugänglich mit first) und einer Liste (dem "`Rest"', zugänglich mit rest).
In einem Familienstammbaum könnte Werte des Typs "`Stammbaum"'
definiert sein als entweder das Symbol 'unknown
oder eine
Person. Eine Person besteht aus einem Symbol (name), einem
weiteren Symbol (eye-color), einer Zahl
(birth-year), und zwei Stammbäumen (mother und
father).
Da selbstbezügliche Datenstrukturen aus zusammengesetzten und gemischten Daten konstruiert werden, schreibst Du auch sie nach den obigen Mustern auf und dokumentierst sie.
Die Schablone ist der erste Entwurf des Funktionsrumpfes, der beim Gerüst noch fehlt. Die Schablone hängt einzig und allein von den Datentypen der Parameter und des Ergebnisses der Funktion ab. Erst danach mußt Du mit der eigentlichen Lösung des Problems beginnen.
Es gibt mehrere Arten von Schablonen -- welche von ihnen für eine gegebene Funktion die beste ist, hängt davon ab, wie komplex die Daten sind, mit denen die Funktion zu tun hat. Außerdem ist es möglich, die Schablone ausgehend von den Eingabe- oder von den Ausgabe-Daten zu entwickeln. Um diese Entscheidung zu treffen, ist es erst einmal notwendig, die Eingabe- und Ausgabe-Daten zu analysieren.
Für die Eingabe-Daten gibt es folgende Fragen, die Du stellen kannst, um die Entscheidung zu treffen, welche Schablone am besten für die Funktion geeignet ist:
Gehören alle Eingaben zu einfachen Typen?
Findet für die Eingaben eine Fallunterscheidung statt?
Gehört die Eingabe zu einem zusammengesetzten Datentyp (mit define-struct)?
Gibt es mehrere Eingaben mit unterschiedlichen Datentypen?
Ist es notwendig, die Eingabe-Daten auf Fehler zu untersuchen?
Gehört eine Liste bekannter Länge zu den Eingaben?
Gehört eine Liste unbekannter Länge zu den Eingaben?
Gehört eine andere selbstbezügliche Datenstruktur zu den Eingaben?
Gibt es mehrere Eingaben mit zusammengesetztem Typ?
Die Antworten auf diese Fragen dienen allesamt dazu, sicherzustellen, daß Deine Funktion später alle möglichen Eingaben vollständig und korrekt verarbeiten kann.
Die Analyse der Ausgabe-Daten erlaubt Dir, sicherzustellen, daß Deine Funktion alle gewünschten Ausgaben produzieren kann. Hier solltest Du folgende Fragen stellen:
Ist die Ausgabe einfachen Typs?
Gehört die Ausgabe zu einer Fallunterscheidung?
Ist die Ausgabe zusammengesetzt?
Kann die Ausgabe zu unterschiedlichen Typen gehören?
Ist die Ausgabe eine Liste bekannter Länge?
Ist die Ausgabe eine Liste unbekannter Länge?
Ist die Ausgabe eine andere selbstbezügliche Datenstruktur?
ist nicht viel an Schablone zu schreiben -- achte lediglich darauf, daß alle Parameter im Rumpf vorkommen, und daß die Ausgabe den richtigen Typ hat.
benutze eine ausgabe-bestimmte Schablone (s.u.).
benutze eine eingabe-bestimmte Schablone (s.u.).
kann es sinnvoll sein, eine eingabe-bestimmte Schablone mit einer ausgabe-bestimmten Schablone zu kombinieren. Oft gibt die eingabe-bestimmte Schablone Hinweise darauf, welche Fragen in einem cond gestellt werden müssen, und die ausgabe-bestimmte Schablone darauf, welche Antworten gebeben werden. In diesem Fall geht es meist nur darum, die Fragen den Antworten korrekt zuzuordnen.
Ähnlich sieht es mit Funktionen aus, die eine Liste als Eingabe akzeptieren und eine Liste als Ausgabe produzieren: Die entsprechende eingabe-bestimmte Schablone schreibt eine Fallunterscheidung nach leerer und nicht-leerer Liste vor, wobei bei nicht-leerer Liste wahrscheinlich die gleiche Funktion mit dem Rest der Liste aufgerufen wird. Die ausgabe-bestimmte Schablone besagt, daß wahrscheinlich in einem Fall ein Element an das Ergebnis des Aufrufs derselben Funktion mit cons vorn angehängt wird. Hier ist die eingabe-bestimmte Schablone:
(define (f list) (cond ((empty? list) ...) ((cons? list) ... (first list) ... ... (f (rest list)) ...)))
Hier ist die ausgabe-bestimmte Schablone:
(define (f list) (cond (... empty) (... (cons ... (f some-list)))))
Wahrscheinliches Fusionsprodukt:
(define (f list) (cond ((empty? list) empty) ((cons? list) (cons ( ... (first list) ...) (f (rest list))))))
Falls alle Eingaben einer Funktion zu einfachen Typen gehören (Zahlen, Wahrheitswerte, Zeichenketten und Symbole), dann besteht die Schablone aus den Parametern mit Ellipsen (...) dazwischen, um Dich daran zu erinnern, daß sie noch irgendwie kombiniert werden müssen. Beispiel:
(define (area-of-ring inner-radius outer-radius) (... inner-radius ... outer-radius ...))
Denke daran, daß die Parameter später in einer anderen Reihenfolge im Funktionsrumpf auftauchen können als in der Schablone, daß manche Parameter mehrfach vorkommen können und manchmal (wenn auch sehr selten) auch ganz wegfallen. Im Fall von area-of-ring ist die Reihenfolge im späteren Rumpf wahrscheinlich vertauscht:
(define (area-of-ring inner-radius outer-radius) (- (area-of-disk outer-radius) (area-of-disk inner-radius)))
Falls die Eingabe einer Funktion zu einem einfachen Typ gehört und in eine von mehreren Kategorien fallen kann, besteht der Funktionsrumpf fast immer aus einem cond mit einer entsprechenden Anzahl von Zweigen.
Beispiel (siehe Abschnitt 2): Ein Kontostand ist eine Zahl, bei der unterschieden wird zwischen folgenden Möglichkeiten:
unter 500,00
mindestens 500,00, aber unter 1000,00
mindestens 1000,00
In diesem Fall sieht die Funktionsschablone für eine Funktion, welche die Guthabenzinsen berechnet, so aus:
(define (interest-rate balance) (cond ((< balance 500.00) ...) ((and (>= balance 500.00) (< balance 1000.00)) ...) ((>= balance 1000.00) ...)))
Falls eine (oder mehrere) der Eingaben zusammengesetzt ist, tauchen in der Regel eine oder mehrere der Werte im Rumpf auf, aus denen die Eingabe zusammengesetzt ist. In diesem Fall tauchen in der Schablone Aufrufe aller Selektoren (die von define-struct definiert wurden) für die Eingabe auf.
Beispiel:
; Ein SONG ist ein Symbol (ARTIST), ein weiteres Symbol (NAME), und ; eine Zahl (LENGTH) (define-struct song (artist name length)) ; make-song: symbol x symbol x number -> song ; song-artist: song -> symbol ; song-name: song -> symbol ; song-length: song -> number ; song?: object -> boolean
Damit sieht die Schablone für eine Funktion, die auf Songs arbeitet, so aus:
(define (f a-song) ... (song-artist a-song) ... ... (song-name a-song) ... ... (song-length a-song) ...)
Denke daran, daß die Aufrufe der Selektoren später in der fertigen Funktion in einer anderen Reihenfolge vorkommen können; manche können auch mehrmals oder gar nicht vorkommen.
Falls einer (oder mehrere der Eingaben) zu einem von mehreren Typen gehören kann, kommt die gleiche Schablone wie bei Fallunterscheidungen zur Anwendung. Hier ist eine mögliche Datendefinition:
; Ein Punkt ist entweder ; eine Zahl, für eine Position auf der X-Aachse ; ein POSN, für eine Position in der Ebene ; Ein POSN ist ein Paar von Zahlen (x,y) (define-struct posn (x y)) ; make-posn: number x number -> posn ; posn-x: posn -> number ; posn-y: posn -> number ; posn?: object -> boolean
Die Schablone für eine Funktion, die einen Punkt als Eingabe akzeptiert, ist also ein cond mit zwei Zweigen:
(define (distance-to-0 a-point) (cond ((number? a-point) ...) ((posn? a-point) ...)))
Falls -- wie in diesem Fall -- einer der alternativen Typen selbst zusammengesetzt ist, kann die Schablone für diesen Fall selbst entsprechend entsprechend der Regel für zusammengesetzte Daten ausgeführt werden:
(define (distance-to-0 a-point) (cond ((number? a-point) ...) ((posn? a-point) ... (posn-x a-point) ... ... (posn-y a-point) ...)))
Falls es möglich ist, daß die Funktion mit inkorrekten Eingaben aufgerufen wird, behandle die Situation genauso wie bei einer Fallunterscheidung und rufe in Zweigen, die zu inkorrekten Eingaben gehören, error auf. Wenn zum Beispiel das Konto von oben nicht übermäßig überzogen sein darf, könnte der erste Fall so aussehen:
(define (f balance) (cond ((< balance -5000.00) (error "Konto übermäßig überzogen.")) ...))
Falls eine (oder mehrere) der Eingaben eine Liste feststehender und bekannter Länge ist, behandle dies ähnlich zur Schablone für zusammengesetzte Daten. Nur extrahierst Du aus der Liste die einzelnen Elemente, anstatt die Selektoren aufzurufen.
Beispiel: Ein Name ist eine Liste aus zwei Symbolen (Vor- und Nachname). Die dazu passende Schablone sieht so aus:
(define (f a-name) ... (first a-name) ... ... (second a-name) ...)
Wie bei der Schablone für zusammengesetzte Funktionen kann es sein, daß in der fertigen Funktion die Aufrufe von first, second, etc. in einer anderen Reihenfolge vorkommen als in der Schablone, oder daß Aufrufe mehrfach oder gar nicht vorkommen.
Falls eine der Eingaben eine Liste unbekannter Länge ist, kombiniere die Schablonen für die Fallunterscheidung mit der Schablone für Listen bekannter Länge:
(define (f a-list) (cond ((empty? a-list) ...) ((cons? a-list) ...)))
Weiterhin weißt Du bei einer Liste, die ein cons ist, daß first und rest anwendbar sind. Du kannst also die Schablone für zusammengesetzte Daten einsetzen und die Schablone folgendermaßen erweitern:
(define (f a-list) (cond ((empty? a-list) ...) ((cons? a-list) ... (first a-list) ... ... (rest a-list) ...)))
Was Du mit (first a-list) und (rest a-list) anfangen kannst, hängt von ihren Typen ab. Da rest immer eine Liste desselben Typs liefert, wie es als Eingabe bekam, ist es am wahrscheinlichsten, daß die Funktion sich auf (rest a-list) selbst aufruft.
(define (f a-list) (cond ((empty? a-list) ...) ((cons? a-list) ... (first a-list) ... ... (f (rest a-list)) ...)))
Falls die Ausgabe einer Funktion zu einem einfachen Typ gehört (Zahlen, Wahrheitswerte, Zeichenketten und Symbole), dann sagt die Schablone lediglich aus, daß im Rumpf ein Ausdruck stehen muß, der einen Wert dieses Typs zurückgibt.
Beispiel:
;; sum-of-squares: number x number -> number (define (sum-of-squares num1 num2) ... num1 ... num2 ...) ; numerischer Ausdruck
In diesem Fall ist die Schablone nicht sehr informativ, dient aber zur Erinnerung daran, daß dort tatsächlich später ein Ausdruck des entsprechenden Typs stehen muß.
Falls die Ausgabe einer Funktion zu einem einfachen Typ gehört und in eine von mehreren Kategorien fallen kann, besteht der Funktionsrumpf fast immer aus einem cond mit einer entsprechenden Anzahl von Zweigen.
Beispiel:
Die Rückmeldung beim Spiel "`Superhirn"' ist eins der folgenden
Symbole: 'perfect
, 'one-color-correct-position
,
'colors-occur
, und 'nothing-correct
. Dann wird die
Schablone einer Funktion, die eine solche Rückmeldung als Ergebnis
liefert soll, so aussehen:
(define (check-guess target1 target2 guess1 guess2) (cond (... 'perfect) (... 'one-color-correct-position) (... 'colors-occur) (... 'nothing-correct)))
Es kann sein, daß in der fertigen Funktion die gleiche Antwort bei mehreren Zweigen herauskommt, etwa so:
(define (check-guess target1 target2 guess1 guess2) (cond (... 'perfect) (... 'one-color-correct-position) (... 'one-color-correct-position) (... 'colors-occur) (... 'colors-occur) (... 'nothing-correct)))
Dies hängt von den Fragen in den Zweigen (und damit wahrscheinlich von der eingabe-bestimmten Schablone) ab.
Falls die Ausgabe zusammengesetzten Typs ist, so muß die Schablone den Ausdruck (make-... ...) enthalten, der einen Wert dieses Typs konstruiert. Achte darauf, schon Platzhalter für Bestandteile des zusammengesetzten Werts in die Schablone zu schreiben.
Beispiel:
; Ein SONG ist ein Symbol (ARTIST), ein weiteres Symbol (NAME), und ; eine Zahl (LENGTH) (define-struct song (artist name length)) ; make-song: symbol x symbol x number -> song ; song-artist: song -> symbol ; song-name: song -> symbol ; song-length: song -> number ; song?: object -> boolean
Eine Funktion, die einen Song zurückgibt, wird wahrscheinlich die Funktion make-song benötigen:
(define (f ...) (make-song symbol symbol number))
Dabei muß jeweils symbol später durch einen Ausdruck ersetzt werden, der ein Symbol liefert, und number durch einen Ausdruck, der eine Zahl liefert.
Falls die Ausgabe einer Funktion zu einem von mehreren möglichen Typen gehört, schreibe eine Schablone ähnlich der für Fallunterscheidung bei der Ausgabe.
Beispiel:
; Ein Punkt ist entweder ; eine Zahl, für eine Position auf der X-Aachse ; ein POSN, für eine Position in der Ebene ; Ein POSN ist ein Paar von Zahlen (x,y) (define-struct posn (x y)) ; make-posn: number x number -> posn ; posn-x: posn -> number ; posn-y: posn -> number ; posn?: object -> boolean
Die Schablone für eine Funktion, die einen Punkt zurückgibt, ist (wahrscheinlich) ein cond mit zwei Zweigen:
(define (f ...) (cond (... number) (... posn)))
Falls einer der möglichen Typen der Ausgabe zusammengesetzt ist (wie in diesem Fall), kannst Du die Schablone noch erweitern:
(define (f ...) (cond (... number) (... (make-posn number number))))
Falls die Ausgabe einer Funktion eine Liste fester und bekannter Länge ist, schreibe eine Schablone entsprechend der für zusammengesetzte Daten, mit dem Unterschied, daß Du cons für die Erzeugung des Ergebnisses verwendest.
Beispiel:
;; three-copies: symbol -> <Liste mit 3 Symbolen> ;;(three-copies 'yo) => (cons 'yo (cons 'yo (cons 'yo empty))) (define (three-copies sym) (cons ... (cons ... (cons ... empty))))
Eine entsprechende Schablone mit list sieht so aus:
;; three-copies: symbol -> <Liste mit 3 Symbolen> ;; (three-copies 'yo) => (cons 'yo (cons 'yo (cons 'yo empty))) (define (three-copies sym) (list ... ... ...))
Falls die Funktion eine Liste unbekannter Länge zurückgeben soll, kombiniere die Schablonen für die Fallunterscheidung und die für Listen bekannter Länge:
(define (f ...) (cond (... empty) (... (cons element list))))
Dabei muß element vom Typ der Listenelemente sein. List muß vom selben Typ sein wie die Ausgabe, sehr oft wird also die Schablone so aussehen:
(define (f ...) (cond (... empty) (... (cons element (f ...)))))