Bisher vorgestellte Programme waren in funktionaler Weise implementiert. Schemes Stärken liegen zwar hauptsächlich in diesem Bereich, jedoch gibt es auch die Möglichkeit, objektorientiert vorzugehen.
Der zweite Teil dieser Reihe zeigte, wie in Scheme Strukturen definiert werden. Da die berühmte Gleichung des Methodikers Niklaus Wirth “Algorithmen + Datenstrukturen = Programme” immer noch gilt, wäre Scheme weniger nützlich, wenn es nicht auch Datenstrukturen gäbe. Sie können Strukturen in DrScheme so anlegen wie in Listing 1. Bei der Benutzung von define-struct wird eine Reihe von Funktionen automatisch generiert. Für jede Struktur erhalten Sie:
- eine make-struct-name-Funktion zum Anlegen eines neuen Elements dieser Struktur,
- eine Prädikatsfunktion struct-name? zur Typüberprüfung,
- Zugriffsfunktionen für jedes Element der Struktur mit dem Namen struct-name-Feld,
- Funktionen zum Modifizieren der Feldelemente ( set-struct-name-Feld!).
Die Verschachtelung von Strukturen erfolgt in der zweiten Form: (define-struct (neue-Struktur struct:alte-Struktur) (neue-Felder)) . Dabei sollten Sie beachten, dass nur den neuen Feldelementen der neue Strukturname vorangestellt wird. Sie müssen daher genau wissen, welche Elemente von der alten Struktur übernommen und welche neu eingeführt wurden. Da ich dies für nicht besonders einsichtig halte, schlage ich vor, noch eine Erweiterung von DrScheme zu nutzen: dessen Objektsystem.
Listing 1: Beispiel für Strukturen |
(define-struct p1 (first-name
last-name))
(define me
(make-p1 "Friedrich" "Dominicus"))
(p1? me)
(p1-first-name me)
(p1-last-name me)
(set-p1-first-name! me "Someone")
(p1-first-name me)
(define-struct (p2 struct:p1)
(address))
(define me (make-p2 "Friedrich"
"Dominicus"
"Bruchsal"))
(p2? me)
(p1? me)
(p1-first-name me)
(p1-last-name me)
(p2-address me)
(set-p1-first-name! me "Someone")
(p1-first-name me)
|
Objekte in Scheme
Die objektorientierte Programmierung erfreut sich immer noch steigender Beliebtheit, obwohl gewisse Vorstellungen über die unbedingte Nützlichkeit schon wieder in Frage gestellt werden. Lisps waren und sind Sprachen, die weitere Paradigmen umarmen, aber nicht erdrücken.
Es gibt eine Reihe von Objektsystemen für Scheme; da in den Beispielen DrScheme genutzt wurde, bietet es sich an, dessen Ansatz genauer zu untersuchen. Die Charakteristika des Systems entsprechen denen von Java:
- einfache Vererbung und
- eine Art multiple Vererbung durch Interfaces.
Die Interface-Vererbung wird in diesem Artikel nicht vorgestellt; wenn Sie sich darüber kundig machen wollen, schlagen Sie bitte in [4] nach. Eine Klassendefinition in Scheme sieht aus wie in Listing 2 dargestellt.
Eine Klasse bekommt ganz herkömmlich mit define einen Namen zugewiesen. Die Klassendefinition wird mit class eingeleitet, danach wird die Superklasse aufgeführt (hier object%). In DrScheme handelt es sich dabei um den Vorfahren aller Klassen, es gibt also keinen spezialisierteren Vorgänger. DrScheme folgt der Namenskonvention, Klassen mit einem % abzuschließen. Es empfiehlt sich, einer solchen Konvention zu folgen, da sie den Erwartungen anderer Benutzer entspricht.
Listing 2: Beispiel für Klassendefinition |
(define p1%
(class object% (firstname lastname)
(private
(fname firstname)
(lname lastname))
(public
(last-name (lambda ()
lname)) ;1
(p1? (lambda ()
(is-a? this p1%)))
(first-name (lambda ()
fname)) ;1
(set-last-name! (lambda (new-name)
(set! lname new-name)))
(print-name (lambda ()
(display "First name: ")
(display fname)
(display " ")
(display "Last name: ")
(display lname)
(newline)))
(dummy 1)
(sequence (super-init)))))
|
Listing 3: Benutzung von Objekten |
(define (use-p1%)
(let ((me (make-object p1% "foo" "bar")))
(send me print-name)
(send me set-last-name! "someone")
(send me print-name)
(display (ivar me dummy))
(newline)
((ivar me print-name))
(values)))
|
Listing 4: Eine abgeleitete Klasse |
(define p2%
(class p1% (first-name last-name an-address an-email)
(rename (super-dummy dummy))
(sequence (super-init first-name last-name))
(public
(address an-address) ; 1
(email an-email) ; 1
(set-email! (lambda (new-address)
(set! email new-address))) ; 2
(p2? (lambda ()
(is-a? this p2%)))
(build-from-file
(lambda (file-name)
(with-input-from-file file-name
(lambda ()
(let ((ll (read)))
(send this build-from-list ll))))))
(build-from-list
(lambda (l)
(make-object p2%
(list-ref l 0)
(list-ref l 1)
(list-ref l 2)
(list-ref l 3)))) ; 4
(write-to-file
(lambda (file-name mode)
(with-output-to-file file-name
(lambda ()
(let ((ll (list (send this first-name)
(send this last-name)
address email))) ; 3
(write ll)))
mode)))
(set-address! (lambda (new-address)
(set! address new-address)))) ;2
(override
(dummy (+ super-dummy 2)))))
|
Keine Angst vor Klassen
In der nachfolgenden Liste werden die Initialisierungsvariablen aufgezählt. Das sind Variablen, die bei der Erzeugung eines Objekts dieser Klasse übergeben werden müssen. Im Beispiel handelt es sich um first-name und last-name.
Im mit private beginnenden Abschnitt erfolgt die Deklaration privater Attribute dieser Klasse ( p1%). Es ist dabei wie immer unerheblich, ob es sich um Funktionen oder Variablen handelt. Die Variablen im Beispiel sind fname und lname, denen die Initialisierungsvariablen first-name und last-name zugeordnet werden. Für den Zugriff von anderen Klassen werden Zu-griffsfunktionen angeboten (Kommentar ; 1). Bitte beachten Sie, dass keine Möglichkeit vorgesehen ist, den Vornamen einer Person zu ändern, wohl aber deren Nachnamen.
Eventuell erklärungsbedürftig ist die Funktion, mit der die Zugehörigkeit eines Objekts zu dieser Klasse getestet wird. Der Name für das Objekt selbst ist this, somit sind C++-Programmierer im Vorteil. Der noch zu klärende Teil ist die Zeile (sequence (super-init)). In jeder Klasse, die Sie definieren, muss an einer Stelle die Initialisierungsmethode des Vorgängers aufgerufen werden, um dessen Instanzvariablen anzulegen. Damit ist die erste Klasse in Scheme implementiert. Betrachten wir einmal, wie ein Objekt angelegt und manipuliert werden kann (Listing 3).
Mit send wird eine Nachricht an ein Objekt versandt, mit ivar auf Instanzvariablen zugegriffen. Für das Anlegen eines Objekts steht die Funktion make-object zur Verfügung. Diese Funktion wird automatisch (wahrscheinlich durch ein Makro) definiert und erwartet als ersten Parameter einen Klassennamen. Nachfolgende Parameter sind die geforderten Initialisierungsparameter, im Beispiel handelt es sich um zwei Parameter. Es bleibt also festzuhalten:
- Objekte werden mit make-objectKlassenname angelegt.
- Funktionen einer Klasse werden mit send aufgerufen.
- Auf Instanzvariablen greift man mit ivar zu.
Betrachten wir nun, wie Vererbung in DrScheme genutzt werden kann.
Vererbung in DrScheme
Wie erwähnt unterstützt DrSchemes Objektsystem nur die einfache Vererbung sowie eine Art Mehrfachvererbung durch Schnittstellenklassen (Interfaces). Lassen Sie sich von der Länge des Beispiels in Listing 4 nicht abschrecken. Es ist so lang, da es möglichst viele Elemente der Vererbung zeigen soll und später in den Beispielen für GUI ebenfalls eingesetzt wird.
Dass p2% von p1% abgeleitet wurde, erkennt man am Klassenkopf. Dort wird als Vorgänger der Klasse p2% p1% angegeben. Die abgeleitete Klasse erwartet weitere Initialisierungselemente (eine Adresse und eine E-Mail). Die beiden neuen Elemente sind von außen zugänglich (Kommentar ; 1) und änderbar (; 2). Weiterhin wurden Möglichkeiten vorgesehen, Objekte dieser Klasse dauerhaft speichern zu können (Persistenz) (; 3) und Objekte dieser Klasse durch eine beliebige (vierelementige) Liste anlegen zu können (; 4).
Spezielle Merkmale für die Behandlung von Vererbung sind (rename super-dummy dummy) sowie die Liste, die mit override eingeleitet wird. Möchten Sie in DrScheme auf ein Element des Vorfahren zugreifen, müssen Sie dem Element einen neuen Namen geben ( rename). Die geerbte und umbenannte Version wird im Abschnitt override (dummy (+ super-dummy 2)) aufgerufen.
Mit override geben Sie an, eine geerbte Variable überschreiben zu wollen. Da der Wert der Instanzvariablen dummy in p1% eins ist, wird der Wert hier auf drei gesetzt. Damit wurden (fast) alle Auszeichnungen für die Vererbung in DrScheme vorgestellt. Bitte schauen Sie in [2] nach ausgelassenen Elementen.
Generische Funktionen
Der Zugriff auf Instanzvariablen mit dem Konstrukt (ivar obj symbol) kann sehr fehleranfällig und/oder ineffizient sein. Als Vereinfachung bieten sich so genannte generische Funktionen an.
Listing 5: Benutzung generischer Funktionen |
(define p-dummy (make-generic/proc p1% `dummy))
(define (use-of-generic-functions)
(let ((p1 (make-object p1% "foo" "bar"))
(p2 (make-object p2% "foo" "bar"
"Footown" "foo@Footown")))
(display "p1% dummy = ")
(display (p-dummy p1))
(display "; p2% dummy = ")
(display (p-dummy p2))
(newline)
(values)))
|
In Listing 5 wird mit dem ersten define eine Funktion definiert, die auf alle Nachfolger von p1% anzuwenden ist und den Inhalt des Symbols dummy ausgibt. Wie in fast allen Lisps sind Funktionen nicht an Klassen gebunden. Die Ausgabe der Funktion dürfte
"p1% dummy = 1; p2% dummy = 3"
sein. Damit möchte ich die Vorstellung des Objektsystems von DrScheme beenden und als krönenden Abschluss dieses Artikels das DrScheme-GUI-Toolkit (MrEd genannt) vorstellen.
GUI-Programmierung mit MrEd von DrScheme
Die Elemente zur Erstellung grafischer Benutzeroberflächen basieren auf dem Objektsystem von DrScheme und dem C++-Toolkit wxWindows (http://wxwindows.org). Elemente der Oberfläche wie Knöpfe, Dialoge und Rahmen sind in ein objektorientiertes System eingebettet. Auch deshalb haben wir das Objektsystem zuerst vorgestellt. Wie bei nahezu allen Elementen von DrScheme, ist die Dokumentation geeignet, sich mit der Funktionalität von MrEd vertraut zu machen (siehe [2]). Ein erstes GUI-Beispiel ist schnell realisiert:
(define (hello-scheme-gui-world)
(message-box "Hello World"
(format "Hallo schöne ~nScheme-Gui~n Welt~n")
#f
`(ok)))
Was wären wir schon ohne das famose “Hello World”? ;-)

Hello World mit GUI.
Die Message-Box hat keine besondere Funktionalität und liefert Symbole zurück, die für den gewählten Button stehen. Es handelt sich um einen modalen Dialog, den Sie schließen müssen, bevor Sie irgendetwas anderes machen können. Der erste Parameter der Funktion ist der Name für den Dialog. Beim zweiten handelt es sich um eine Zeichenkette, die Sie beliebig formatieren können. Für die Formatierung wurde hier DrSchemes format-Funktion benutzt.
Der nächste Parameter gibt an, zu wem das Meldungsfenster gehört; #f bedeutet, dass es sich hier um das Element handelt, von dem aus der Dialog aufgerufen wurde. Der letzte Parameter ist eine Liste der gewünschten Knöpfe. Diese Liste darf nur ein Element enthalten!
Interaktion mit dem System
Grafische Benutzeroberflächen sind eigentlich nur für interaktive Eingaben interessant. Die GUIs basieren auf folgendem Schema:
Es gibt eine Ereignisschleife, in der auf Reaktionen von Benutzern gewartet und den Eingaben entsprechend reagiert wird. Ereignisse werden üblicherweise in eine Queue gepackt und sequentiell abgearbeitet. Die Anbindung von Ereignissen auf Benutzereingaben erfolgt oft durch Callback-Funktionen.
Listing 6: Beispiel für die Interaktion mit dem GUI |
(define (frame-example)
(let* ((frame (make-object frame% "Beispiel"))
(msg (make-object message% "Bislang nichts los hier" frame)))
(send (make-object button% "Drück mich ;-)" frame
(lambda (button event)
(send msg set-label "Maus-klick")
(send button set-label "Bin gedrückt worden")))
min-width 200)
(send frame show #t)))
|
Das GUI-Modell MrEds bildet keine Ausnahme. Betrachten wir daher in Listing 6 zunächst einmal die Interaktion mit der grafischen Benutzeroberfläche.

Fenster vor dem Drücken des Knopfs

Fenster nach dem Drücken des Knopfs
Die Callback-Funktion eines Knopfes ist eine Funktion mit zwei Parametern: einem Knopf und einem Ereignis. Hier wurden die Parameter einfach ignoriert und nur der Meldungstext auf Maus-klick geändert.
Umfangreicheres GUI-Beispiel
Alle bisherigen Beispiele waren so strukturiert, dass ein bestimmtes Sprachmerkmal Schemes gezeigt wurde. In diesem Beispiel werden verschiedene Elemente kombiniert. Da es sich um fast drei Seiten Quelltext handelt (siehe [3]), kann nur auf einige Programmauszüge eingegangen werden.
Listing 7: Erstellen und Anzeigen des Dialogs |
(define (make-dialog)
(let* ((frame (make-object frame%
dialog-title
#f
initial-frame-width
initial-frame-height))
(hp (make-object horizontal-pane% frame))) ;; md 1
(set! vp1 (make-vertical-pane hp first-vertical-pane-template))
; md 2
(set! vp2 (make-vertical-pane hp second-vertical-pane-template))
; md 2
(let ((btn-pane (make-button-pane hp)))
; md 2
;(send frame stretchable-width #f)
(send frame stretchable-height #f)
frame)))
|
Listing 8: Die Ausgabemaske |
(define (make-vertical-pane parent elements)
;; a vertical pane just for displayint the contents of a list of
;; addresses
(let ((vertical-pane (make-object vertical-pane% parent))) ; mvp 1
(let loop ((el elements))
(cond
((null? el) `())
(else (cons (make-single-text-field
(first el) ; label for the field
vertical-pane
void ; callback here do nothing
initial-text-field-width
initial-text-field-height) ;initial size
(loop (cdr el))))))
vertical-pane))
|
Der Dialog in Listing 7 besteht aus verschiedenen Elementen (Listing 8). Für den Top-Level bietet sich entweder ein Rahmen (frame) oder ein Dialog (dialog) an (im Beispiel ein Rahmen). In den Rahmen werden ein horizontal ausgerichtetes Element (Kommentar ; md 1) sowie drei Elemente mit vertikal ausgerichteten Unterelementen (; md 2) aufgenommen. Sie vermissen vielleicht Platzierungsanweisungen, doch die benötigt man glücklicherweise nicht, da ein Geometrie-Manager die Elemente verwaltet. Man muss also nicht die einzelnen Elemente so lange verschieben, bis sie endlich passen, sondern legt nur noch fest, welche Elemente in welcher Reihenfolge im Dialog vorkommen sollen.
Bleibt die Frage zu klären, wie die Ausgabemaske aussehen soll. Schauen Sie sich noch einmal die Deklaration der Klasse p2% an. Sie besitzt vier Instanzvariablen für Informationen. Die Ausgabe der Adressen erfolgt in zwei Spalten mit jeweils zwei Textfeldern. Für die Aufnahme der Ausgabefelder wird ein Rahmen mit vertikal ausgerichteten Elementen vorgesehen. Die Funktion make-vertical-pane, abstrahiert das Anlegen einer solchen Spalte.
Beim ersten Parameter handelt es sich um das Element, in das eine neue Spalte eingefügt werden soll. (Kommentar ; mvp1). Der zweite Parameter ist eine Liste aus Zeichenketten, mit denen das Textfeld beschriftet wird. Für die Bedienung ist eine Knopfleiste vorgesehen (Listing 9).
Listing 9: Erstellung einer Knopfleiste zur Interaktion |
<b-5>si(define (make-button-pane parent)
;; two buttons to click through a list of addresses
;; one quit button for finishing the program
(let ((button-pane (make-object vertical-pane% parent)))
(let loop ((btn-list (button-list-template)))
(cond
((null? btn-list) `())
(else (cons (send (make-button
(list-ref (first btn-list)
button-label-const)
button-pane
(list-ref (first btn-list)
button-callback-const))
min-width initial-button-width) ; bp 1
(loop (rest btn-list))))))
button-pane))
|
Auch hier wurde das gleiche Schema wie für die Textfelder angewandt. Allerdings sieht die zu verarbeitende Liste etwas anders aus:
(define (button-list-template)
`(("&previous" ,previous-button-callback)
("&next" ,next-button-callback)
("&Quit" ,quit-button-callback)))
Die Liste besteht aus einem Namen und einem ausgewerteten Symbol. Betrachten wir beispielhaft das erste Element der Liste. Der Knopf wird mit previous beschriftet. Die zugehörige Callback-Funktion hat den Namen previous-button-callback. Das Symbol wird ausgewertet und man erhält die dahinter stehende Funktion, die als Callback-Funktion registriert werden kann.
Der Vorteil der indirekten Vorgehensweise liegt in der möglichen Wiederverwendbarkeit von Methoden, die — in diesem Fall — Ausgabespalten generieren. Außerdem lassen sich so die Anwendungsdaten des Programms an einem Punkt bündeln. Etwaige Änderungen wie zum Beispiel das Hinzufügen eines weiteren Feldes, sind einfach vorzunehmen.
Da die Knöpfe die gleiche Ausdehnung haben sollten, wurde eine Mindestbreite (; bp 1) vorgesehen. Das Zeichen & vor einzelnen Buchstaben kennen Sie vielleicht von Windows. Damit legen Sie fest, mit welcher Tastaturkombination der Knopf zusätzlich bedient werden kann.
Betrachten wir nun die Implementierung für einen Knopf aus der Knopfleiste. Sie sehen in Listing 10, dass counter immer um eins erniedrigt wird, es sei denn, man befindet sich am Ende der Liste, dann wird counter auf die Anzahl der Elemente der Liste gesetzt. Sie können sich also beliebig lange durch diese Liste bewegen. In der Funktion update-persons-data werden die Daten der aktuellen Person angepasst und die neuen Werte in die Ausgabeelemente eingetragen.
Listing 10: Callback für den Knopf Previous |
(define (previous-button-callback button event)
(if (= counter 0)
(set! counter number-of-persons)
(set! counter (- counter 1)))
(update-persons-data))
|
Zusammenfassung
In diesem Teil haben Sie gesehen, dass Scheme nicht nur für die funktionale Programmierung eingesetzt werden kann, sondern dass die Sprache offen für Erweiterungen und andere Vorgehensweisen ist, etwa die objektorientierte Programmierung. DrScheme und eine Reihe weiterer Scheme-Implementierungen (www. schemers.org) bieten Objektsysteme zur objektorientierten Programmierung an. Das von DrScheme ist nicht das umfangreichste, es ist aber einfach zu verstehen und reicht zur Implementierung eines nicht trivialen Systems (MrEd) aus.
Die Art und Weise der Bedienung des Systems weist auf eine ähnliche Implementierung wie in [1] hin. Offensichtlich reichen lexikalische Bindung und Funktionen aus, ein Objektsystem zu implementieren — was ich bemerkenswert finde.
Ausblick
Im nächsten Teil stelle ich Ihnen Scheme als Skriptsprache (für Shell- und auch Netz-Programmierung) vor und gehe dazu auf eine Scheme-Implementierung ein, die speziell für die Shell-Programmierung unter Unix entwickelt wurde. Es handelt sich dabei um die SCSH (Scheme-Shell) von Olin Shivers. Interessant dürften auch Scheme-Implementierungen sein, die andere besondere Merkmale haben, zum Beispiel Schemes, die unproblematisch auf Java-Klassen zugreifen können.
Wie immer freue ich mich auf Ihre Zuschriften (frido@q-software-solutions.com) und hoffe, dass Ihnen die Serie genug Anreize gibt, Scheme einmal auszuprobieren. ( uwo) n
Infos |
|
[1] Linux Magazin 1/01, S. 165f Kapitel “Datenkapselung in Scheme” [2] Online-Handbuch MrEd von DrScheme (HelpDesk -> Manuals -> MrEd) [3] Quellen zu diesem Teil: ftp://ftp.linux-magazin.de/pub/listings/magazin/2001/03/Scheme [4] Online-Handbuch MzScheme von DrScheme (HelpDesk -> Manuals -> MzScheme) |
Der Autor |
|
Friedrich Dominicus arbeitet seit Version 0.99pl13 mit Linux. Er hat fast alle Distributionen schon einmal ausprobiert und ist seit zwei Jahren Debianer. Derzeit entwickelt er mit ein paar Leuten — selbstverständlich unter Linux — eine neue Software mit Eiffel, die hier sicher einmal vorgestellt werden wird. Seit Februar 2000 ist er zweifacher Vater und freut sich über jeden Tag mit seinen Töchtern. Seine Hobbys sind Computer, Sport und Lesen. |




