# Created 2024-11-19 Tue 16:34
#+title: Ein Scheme-Primer
#+author: Christine Lemmer-Webber und das Spritely Institute, Übersetzer Florian Pelz
#+language: de
#+odt_styles_file: styles.xml
#+html_head:
[[https://spritely.institute/static/papers/scheme-primer.html][Englisches Original.]]
Dieser Text bietet einen Einstieg in die [[https://de.wikipedia.org/wiki/Scheme][Scheme-Familie]] von
Programmiersprachen. Er wurde ursprünglich geschrieben, um Leuten zu
helfen, die mit der beim [[https://spritely.institute/][Spritely Institute]] entwickelten Technologie
noch nicht vertraut sind. Trotzdem wurde er so allgemein gestaltet,
dass jeder diesen Primer lesen kann, der sich für Scheme interessiert.
Dieses Dokument ist dual lizenziert unter [[https://www.apache.org/licenses/LICENSE-2.0][Apache v2]] und [[https://creativecommons.org/licenses/by/4.0/][Creative
Commons Attribution 4.0 International]]. Sein Quellcode ist [[https://gitlab.com/spritely/scheme-primer][öffentlich
verfügbar]].
* Einführung
In der ganzen Welt der Computer-Programmierung gibt es nur wenige
Sprachen, die so einfach, klar, umfassend, mächtig und erweiterbar
sind wie Scheme. Die Einführung am Anfang der [[https://schemers.org/Documents/Standards/R5RS/][R5RS-Ausgabe]] von
Schemes Standardisierung[fn:rnrs] erklärt die Philosophie dahinter
gut:
#+begin_quote
Programmiersprachen sollten nicht entworfen werden, indem man
Funktionalitäten über Funktionalitäten schichtet, sondern indem man
Schwächen und Einschränkungen entfernt, die es so erscheinen lassen,
als wären weitere Funktionalitäten nötig.
#+end_quote
Durch diesen Minimalismus sind die Grundlagen von Scheme leicht zu
erlernen. Die R5RS-Einführung geht weiter mit:
#+begin_quote
Scheme demonstriert, dass eine sehr kleine Zahl von Regeln, wie
Ausdrücke gebildet werden, ohne Einschränkungen, wie man sie
zusammenfügt, genügen, um eine praktische und effiziente
Programmiersprache zu bilden, die flexibel genug ist, die meisten
größeren Programmierparadigmen, die heute genutzt werden, zu bedienen.
#+end_quote
Mit nur einer kleinen Anzahl Regeln und unglaublich einfacher Syntax
ist es in Scheme möglich, jedes Sprachparadigma aufzugreifen, das
Ihnen einfällt. Wegen der minimalistischen Basis und starken
Erweiterbarkeit hat Scheme eine Anhängerschaft und viele Nutzer in der
akademischen Forschung zu Sprachen gefunden.
Doch mit einem guten Ruf in einem Bereich geht ein schlechter in einem
anderen einher. Weil man Scheme als akademische Sprache auffasst,
wird gerne behauptet, es sei zu schwer, als dass „normale“
Programmierer es sich aneignen sollten.
In Wirklichkeit ist an Scheme gar nichts schwierig. Jeder
Programmierer kann es in sehr kurzer Zeit erlernen. (Tatsächlich gilt
das erst recht für Kinder und
Nichtprogrammierer.)[fn:scheme-and-non-programmers]
Beim [[https://spritely.institute/][Spritely Institute]] haben wir beschlossen, unsere Kerntechnologie,
[[https://spritely.institute/goblins/][Spritely Goblins]], auf Scheme aufzubauen. Es hat sich für uns
herausgestellt, dass es zwar exzellente tiefgehende Literatur zu
Scheme gibt und auch ein paar einfache, kurze Scheme-Anleitungen, aber
eine Einführung auf mittlerem Niveau fehlt. Der folgende Text stellt
daher ein Mittelding zwischen einem kurzen und einem umfassenden
Überblick von Scheme dar. Um mit Scheme produktiv loslegen zu können,
genügt es, den weiteren Text grob zu lesen, aber enthusiastische Leser
bekommen auch die Hintergründe erklärt (besonders in den Fußnoten).
Wir machen keine Annahmen über bisherige Programmierkenntnisse,
dennoch werden erfahrene Leser schneller durch die ersten Kapitel
durchkommen. Je weiter diese Anleitung voranschreitet, desto
fortgeschrittener werden die Themen. Zum Schluss gibt es ein
besonderes Spektakel: Wir sehen, [[#scheme-in-scheme][wie man einen Scheme-Interpetierer in
Scheme schreibt]], in nur 30 Code-Zeilen.
[fn:rnrs] R5RS steht für „Revised(5) Report on the Algorithmic
Language Scheme“. Das ist nicht die einzige Auflage, die es von
Scheme gibt, aber es ist besonders klein, klar und minimal.
Vielleicht ist es sogar auf eine Art zu klein: Es ist allgemein
bekannt, dass Implementierungen von R5RS-Scheme miteinander
inkompatibel sind, wenn man sich die damit tatsächlich genutzten
Bibliotheken anschaut. Das hat nicht zuletzt den Grund, dass R5RS
überhaupt kein System festlegt, wie man Bibliotheken schreibt!
Allerdings ist R5RS kurz und leicht zu lesen. Wenn Ihnen der Primer
hier gefällt, empfehlen wir, dass Sie auch entweder R5RS lesen
([[https://web.archive.org/web/20190924235927/https://pelzflorian.de/files/r5rs.de.try3.html][inoffizielle deutsche Übersetzung]]), oder R7RS-small, was eine nur
wenig längere, miteinander kompatible Fassung des Reports zu Scheme
darstellt.
[fn:scheme-and-non-programmers] Im Allgemeinen kümmern sich die
Texteditoren, die Programmierer von Scheme/Lisp verwenden, um die
Klammern in deren Code. Meistens lesen sie den Code anhand seiner
Einrückung und nicht anhand der Gruppierung durch Klammern. Anders
gesagt verbringen Lisp-Programmierer ihre Zeit gar nicht damit, über
Klammerung nachzudenken.
Weil aber die Syntax der meisten Programmiersprachen Klammerung
/nicht/ so allumfassend benutzt, fällt es Programmierern mit
Kenntnissen in anderen Sprachen als Lisp teils schwer, mit dem
geklammerten Stil in Lisps Syntax klarzukommen. (Dagegen ist es sogar
leichter für Schüler ganz ohne Programmiererfahrung, wenn sie diese
traditionelle Lisp-Syntax erlernen.)
Als wir Workshops veranstaltet haben, die zeigen, wie man
programmiert, haben wir die Erfahrung gemacht, dass die, die zum
ersten Mal Programmieren lernen, die Syntax von Lisp nicht für
abschreckend halten, während Programmierern mit Vorkenntnissen in den
meisten anderen Sprachen die Lisp-Syntax erst einmal fremd vorkommt.
Wir haben sogar die Erfahrung gemacht, dass es beim parallelen
Unterrichten von Scheme (in Form von Racket) und Python vielen
Lernenden ohne jeglichen Programmierhintergrund (die Workshops
richteten sich an Studenten mit geisteswissenschaftlichem Hintergrund)
wesentlich lieber war, mit geklammerter Lisp-Syntax zu arbeiten, die
sie verständlicher fanden und wo ihnen die Fehlersuche leichter fiel,
vorausgesetzt der Texteditor unterstützt sie. (In Racket ist diese
Unterstützung in deren einsteigerfreundlicher Entwicklungsumgebung,
DrRacket, vorhanden.) Sie können mehr über dieses Phänomen im Vortrag
[[https://fosdem.org/2022/schedule/event/lispforeveryone/][Lisp but Beautiful; Lisp for Everyone]] hören.
* Einrichten
Sie werden sich entscheiden müssen, mit welcher Scheme-Implementierung
Sie arbeiten möchten, und auch mit welchem Editor.
Es gibt für beides viele Möglichkeiten, aber wir machen hier nur zwei
Vorschläge:
- [[https://www.gnu.org/software/guile/][Guile Scheme]] + [[https://www.gnu.org/software/emacs/][GNU Emacs]] + [[https://www.nongnu.org/geiser/][Geiser]]: Damit wurde diese Anleitung
verfasst. Diese Wahl steht für mächtige Werkzeuge (und
anschließend können Sie damit an Code für [[https://guix.gnu.org/][Guix]] arbeiten, einem der
interessantesten Scheme-Projekte überhaupt). Andererseits ist die
Lernkurve hier auch beachtlich.
- [[https://racket-lang.org/][Racket]], das eine eingebaute Entwicklungsumgebung („IDE“) namens
DrRacket mitbringt. Es ist für den Anfang die leichtere Wahl.
Am Anfang mancher Code-Beispiele schreiben wir =REPL>=. /REPL/ steht
für „Read Eval Print Loop“ (Lese-Auswerten-Schreiben-Schleife). Damit
ist eine interaktive Scheme-Eingabeaufforderung gemeint, auf der man
Ausdrücke, englisch /expressions/, probeweise eintippen kann.
* Hallo Scheme!
Hier sehen Sie das vertraute Hallo-Welt-Programm, geschrieben in
Scheme:
#+begin_src scheme
(display "Hallo Welt!\n")
#+end_src
So wird „Hallo Welt!“ auf dem Bildschirm angezeigt. (Das ="\n"= steht
für eine neue Zeile, wie wenn Sie die Eingabeteste drücken, nachdem
Sie Text in ein Textverarbeitungsprogramm eingetippt haben.)
Wenn Sie sich schon mit anderen Programmiersprachen auskennen, sieht
das vielleicht ein bisschen vertraut und doch ein wenig anders aus.
In den meisten anderen Programmiersprachen würde es etwa so
geschrieben:
#+begin_src python
display("Hallo Welt!\n")
#+end_src
In diesem Sinne ist das Aufrufen von Funktionen in Scheme (und anderen
ähnlichen Lisps) nicht viel anders als in den anderen Sprachen, bis
auf dass der Name innerhalb der Klammern steht.
* Grunddatentypen, ein paar kurze Funktionen
Nicht so wie in manchen anderen Sprachen sind mathematische Ausdrücke
wie =+= und =-= Präfix-Funktionen wie andere Funktionen auch, daher
stehen sie vorne:
#+begin_src scheme
(+ 1 2) ; => 3
(/ 10 2) ; => 5
(/ 2 3) ; => 2/3
#+end_src
Die meisten davon können mehrere Argumente nehmen:
#+begin_src scheme
(+ 1 8 10) ; äquivalent zu "1 + 8 + 10" in Infix-Notation
#+end_src
Prozeduren können auch verschachtelt werden. Mit der
„Substitutionsmethode“ können wir ermitteln, wozu sie vereinfacht
werden:
#+begin_src scheme
(* (- 8 (/ 30 5)) 21) ; Anfangsausdruck
(* (- 8 6) 21) ; vereinfachen: (/ 30 5) => 6
(* 2 21) ; vereinfachen: (- 8 6) => 2
42 ; vereinfachen: (* 2 21) => 42
#+end_src
Eine Vielzahl von Typen wird unterstützt. Hier zum Beispiel sind ein
paar mathematische Typen:
#+begin_src scheme
42 ; Ganze Zahlen, "integer"
98.6 ; Gleitkommazahlen, "floating point"
2/3 ; Brüche bzw. "rationale" Zahlen
-42 ; sie können alle auch negativ sein
#+end_src
Weil Scheme einerseits mit „exakten“ Zahlen wie Integern und Brüchen
umgehen kann, ohne Einschränkungen bezüglich der Größe der Zahlen, ist
es sehr gut geeignet für genaues wissenschaftliches und mathematisches
Rechnen. Die Gleitkommadarstellung wird als „inexakt“ aufgefasst; wir
verzichten dort auf Genauigkeit zu Gunsten der Geschwindigkeit.
Hier sind noch ein paar Typen:
#+begin_src scheme
#t ; "boolean" für wahr/true
#f ; "boolean" für falsch/false
"Pangalaktischer Donnergurgler"; String (Text, also eine Zeichenkette)
'foo ; Symbol
'(1 2 3) ; eine Liste (von Zahlen, in diesem Fall)
(lambda (x) (* x 2)) ; Prozedur (dazu später mehr)
'(lambda (x) (* x 2)) ; eine Liste von Listen, Symbolen und Zahlen
#+end_src
Symbole sind der vielleicht seltsamste Typ, wenn Sie andere
Programmiersprachen als Lisp schon kennen (mit ein paar Ausnahmen).
Während Symbole nach Strings aussehen, ist ihre Bedeutung eher
programmiertechnischer Natur.
(In der Syntax für Methoden in /Goblins/ bezeichnen wir Methoden
mit Symbolen als Methodenname.)
Interessanterweise können wir Ausdrücke in Lisp maskieren
beziehungsweise quotieren mit ='=, zum Beispiel gelten oben im
quotierten =lambda=-Ausdruck die Symbole im Innern auch automatisch
als quotiert.
Wir werden uns für Listen Zeit nehmen, wenn wir in
[[#scheme-lists-and-cons][Listen und "cons"]] angekommen sind.
Die Kombination von Listen und Symbolen spielt in vielen Lisps eine
große Rolle, so auch in Scheme, denn darin liegt Schemes
Erweiterbarkeit begründet: Code kann Code schreiben.
Auf diese Stärke und wie man sie benutzt werden wir näher eingehen in
[[#scheme-extensibility][Über die Erweiterbarkeit von Scheme (und Lisp generell)]].
* Variable und Prozeduren
Variablen können wir Werte zuweisen, indem wir =define= verwenden:
#+begin_src scheme
REPL> (define name "Jane")
REPL> (string-append "Hallo " name "!")
; => "Hallo Jane!"
#+end_src
Wenn wir das, was auf =define= folgt, in Klammern einpacken,
interpretiert Scheme das Ganze als eine Prozedurdefinition:
#+begin_src scheme
(define (greet name)
(string-append "Hallo " name "!"))
#+end_src
Jetzt, wo die Prozedur einen Namen hat, können wir sie aufrufen:
#+begin_src scheme
REPL> (greet "Samantha")
; => "Hallo Samantha!"
#+end_src
Wie Sie merken, sind in /Scheme/ Return-Anweisungen /implizit/. Das
heißt, weil es der Ausdruck am Ende der Prozedur ist, wird das
Ergebnis vom =string-append= automatisch auch das zurückgelieferte
Ergebnis an den Aufrufer.
Eigentlich ist diese zweite Syntax für =define= bloß /syntaktischer Zucker/.
Diese beiden Definitionen von =greet= sind genau das Gleiche.
#+begin_src scheme
(define (greet name)
(string-append "Hallo " name "!"))
(define greet
(lambda (name)
(string-append "Hallo " name "!")))
#+end_src
=lambda= ist die Bezeichnung für eine „anonyme Prozedur“ (sie hat also
keinen Namen). Obwohl wir ihr den Namen =greet= zuweisen, könnte man
die Prozedur auch ohne einen Namen benutzen:
#+begin_src scheme
REPL> ((lambda (name)
(string-append "Hallo " name "!"))
"Horatio")
; => "Hallo Horatio!"
#+end_src
Es gibt auch andere Möglichkeiten, Dingen einen Namen zu geben, außer
mit =define=, nämlich mit =let=. Dafür gibt man eine Reihe von
Variablen an, die gebunden werden, und danach gibt man einen Rumpf an,
der mit diesen Bindungen ausgewertet wird.
=let= hat die Form:
#+begin_src scheme
(let (() ...)
...)
#+end_src
(Mit =...= meinen wir in diesem Beispiel, dass der vorherige Ausdruck
wiederholt angegeben werden darf.)
Hier sehen wir ein Beispiel, wie =let= benutzt wird:
#+begin_src scheme
REPL> (let ((name "Horatio"))
(string-append "Hallo " name "!"))
; => "Hallo Horatio!"
#+end_src
Schlauen Lesern mag auffallen, dass das sehr ähnlich aussieht wie im
letzten Beispiel, und ja, auch =let= ist /Syntaxzucker/ für ein
lambda, das wir sofort und mit Argumenten anwenden.
Die letzten beiden Code-Beispiele sind völlig äquivalent:
#+begin_src scheme
REPL> (let ((name "Horatio"))
(string-append "Hallo " name "!"))
; => "Hallo Horatio!"
REPL> ((lambda (name)
(string-append "Hallo " name "!"))
"Horatio")
; => "Hallo Horatio!"
#+end_src
=let*= ist wie =let=, allerdings dürfen sich Bindungen auf vorherige
Bindungen innerhalb des Ausdrucks beziehen:[fn:more-lets]
#+begin_src scheme
REPL> (let* ((name "Horatio")
(greeting
(string-append "Hallo " name "!\n")))
(display greeting)) ; greeting auf dem Bildschirm anzeigen
; zeigt an: Hallo Horatio!
#+end_src
Es ist möglich, eine Liste von Argumenten manuell auf eine Prozedur
anzuwenden, mit =apply=. Zum Beispiel können wir die Summe über eine
Liste von Zahlen bilden, indem wir =apply= mit =+= kombinieren.
#+begin_src scheme
REPL> (apply + '(1 2 5))
; => 8
#+end_src
Umgekehrt lässt sich eine Argumentliste variabler Länge in
Punkt-Notation beschreiben.[fn:dot-notation-cons-related] Das zeigen
wir nun zugleich mit Guiles =format=-Prozedur. Wir können sie mit
=#f= als erstem Argument aufrufen, so dass eine formatierte
Zeichenkette als Wert zurückgeliefert wird, aber mit =#t= als erstem
Argument wird sie stattdessen auf dem Bildschirm angezeigt. Letzteres
brauchen wir hier:
#+begin_src scheme
REPL> (define (geschwätziges-add geschwätziger-name . nums)
(format #t "<~a> Wenn Sie die addieren, kommt ~a raus!\n"
geschwätziger-name (apply + nums)))
REPL> (geschwätziges-add "Chester" 2 4 8 6)
; Zeigt an:
; Wenn Sie die addieren, kommt 20 raus!
#+end_src
Auch wenn sie nicht zum Standard von Scheme gehören, unterstützen
viele Scheme-Implementierungen auch optionale Argumente sowie
Schlüsselwort-Argumente.
Guile implementiert diese Abstraktion als =define*=:
#+begin_src scheme
REPL> (define* (ladenbesitzer artikel-den-du-kaufst
#:optional (wie-viele 1)
(preis 20)
#:key (ladenbesitzer "Michael")
(laden "Bands Frischeladen"))
(format #t "Du gehst in ~a, nimmst dir was von den Regalen\n"
laden)
(display "und gehst zur Kasse.\n\n")
(format #t "~a schaut dich an und sagt: "
ladenbesitzer)
(format #t "'~a ~a also? Das macht ~a Münzen!'\n"
wie-viele artikel-den-du-kaufst
(* preis wie-viele)))
REPL> (ladenbesitzer "Äpfel")
; Zeigt an:
; Du gehst in Bands Frischeladen, nimmst dir was von den Regalen
; und gehst zur Kasse.
;
; Michael schaut dich an und sagt: '1 Äpfel also? Das macht 20 Münzen!'
REPL> (ladenbesitzer "Bananen" 10 28)
; Zeigt an:
; Du gehst in Bands Frischeladen, nimmst dir was von den Regalen
; und gehst zur Kasse.
;
; Michael schaut dich an und sagt: '10 Bananen also? Das macht 280 Münzen!'
REPL> (ladenbesitzer "Schrauben" 3 2
#:ladenbesitzer "Horatio"
#:laden "Horatios Baumarkt")
; Zeigt an:
; Du gehst in Horatios Baumarkt, nimmst dir was von den Regalen
; und gehst zur Kasse.
;
; Horatio schaut dich an und sagt: '3 Schrauben also? Das macht 6 Münzen!'
#+end_src
Zu guter Letzt können wir Schemes Prozeduren noch etwas anderes
Interessantes tun lassen, nämlich können sie mehrere Werte
zurückliefern mit … =values=!
Aus purer Blödelei wählen wir als Beispiel, wie es wäre, zwei Zahlen
simultan zu addieren und zu multiplizieren:
#+begin_src scheme
REPL> (define (add-and-multiply x y)
(values (+ x y)
(* x y)))
REPL> (add-and-multiply 2 8)
; => 10
; => 16
REPL> (define-values (added multiplied)
(add-and-multiply 3 10))
REPL> added
; => 13
REPL> multiplied
; => 30
#+end_src
Wie Sie sehen können, erfassen wir beide Werte mit =define-values= wie
oben gezeigt. (Sie können auch =let-values= oder =call-with-values=
verwenden, aber in diesem Abschnitt haben wir genug Syntax gesehen!)
[fn:more-lets] Außerdem gibt es noch =letrec=, womit sich Bindungen
rekursiv aufeinander (oder sich selbst) beziehen können. Sowohl
=let*= als auch =letrec= können theoretisch zusätzliche Ressourcen
beanspruchen, die nicht unbedingt nötig wären (sogenannten Overhead),
aber fortschrittliche Compiler stellen fest, wenn sie zu =let=
äquivalent sind, und optimieren entsprechend. Wenn Scheme heute von
Grund auf neu entworfen würde, würde man vielleicht nur eine Art =let=
haben, die sowohl =let*= als auch =letrec= in sich aufnimmt. Doch
Geschichte lässt sich nicht ändern.
[fn:dot-notation-cons-related] Diese Notation hat direkt zu tun mit
dem Aufbau von =cons=-Zellen, worauf wir in [[#scheme-lists-and-cons][Listen und "cons"]] näher
eingehen.
* Bedingungen und Prädikate
Gelegentlich möchten wir prüfen, ob eine Aussage wahr ist oder falsch.
Zum Beispiel können wir nachsehen, ob es sich bei einem Objekt um
einen String handelt, indem wir =string?= fragen:[fn:string-huh]
#+begin_src scheme
REPL> (string? "Apfel")
; => #t
REPL> (string? 128)
; => #f
REPL> (string? 'Apfel)
; => #f
#+end_src
(Denken Sie daran, dass =#t= für "true" steht und =#f= für "false".)
Wir können das zusammen mit =if= einsetzen. Es hat die Form:
#+begin_src scheme
(if
[])
#+end_src
(Die eckigen Klammern um == bedeuten, dass man sie
weglassen darf.)[fn:when]
Wir können also eine ulkige Funktion schreiben, die begeistert darüber
Auskunft gibt, ob ein an sie übergebenes Objekt eine Zeichenkette ist
oder nicht:
#+begin_src scheme
REPL> (define (string-enthusiast obj)
(if (string? obj)
"Große Freude, das ist EIN STRING!!!"
"Das WAR JA GAR KEIN STRING!! STRINGS BITTE!"))
REPL> (string-enthusiast "Karotte")
; => "Große Freude, das ist EIN STRING!!!"
REPL> (string-enthusiast 529)
; => "Das WAR JA GAR KEIN STRING!! STRINGS BITTE!"
#+end_src
Wie wir sehen, liefert =if=, anders als in manchen anderen geläufigen
Sprachen, auch gleich den Wert als Ergebnis zurück, der sich ergibt,
wenn je nach == der eine oder andere Zweig ausgewertet wird.
In Scheme stehen von Anfang an bestimmte mathematische Vergleiche als
Test zur Verfügung. =>= und =<= bedeuten jeweils „größer als“ und
„kleiner als“, außerdem stehen =>== und =<== für „größer als oder
gleich“ und „kleiner als oder gleich“, wohingegen man mit === auf
numerische Gleichheit hin prüft:[fn:prefix-vs-infix]
#+begin_src scheme
REPL> (> 8 9)
; => #f
REPL> (< 8 9)
; => #t
REPL> (> 8 8)
; => #f
REPL> (>= 8 8)
; => #t
#+end_src
Wenn wir mehrere Möglichkeiten testen wollten, sind verschachtelte
=if=-Anweisungen möglich:
#+begin_src scheme
REPL> (define (goldlöckchen n smallest-ok biggest-ok)
(if (< n smallest-ok)
"Zu klein!"
(if (> n biggest-ok)
"Zu groß!"
"Genau richtig!")))
REPL> (goldlöckchen 3 10 20)
; => "Zu klein!"
REPL> (goldlöckchen 33 10 20)
; => "Zu groß!"
REPL> (goldlöckchen 12 10 20)
; => "Genau richtig!"
#+end_src
Jedoch sieht es gleich viel hübscher aus, wenn wir stattdessen die
Syntax namens =cond= benutzen. Sie hat die folgende
Form:[fn:cond-or-if]
#+begin_src scheme
(cond
( ...) ...
[(else ...)])
#+end_src
Vergleichen Sie, wie wir unsere Prozedur =goldlöckchen= gleich viel
lieber mögen mit =cond= anstelle von verschachtelten =if=-Anweisungen:
#+begin_src scheme
;; Version mit verschachtelten "if"
(define (goldlöckchen n smallest-ok biggest-ok)
(if (< n smallest-ok)
"Zu klein!"
(if (> n biggest-ok)
"Zu groß!"
"Genau richtig!")))
;; "cond"-Version
(define (goldlöckchen n smallest-ok biggest-ok)
(cond
((< n smallest-ok)
"Zu klein!")
((> n biggest-ok)
"Zu groß!")
(else
"Genau richtig!")))
#+end_src
Außerdem bietet Scheme mehrere verschiedene Vergleiche, ob zwei
Objekte gleich sind oder nicht.
Am kürzesten und einfachsten (aber nicht vollständig) kann man den Zoo
der Gleichheitsprädikate so zusammenfassen: =equal?= vergleicht den
Inhalt, während =eq?= die Identität der Objekte vergleicht (in dem
Sinn, wie es die Laufzeitumgebung der Sprache
festlegt).[fn:equality-is-a-tough-subject]
Zum Beispiel legt =list= eine neue Liste jedes Mal mit neuer Identität
an, deshalb sind folgende Objekte =equal?=, aber nicht =eq?=:
#+begin_src scheme
REPL> (define a-list (list 1 2 3))
REPL> (define b-list (list 1 2 3))
REPL> (equal? a-list a-list)
; => #t
REPL> (eq? a-list a-list)
; => #t
REPL> (equal? a-list b-list)
; => #t
REPL> (eq? a-list b-list)
; => #f
#+end_src
Abschließend gilt in Scheme, dass alles, was nicht =#f= ist, als true
aufgefasst wird. Das verwendet man manchmal etwa bei =member=.
=member= such nach passenden Elementen und liefert die verbleibende
Liste zurück, wenn etwas Gleiches gefunden wurde, und sonst =#f=:
#+begin_src scheme
REPL> (member 'b '(a b c))
; => (b c)
REPL> (member 'z '(a b c))
; => #f
REPL> (define (obstschnüffler obst korb)
(if (member obst korb)
"Das gesuchte Obst ist im Korb!"
"Ich kann das Obst nicht finden! Verdammich!"))
REPL> (define obstkorb '(apfel banane zitrone))
REPL> (obstschnüffler 'banane obstkorb)
; => "Das gesuchte Obst ist im Korb!"
REPL> (obstschnüffler 'ananas obstkorb)
; => "Ich kann das Obst nicht finden! Verdammich!"
#+end_src
[fn:string-huh] Auf Wahrheit / Falschheit testende Prozeduren heißen
in Scheme /Prädikate/. Der Ausdruck verwirrt manche, weil er bei
natürlichen Sprachen etwas anderes bedeutet und weil /Prädikate/ in
der Mathematik etwas über Relationen zueinander aussagen. Technisch
gesehen sagt ein Test, der einen booleschen Wert zurückliefert,
durchaus etwas aus über eben diesen Test, also ist der Begriff nicht
falsch, aber je nach Hintergrund nicht intuitiv.
Schemes Prädikaten gibt man traditionell einen Namen mit =?= als
Suffix. Auf Englisch wird das =?= gerne als „huh?“ gesprochen, daher
„string-huh?“. Einige andere Lisps hängen auf dieselbe Weise =-p= als
Suffix an; in Scheme nimmt man =?=.
[fn:when] Folgt man dem Scheme-Standard, ist die ==
technisch gesehen optional. Doch viele Schemes bieten eine separate
Prozedur namens =when= an mit folgender Form:
#+begin_src scheme
(when ...)
#+end_src
In /Racket/ ist ein =if= ohne == (man sagt „einbeiniges
=if=“) verboten und selbst wenn man kein /Racket/ benutzt, wird =when=
da vorgezogen. Mit hinein spielt, dass es in /rein funktionalen
Programmiersprachen/ gar keine Bedingung geben darf, bei der womöglich
gar nichts Wertvolles zurückgeliefert wird. Anders gesagt ergibt
=when= (oder ein „einbeiniges =if=“) nur Sinn, wenn man sich für einen
/Side Effect/, also eine Nebenwirkung, interessiert. Es ist gut, wenn
man sich des Unterschieds bewusst ist. Wir kommen später auf =when=
zurück und wie man es selbst schreibt, in [[#scheme-extensibility][Über die Erweiterbarkeit von
Scheme (und Lisp generell)]].
[fn:prefix-vs-infix] Zugegeben, Lisps Präfix-Notation ist hier weniger
angemessen als Infix-Notation, denn hinter dem Aussehen der spitzen
Klammern steckt die visuelle Notation der Idee, dass eines kleiner und
eines größer ist. Manche Schemes unterstützen [[https://srfi.schemers.org/srfi-105/srfi-105.html][SRFI-105: Geschweifte
Infix-Ausdrücke]], was leichter zu lesen ist. Zum Vergleich:
#+begin_src scheme
REPL> (> 8 9)
; => #f
REPL> (< 8 9)
; => #t
REPL> (> 8 8)
; => #f
REPL> (>= 8 8)
; => #t
#+end_src
Gegenüber:
#+begin_src scheme
REPL> {8 > 9}
; => #f
REPL> {8 < 9}
; => #t
REPL> {8 > 8}
; => #f
REPL> {8 >= 8}
; => #t
#+end_src
[fn:cond-or-if] Wie wir noch sehen werden in
[[#scheme-extensibility][Über die Erweiterbarkeit von Scheme (und Lisp generell)]],
erlaubt Scheme es uns, neue Formen von Syntax zu implementieren.
Für eine Scheme-Implementierung genügt eine grundlegende Form von
Syntax, denn =if= kann man als vereinfachte Version von =cond=
implementieren und =cond= als verschachtelte Abfolge von
=if=-Anweisungen.
[fn:equality-is-a-tough-subject] Die größte Untertreibung in den
Fußnoten der Informatik findet sich in [[https://dspace.mit.edu/handle/1721.1/44215][The Art of the Propagator]] von
Gerald Jay Sussman und Alexey Radul, wo es schlicht heißt:
#+begin_quote
Gleichheit ist ein schwieriges Thema.
#+end_quote
* Listen und "cons"
#+begin_verse
"My other CAR is a CDR"
--- Autoaufkleber eines Lisp-Anhängers
#+end_verse
Für strukturierte Daten verwendet man in Scheme Listen, die jeden
anderen Typ enthalten können.[fn:other-compound-types] Wir zeigen nun
zwei Schreibweisen für dieselbe Liste:
#+begin_src scheme
REPL> (list 1 2 "Miezekatze" 33.8 'foo)
; => (1 2 "Miezekatze" 33.8 foo)
REPL> '(1 2 "Miezekatze" 33.8 foo)
; => (1 2 "Miezekatze" 33.8 foo)
#+end_src
Es besteht dazwischen ein Unterschied, nämlich musste im quotierten
Beispiel das Symbol „foo“ nicht quotiert werden, weil es durch die
Quotierung der äußeren Liste implizit mitquotiert wird.
Wir haben eine „besondere“ Liste, bekannt als „die leere Liste“. Es
ist eine Liste ohne Elemente und wir schreiben sie einfach als ='()=.
(Man kennt sie auch als /nil/. Sie ist das einzige Objekt, für das
das Prädikat =null?= laut dem Standard von Scheme =#t= zurückliefert.)
Listen in /Scheme/ sind tatsächlich „verkettete Listen“. Das sind
Kombinationen aus Paaren, die jeweils „cons-Zelle“ genannt werden. Am
Ende der Paarabfolge steht die leere Liste:
#+begin_src scheme
REPL> '()
; => ()
REPL> (cons 'a '())
; => (a)
REPL> (cons 'a (cons 'b (cons 'c '())))
; => (a b c)
#+end_src
Äquivalent können wir eines hiervon schreiben:
#+begin_src scheme
REPL> (list 'a 'b 'c)
; => (a b c)
REPL> '(a b c)
; => (a b c)
#+end_src
Aus lang historischen Gründen[fn:cons-car-cdr-historical-reasons]
greift man auf das erste Element einer cons-Zelle mittels =car= zu und
auf das zweite Element einer cons-Zelle mit =cdr= (ausgesprochen
„kudder“):[fn:first-and-rest]
#+begin_src scheme
REPL> (car '(a b c))
; => a
REPL> (cdr '(a b c))
; => (b c)
REPL> (car (cdr '(a b c)))
; => b
#+end_src
Das zweite Element in =cons= muss keine andere cons-Zelle oder die
leere Liste sein. Wenn wir etwas anderes hineintun, haben wir eine
„gepunktete Liste“. Diese schreibt sich in einer für Lisp
ungewöhnlichen Infix-Syntax:
#+begin_src scheme
REPL> (cons 'a 'b)
; => (a . b)
#+end_src
Sie sollten den strukturellen Unterschied hierzu erkennen:
#+begin_src scheme
REPL> (cons 'a (cons 'b '()))
; => (a b)
#+end_src
Passt man nicht auf, verbringt man seine Zeit mit dem
Auseinanderklamüsern von =cons=-Zellen (wovon es heißt, dass Schemer
das viel zu oft täten, aber =cons= ist eben elegant und
mächtig).[fn:little-schemer]
In gewissem Sinn schweifen wir in diesem Unterabschnitt bloß ab. In
unserem Paper vermeiden wir =cons= mit Absicht und erwähnen =car= und
=cdr= gleich gar nicht im Haupttext. Warum erwähnen wir Listen dann
in einem eigenen Unterabschnitt?
Das liegt daran, dass wir auf ein wichtiges Thema hinarbeiten, nämlich
wie leicht sich Scheme erweitern lässt. Scheme ist in seinen
Kerndatentypen geschrieben worden und mit ihnen kann man die Sprache
auch verändern. Bald sagen wir mehr darüber, aber zunächst bringen
wir ein Beispiel, dass man jede Art Ausdruck quotieren kann und so aus
Code Daten macht:
#+begin_src scheme
REPL> (+ 1 2 (- 8 4))
; => 7
REPL> '(+ 1 2 (- 8 4))
; => (+ 1 2 (- 8 4))
REPL> (let ((name "Horatio"))
(string-append "Hallo " name "!"))
; => "Hallo Horatio!"
REPL> '(let ((name "Horatio"))
(string-append "Hallo " name "!"))
; => (let ((name "Horatio")) (string-append "Hallo " name "!"))
#+end_src
Das Beispiel eben macht uns neugierig: Endlich haben wir den Zweck von
Symbolen erkannt, denn mit ihnen kann man die Namen von Funktionen und
von Syntax erfassen, wenn man diese quotiert. Insofern kann man
sagen, Lisp (Scheme eingeschlossen) ist in Lisp geschrieben: Es gibt
kaum einen Unterschied zwischen der Darstellung, die der Programmierer
sieht, und der für den Compiler. Mehr dazu in [[#scheme-extensibility][Über die Erweiterbarkeit
von Scheme (und Lisp generell)]].
Es sei noch gesagt, wenn wir mit einem Apostroph quotieren, ist das
eigentlich nur eine Kurzschreibweise für =(quote )=:
#+begin_src scheme
;; die beiden sind dasselbe
'foo
(quote foo)
;; auch die beiden sind dasselbe
'(lambda (x) (* x 2))
(quote (lambda (x) (* x 2)))
#+end_src
Mit Listen kann man auch eine assoziative Abbildung zwischen
Schlüsseln und Werten darstellen. Das nennen wir /Alists/
(assoziative Listen). Es gibt mehrere Prozeduren, um den Wert zu
einem Schlüssel bequem nachzuschauen, darunter =assoc=, welche das
Paar mit ihm liefert, wenn es enthalten ist, und sonst =#f=:
#+begin_src scheme
REPL> (define tierlaute
'((katze . miau)
(hund . wuff)
(schaf . bäh)))
REPL> (assoc 'katze tierlaute)
; => (katze . miau)
REPL> (assoc 'alien tierlaute)
; => #f
#+end_src
Assoziative Listen lassen sich leicht implementieren, schauen
ansehnlich genug aus in der gedruckten Darstellung von Scheme und sind
geeignet für funktionale Programmierung.
(Sie möchten etwas in eine Alist einfügen? cons einfach noch eine
cons-Zelle d’rauf!)
Also sind sie beliebt bei Schemern.
Trotzdem sind sie oftmals nicht effizient.
Zwar ist =assoc= bei kurzen assoziativen Listen gut, aber wenn die
assoziative Liste eintausend Elemente lang ist, dauert es auch tausend
Schritte, ein am Ende vergrabenes Schlüssel-Wert-Paar nachzuschlagen.
Scheme-Implementierungen haben für gewöhnlich andere Datenstrukturen
wie Hashmaps auf Lager, die im Schnitt in konstanter Zeit nachschlagen
lassen. Manchmal sind sie die bessere Wahl.
Neben quote kann man auch quasiquote verwenden, was man mit einem
Backtick-Zeichen einleitet und mit einem Komma demaskiert man wieder.
Auf diese Weise kann man schnell zwischen den Daten und Code hin- und
herschalten. Betrachten wir eine etwas eigenartige Metrik, um
Katzenjahre in Menschenjahre umzurechnen:
#+begin_src scheme
REPL> (define (katzenjahre jahre)
(cond
((<= jahre 1) ; erstes Jahr macht 15 Menschenjahre
(* jahre 15))
((<= jahre 2)
(+ 15 (* 9 (- jahre 1)))) ; zweites Jahr 9
(else
(+ 24 (* 4 (- jahre 2)))))) ; spätere Jahre 4
REPL> (define (katze-eintrag name alter)
`(katze (name ,name)
(alter ,alter)
(katzenjahre-alter ,(katzenjahre alter))))
REPL> (katze-eintrag "Missy Rose" 16)
; => (katze (name "Missy Rose")
; (alter 16)
; (katzenjahre-alter 80))
REPL> (katze-eintrag "Kelsey" 22)
; => (katze (name "Kelsey")
; (alter 21)
; (katzenjahre-alter 104))
#+end_src
Mensch, sind das alte Katzen!
[fn:other-compound-types] /Scheme/ hat auch eingebaute Unterstützung
für /Vektoren/. Die sehen erstmal aus wie Listen, haben aber den
Vorteil, dass Zugriff in konstanter Zeit möglich ist, jedoch taugen
sie weniger für funktionale Programmierung, denn man kann keine neuen
Elemente vorne d’ranhängen. Es gibt in vielen Scheme-Sprachen noch
andere interessante Datentypen wie Hashmaps und benutzerdefinierte
Verbünde (Records).
[fn:cons-car-cdr-historical-reasons] Der Name =cons= ist sinnvoll; er
bedeutet, dass man ein Paar konstruiert/construct. Aber der Grund für
die Namen =car= und =cdr= ist ein rein historisches Detail der ersten
Lisp-Implementierung. =car= bedeutet „contents of the address
register“ und =cdr= bedeutet „contents of the decrement register“.
Beeindruckend, wie lange solche Begriffe überdauern, im Guten wie im
Schlechten.
Mehr interessante Lisp-Geschichte finden Sie hier:
- [[http://jmc.stanford.edu/articles/lisp.html][History of Lisp]] von John McCarthy
- [[https://www.dreamsongs.com/Files/Hopl2.pdf][The Evolution of Lisp]] von Guy L. Steele und Richard P. Gabriel
- [[https://www.softwarepreservation.org/projects/LISP/][History of LISP]] von Paul McJones
[fn:first-and-rest] Weil =car= und =cdr= nur „historische Details“
sind, mag man auf die Idee kommen, sie durch bessere Namen zu
ersetzen. Für jemanden, der nur Listen verwendet, erscheinen =first=
und =rest= wie gute alternative Namen:
#+begin_src scheme
REPL> (first '(a b c))
; => a
REPL> (rest '(a b c))
; => (b c)
REPL> (first (rest '(a b c)))
; => b
#+end_src
Das Problem ist, wenn eine cons-Zelle ein einfaches Paar enthält wie
=(cons 'a 'b)=, dann geht der Sinn wieder verloren … =rest= liefert
nur ein einzelnes Element und keine Folge. So verbleiben wir bei den
Namen =car= und =cdr=.
[fn:little-schemer] Es lässt sich noch viel über =cons= sagen. Im
Buch [[https://mitpress.mit.edu/books/little-schemer-fourth-edition][The Little Schemer]] wird es als prächtig tituliert, „=cons= the
magnificent“, und viel mehr auf =cons= eingegangen und darauf, wie man
einen intuitiven Sinn für Rekursion entwickelt.
* Closures (Abschlüsse)
Erinnern Sie sich zurück, wie wir =goldlöckchen= definiert und benutzt
haben:
#+begin_src scheme
REPL> (define (goldlöckchen n smallest-ok biggest-ok)
(cond
((< n smallest-ok)
"Zu klein!")
((> n biggest-ok)
"Zu groß!")
(else
"Genau richtig!")))
REPL> (goldlöckchen 3 10 20)
; => "Zu klein!"
REPL> (goldlöckchen 33 10 20)
; => "Zu groß!"
REPL> (goldlöckchen 12 10 20)
; => "Genau richtig!"
#+end_src
Immer wieder dieselben Werte für =smallest-ok= und =biggest-ok=
anzugeben, ist anstrengend.
Goldlöckchen hat /nicht/ ständig andere Gewohnheiten von Aufruf zu Aufruf.
Kann man keine Version von Goldlöckchen mit Gedächtnis entwickeln,
damit wir =smallest-ok= und =biggest-ok= nur einmal angeben müssen und
trotzdem mehrere Werte von =n= testen?
Doch, natürlich kann man. /Closures/ (deutsch Abschlüsse) sind die Lösung!
#+begin_src scheme
(define (konstr-goldlöckchen smallest-ok biggest-ok)
(define (goldlöckchen n) ; wir haben smallest-ok und
(cond ; biggest-ok in eine äußere
((< n smallest-ok) ; Prozedur eingepackt, damit
"Zu klein!") ; nur das n noch als Argument
((> n biggest-ok) ; übergeben werden muss
"Zu groß!")
(else
"Genau richtig!")))
goldlöckchen) ; Ergebnis ist goldlöckchen
#+end_src
Damit rufen wir =konstr-goldlöckchen= auf und es liefert uns die
eingeschlossene =goldlöckchen=-Prozedur.
#+begin_src scheme
REPL> (konstr-goldlöckchen 10 30)
; => #
#+end_src
Jetzt genügt es, das innere =goldlöckchen= wiederholt aufzurufen.
#+begin_src scheme
REPL> (define goldi
(konstr-goldlöckchen 10 30))
REPL> (goldi 7)
; => "Zu klein!"
REPL> (goldi 256)
; => "Zu groß!"
REPL> (goldi 22)
; => "Genau richtig!"
#+end_src
Die äußere Prozedur bildet einen Abschluss um die innere Prozedur und
gewährt ihr den Zugriff (und Gedächtnis) auf =smallest-ok= und
=biggest-ok=.
Wir merken an, dass in /Goblins/ nach demselben Muster Konstruktoren
für die Objekte geschrieben werden: Die äußere Prozedur ist der
Konstruktor, die innere Prozedur das Verhalten des Objekts.
(Der Unterschied liegt in erster Linie darin, dass /Goblins/-Objekte,
die man mit =spawn= anlegt, eine Capability /bcom/ erhalten, mit der
sie ihr Verhalten ändern können!)
Es ist schön, wie wir unsere eigene Implementierung von cons-Zellen
aus reiner Abstraktion auf dieselbe Weise schreiben können.
#+begin_src scheme
REPL> (define (abstract-cons car-data cdr-data)
(lambda (method)
(cond
((eq? method 'car)
car-data)
((eq? method 'cdr)
cdr-data)
(else (error "Unbekannte Methode:" method)))))
REPL> (define our-cons (abstract-cons 'foo 'bar))
REPL> (our-cons 'car)
; => foo
REPL> (our-cons 'cdr)
; => bar
#+end_src
Das meinen wir, wenn wir Abschlüsse als aus dem Programmfluss selbst
bestehende Datenstrukturen beschreiben.
Abgeschlossenheit ergibt sich aus der /lexikalischen Bindung/ (Lexical
Scope).
Wir machen uns diese Eigenschaft in Goblins zunutze: Die Capabilitys,
die ein Objekt hat, sind einfach die Capabilitys im Geltungsbereich
seines Verhaltens.
* Iteration und Rekursion
Beim Programmieren geht es oft um Abfolgen von Operationen, besonders
im Zusammenhang mit Datenstrukturen, die andere Informationen
enthalten.
Von besonderem Nutzen in der funktionalen Programmierung ist die
Prozedur =map=. Mit ihr wendet man auf jedes Element einer Folge die
Prozedur im ersten Argument an.
Ein Beispiel: =string-length= gibt uns die Anzahl von Zeichen, die in
einer Zeichenkette enthalten sind:
#+begin_src scheme
REPL> (string-length "Katze")
; => 5
REPL> (string-length "Gorilla")
; => 7
#+end_src
Mit =map= können wir leicht eine Liste konstruieren, die zu jeder
Zeichenkette ihre Länge angibt:
#+begin_src scheme
REPL> (map string-length '("Katze" "Hund" "Gorilla" "Salamander"))
; => (5 4 7 10)
#+end_src
Wir können auch eine selbstdefinierte Prozedur angeben:
#+begin_src scheme
REPL> (define (symbol-length sym)
(string-length (symbol->string sym)))
REPL> (map symbol-length '(Basilikum Oregano Petersilie Thymian))
; => (9 7 10 7)
#+end_src
Wir müssen der Prozedur eigentlich keinen Namen geben … denn mit
=lambda= konstruieren wir ja eine anonyme Prozedur und so eine
übergeben wir direkt an =map=:
#+begin_src scheme
REPL> (map (lambda (str)
(string-append "Ich liebe "
(string-upcase str)
" einfach!!!"))
'("Erdbeeren" "Bananen" "Trauben"))
; => ("Ich liebe ERDBEEREN einfach!!!"
; "Ich liebe BANANEN einfach!!!"
; "Ich liebe TRAUBEN einfach!!!")
#+end_src
Bei =map= fällt ein bisschen Mehrarbeit an: Mit jedem Mal wird eine
Liste von Ergebnissen aufgebaut.
Was aber, wenn wir unsere Liebe zu Lebensmitteln einfach nur mit
=display= auf dem Bildschirm anzeigen wollten und kein Interesse daran
hätten, die Daten weiterzuverarbeiten?
Die Antwort lautet =for-each=, das denselben Aufbau hat wie =map=,
jedoch kein Ergebnis aufbaut:
#+begin_src scheme
REPL> (for-each (lambda (str)
(display
(string-append "Ich liebe "
(string-upcase str)
" einfach!!!\n")))
'("Eiscreme" "Toffee" "Kekse"))
; zeigt an:
; Ich liebe EISCREME einfach!!!
; Ich liebe TOFFEE einfach!!!
; Ich liebe KEKSE einfach!!!
#+end_src
#+begin_center
*ANMERKUNG!* Der folgende Text in diesem Unterabschnitt und
eigentlich der gesamten verbleibenden Scheme-Anleitung geht über
alle Erfordernisse hinaus, um den Hauptteil unseres Papers „The
Heart of Spritely“ zu verstehen! Dennoch trägt er merklich zum
Scheme-Verständnis eines neuen Lesers bei.
#+end_center
Eine überraschende Eigenschaft von Scheme ist, dass Iteration in Form
von Rekursion definiert ist!
Was wir damit meinen:
Wir können unsere eigene Fassung von =for-each= definieren:
#+begin_src scheme
(define (for-each proc lst)
(if (eq? lst '()) ; Liste zu Ende?
'fertig ; Wir sind fertig und liefern das als Ergebnis
(let ((item (car lst))) ; Ansonsten holen wir uns noch ein Item
(proc item) ; Rufen die Prozedur auf Item auf
(for-each proc (cdr lst))))) ; Und iterieren auf dem Rest
#+end_src
Damit rufen wir =proc= nacheinander auf jedem Item auf, bis kein Item
mehr übrig ist.
Wenn Sie sich schon mit anderen Programmiersprachen auskennen, könnten
Sie denken, mit diesem Programmentwurf bekämen wir unter Umständen
einen ungewollten Stacküberlauf.
Aber Scheme ist eine intelligente Sprache: Es kriegt mit, wenn wir die
Version in =for-each= nicht mehr brauchen, nachdem wir in der letzten
Zeile angekommen sind. Anders ausgedrückt, wenn =for-each= sich
selbst endrekursiv aufruft.
Daher braucht Scheme gar nicht erst einen neuen Rahmen auf dem Stack
anzulegen, sondern „springt“ wieder zurück an den Anfang von
=for-each= mit den neuen Variablen alloziert im Speicher.
Das nennt sich /Endrekursionsbeseitigung/ und tatsächlich sind in
Scheme alle Iterationskonstrukte rekursiv implementiert.
Es ist ebenso möglich, baumrekursive Prozeduren zu entwickeln.
Folgende Prozedur (die durchaus etwas für Fortgeschrittene ist;
einfach Weiterlesen ist in Ordnung) baut einen Binärbaum:
#+begin_src scheme
(define (build-tree depth)
(if (= depth 0)
'(0)
(list depth
(build-tree (- depth 1))
(build-tree (- depth 1)))))
#+end_src
#+begin_src scheme
REPL> (build-tree 3)
; => (3 (2 (1 (0)
; (0))
; (1 (0)
; (0)))
; (2 (1 (0)
; (0))
; (1 (0)
; (0))))
#+end_src
Oder besser visualisiert:
#+begin_src text
3
/ \
/ \
2 2
/ \ / \
1 1 1 1
/\ /\ /\ /\
0 0 0 0 0 0 0 0
#+end_src
Anders als =for-each= ruft =build-tree= sich selbst /nicht/ nur
endständig auf.
Es gibt keine Möglichkeit, einfach an den Anfang der Prozedur zu
„springen“, ohne sich auf dem Stack zu notieren, was noch zu tun ist,
wenn der Code so geschrieben ist: Wir schieben Arbeit auf später auf
und =list= muss auf deren Ergebnis warten.
=build-tree= ist /nicht/ iterativ wie =for-each=, obwohl es rekursiv
ist.
Es ist Zeit, über einfachere Schreibweisen nachzudenken.
Hier zeigen wir zwei Varianten von =let=, die für sowohl rekursive als
auch iterative Prozeduren taugen.
Zunächst einmal =letrec=. Damit können sich im =letrec= angegebene
Prozeduren selbst oder gegenseitig aufrufen oder aufeinander beziehen,
auch wenn die Prozeduren erst weiter unten in der Reihenfolge stehen:
#+begin_src scheme
REPL> (letrec ((alice
(lambda (first?)
(report-status "Alice" first?)
(if first? (bob #f))))
(bob
(lambda (first?)
(report-status "Bob" first?)
(if first? (alice #f))))
(report-status
(lambda (name first?)
(display
(string-append name " ist "
(if first?
"Nummer eins"
"Nummer zwei")
"!\n")))))
(alice #t)
(display "-----\n")
(bob #t))
; zeigt an:
; Alice ist Nummer eins!
; Bob ist Nummer zwei!
; -----
; Bob ist Nummer eins!
; Alice ist Nummer zwei!
#+end_src
Die andere hilfreiche Abstraktion ist das /Let mit Namen/, auch eine
Variant von =let=, worin als allererstes Argument ein Bezeichner für
den Schleifendurchlauf steht:
#+begin_src scheme
REPL> (let loop ((words '("Karotte" "Kartoffel" "Erbse" "Sellerie"))
(num-words 0)
(num-chars 0))
(if (eq? words '())
(format #f "Das waren ~a Wörter aus ~a Zeichen!"
num-words num-chars)
(loop (cdr words)
(+ num-words 1)
(+ num-chars (string-length (car words))))))
; => "Das waren 4 Wörter aus 29 Zeichen!"
#+end_src
Die Wirkungsweise eines /Let mit Namen/ wird über die benannte
Prozedur definiert (hier bekommt sie den Namen =loop=) und sie wird
dadurch sofort aufgerufen. Anfänglich sind die Variablen wie im =let=
gebunden.
Später kann die Prozedur im Rumpf des =let= aufgerufen werden, wodurch
sich rekursive (vielleicht iterative) Aufrufe leicht schreiben lassen.
* Veränderungen, Zuweisungen und andere Nebenwirkungen
Dieser Abschnitt steht hier der Vollständigkeit halber.
Es sei gesagt, dass Goblins vieles hiervon anders angeht. Den anderen
Ansatz diskutieren wir zum Schluss.
Von Haus aus gibt es in Scheme die Möglichkeit, den aktuellen Wert
einer Variablen neu zuzuweisen, mit =set!=:
#+begin_src scheme
REPL> (define truhe 'schwert)
REPL> truhe
; => schwert
REPL> (set! truhe 'gold)
REPL> truhe
; => gold
#+end_src
Dies lässt sich kombinieren mit den Techniken aus [[#scheme-closure][Closures
(Abschlüsse)]].
Zum Beispiel zählt dieses Objekt von einer Startzahl =n= an herunter
bis null und ab da wird nur noch null zurückgeliefert.
#+begin_src scheme
REPL> (define (make-countdown n)
(lambda ()
(define last-n n)
(if (zero? n)
0
(begin
(set! n (- n 1))
last-n))))
REPL> (define cdown (make-countdown 3))
REPL> (cdown)
; => 3
REPL> (cdown)
; => 2
REPL> (cdown)
; => 1
REPL> (cdown)
; => 0
REPL> (cdown)
; => 0
#+end_src
Mehrere interessante Dinge gibt es in diesem Beispiel:
- Wir haben Zeit und Veränderung in unsere Berechnungen gebracht.
Vor der Einführung von Nebenwirkungen, wie Zuweisungen, hatte jeder
Aufruf einer Prozedur mit denselben Argumenten immer auch dasselbe
Ergebnis gehabt. Aber wie das Beispiel zeigt, ändert sich die
Antwort von =cdown= mit der Zeit (sogar ohne dass sie Argumente
übergeben bekommt).
- Weil wir beim ersten Aufruf der Prozedur mit der Startzahl loslegen
möchten, ist es notwendig, zuerst =last-n= zu behalten, /bevor/ wir
mit =set!= den Wert von =n= verändern. Wenn wir aus Versehen die
Reihenfolge tauschen, hätten wir unbeabsichtigt einen Bug
geschaffen und =cdown= hätte im Beispiel ab =2= gezählt statt ab
=3=.
- Außerdem kommt eine interessante neue Syntax vor: =begin=. Mit
=begin= werden mehrere Ausdrücke in Folge ausgeführt und der Wert
des letzten zurückgeliefert.
Letzteres ist wissenswert.
Als wir noch keine Wirkungen hatten (wie die Zuweisung dort oben,
Anzeigen auf dem Bildschirm, Protokollierung in eine Datei oder
Datenbank und so weiter), da gab es nie einen Grund für =begin=.
Um das zu verstehen, müssen Sie sich an die /Substitutionsmethode/ vom
Anfang dieses Tutorials zurückerinnern:
#+begin_src scheme
(* (- 8 (/ 30 5)) 21) ; Anfangsausdruck
(* (- 8 6) 21) ; vereinfachen: (/ 30 5) => 6
(* 2 21) ; vereinfachen: (- 8 6) => 2
42 ; vereinfachen: (* 2 21) => 42
#+end_src
Vor Wirkungen hatte jede aufgerufene Prozedur einen neuen Teil des
Programms berechnen sollen.
Aber weil jeder Zweig des =if= nur einen einzigen Ausdruck auswertet,
müssen wir irgendwie die Klausel mit der /Alternative/ sequenzieren,
dass sie sowohl =set!= durchführt als auch einen Wert
zurückliefert.[fn:cond-begin]
Mit anderen Worten ist der Zweck eines /rein funktionalen Programms/
wirklich nur, gegeben eine Reihe von Eingaben stets einen Wert zu
berechnen, denselben Wert, jedes Mal.
Es ist eine saubere Reihung von Substitutionen von Anfang bis Ende
der Auswertung.
(Mit anderen Worten bekamen wir vor der Einführung von Zeit gänzlich
/deterministische/ Programme.)
Mit der Einführung von /Veränderungen/ (Mutation) und /Nebenwirkungen/
(Side effects) kam ein mächtiges, aber gefährliches, neues Konstrukt
in unser Programm: die Zeit.
Wir können nicht länger sagen, unsere Programme seien /rein
funktional/, denn durch die Zeit sind sie /imperativ/ geworden: tu
dies, dann tu das.
Zeit ist Veränderung und bei Veränderungen spielt eine Rolle, in
welche Reihenfolge die Ereignisse sequenziert sind; Substitutionen
alleine genügen nicht.
Und durch die Zeit werden auch dieselben Programme und Prozeduren,
wenn man sie mit denselben Eingaben aufruft, unterschiedliche Ausgaben
zur Folge haben können.
Wir haben eine zeitlose Welt gegen eine mit Veränderung getauscht.
Trotz aller Mahnung kann Veränderung auch wünschenswert sein.
Wir leben in einer Welt mit Zeit und Wandel, meistens genau wie unsere
Programme.
Die Namensgebung in Scheme berücksichtigt (zumindest in der Regel)
Zeit und Veränderlichkeit durch Anhängen von =!= als Suffix, wie wir
bei =set!= gesehen haben.
Man kann das =!= als eine Art Warnung sehen, wie wenn der Nutzer
schreiend auf eine mögliche Veränderlichkeit hinweist.
(Allerdings werden durch Schemes Laufzeitumgebung keine Garantien
gemacht, dass hinter dem Vorhandensein oder Fehlen dieses Suffixes
überhaupt etwas von Veränderlichkeit steckt.)
=set!= ist nicht die einzige Form von Veränderung und Mutation im
standardisierten Scheme (oder in über den Standard hinausgehenden
Schemes).[fn:set-vs-w7-cells] Ein anderes Beispiel bilden
veränderliche Vektoren und =vector-set!=:
#+begin_src scheme
REPL> (define vec (vector 'a 'b 'c))
REPL> vec
; => #(a b c)
REPL> (vector-ref vec 1)
; => b
REPL> (vector-set! vec 1 'boop)
REPL> (vector-ref vec 1)
; => boop
REPL> vec
; => #(a boop c)
#+end_src
Die beiden Beispiele machen den Eindruck von Mutation.
Aber im Lauf dieser Anleitung haben wir bereits eine andere Art
Nebeneffekt kennengelernt, nämlich =display=, das auf den Bildschirm
schreibt.
Genauer gesagt basiert =display= selbst auf dem Konzept von /Ports/.
Ports sind Mechanismen in Scheme, um von Eingabegeräten zu lesen oder
auf Ausgabegeräte zu schreiben.
Sie alle stellen uns vor dieselben Schwierigkeiten wie bei =set!=.
Einfach ausgedrückt werden, wenn wir die umgebende Zeit in unsere
Programme einbringen, unsere Programm zeitabhängiger.
Doch es stellt sich als vergeblich heraus, /alle/ Zeit oder
Veränderung von unseren Rechnern verbannen zu wollen, wie es diese
ineinandergelegten Zitate hier darstellen:
#+begin_quote
Wie Simon Peyton Jones, ein bekannter funktionaler Programmierer,
gerne anmerkt: „Alles, was man noch tun kann, wenn es keine
Nebenwirkungen gibt, ist einen Knopf drücken und zuschauen, wie der
Kasten eine Zeit lang warm wird.“ (Auch das stimmt nicht ganz, denn
auch das Warmwerden des Rechners ist eine Nebenwirkung.)
--- Aus /Land of Lisp/ von Conrad Barski, M.D.
#+end_quote
Worauf Simon und Conrad hinweisen, ist das Dilemma funktionaler
Programmierung, dass Nebenwirkungen gefährlich sein können und sich
der Nutzer trotzdem eigentlich nur für Nebenwirkunngen interessiert.
Irgendwo müssen, damit der Rechner einen Zweck erfüllt, Eingaben vom
Nutzer eingelesen und Ausgaben zurückgegeben werden, was von sich aus
Nebenwirkungen hat.
Und auch wenn man mit der vom geschäftigen Rechner abgestrahlten Hitze
sein Haus wärmt, ist das eine Nebenwirkung.
Es führt kein Weg daran vorbei, zwischen dem Reich rein mathematischer
Utopie und der echten Welt hin- und herzuwechseln.[fn:monads-etc]
[fn:cond-begin] Es sei erwähnt, dass auch in =cond= mehrere Ausdrücke
im == / == stehen dürfen, doch können wir
das so auslegen, als ob im Innern der Implementierung von =cond= ein
=begin= stünde.
[fn:set-vs-w7-cells] Dass =set!= in vielem quasi eingebaut ist,
schafft Probleme in Scheme-Programmen, die sich darin einschränken
sollen, was sie anrichten können. Interessierte Leser sollten sich
den alternativen Ansatz aus Schemes Variante /W7/ in [[http://mumble.net/~jar/pubs/secureos/secureos.html][A Security Kernel
Based on the Lambda Calculus]] anschauen.
[fn:monads-etc] Funktionale Programmierer fangen Nebenwirkungen
manchmal in einem cleveren Trick: /Monaden/.
Monaden gehören nicht zum Stoff dieser Anleitung, aber man kann sie
sich als clevere, explizite Form des Umgangs mit Zeit vorstellen,
indem man ein Zustandsbündel durch ein ansonsten zustandsloses
Programm fädelt: Zeit existiert, doch sie bleibt deterministisch und
der Programmierer wird Herr der Zeit.
Der Nachteil ist, dass sich Programmierer damit herumschlagen müssen.
Goblins verfolgt einen anderen Ansatz, den wir in [[https://spritely.institute/static/papers/spritely-core.html#turns-are-cheap-transactions][Turns are cheap
transactions]] und [[https://spritely.institute/static/papers/spritely-core.html#time-travel-distributed-debugging][Time-travel distributed debugging]] erörtern.
Indem wir die Natur der Zeit in Zügen erfassen, kann der Programmierer
durch die Zeit reisen.
Durch /Sicherheit auf der Sprachebene/, auf die wir in [[https://spritely.institute/static/papers/spritely-core.html#application-library-safety][Application
safety, library safety, and beyond]] eingehen, kann eine gänzlich
deterministische und isolierte Ausführung garantiert werden.
All das ist möglich, indem wir die Details im Umgang mit Veränderung
abstrahieren, so dass der Nutzer nicht darüber nachdenken muss; der
Kern von Goblins macht das schon für den Nutzer.
Dazu mehr in einem späteren Paper.
* Über die Erweiterbarkeit von Scheme (und Lisp generell)
Sagen wir, wir hätten gerne neue Syntax.
Vielleicht wollen wir, zum Beispiel, falls eine Bedingung erfüllt ist,
mehrere Code-Stückchen sequenziert nacheinander ausführen.
Wir könnten schreiben:
#+begin_src scheme
(if (unser-test)
(begin
(mach-die-sache-1)
(mach-die-sache-2)))
#+end_src
Doch das sieht nicht schön aus.
Wie wäre es, wenn wir neue Syntax just für diesen Zweck erfänden?
#+begin_src scheme
(when (unser-test)
(mach-die-sache-1)
(mach-die-sache-2))
#+end_src
=when= lässt sich nicht als eine Funktion realisieren, weil wir
=(mach-die-sache-1)= und =(mach-die-sache-2)= nur dann ausführen
möchten, wenn =(unser-test)= erfolgreich war.
Wir brauchen neue Syntax.
Könnten wir die neue Syntax selbst schreiben?
Wenn wir uns zurückerinnern, dass man „Lisp in Lisp schreiben“ kann,
scheint es, als laute die Antwort ja:
#+begin_src scheme
REPL> (define (when test . rumpf)
`(if ,test
,(cons 'begin rumpf)))
REPL> (when '(unser-test)
'(mach-die-sache-1)
'(mach-die-sache-2))
; => (if (unser-test)
; (begin
; (mach-die-sache-1)
; (mach-die-sache-2)))
#+end_src
Tatsächlich wird die richtige Syntax geschrieben!
Und es zeigt, dass man mit Lisp tatsächlich „Code schreiben kann, der
Code schreibt“.[fn:homoiconic]
Allerdings zeigen sich auch zwei offensichtliche Probleme mit diesem
ersten Versuch:
- Wir mussten jedes an =build-when= übergebene Argument quotieren.
Das nervt.
- =build-when= führt seinen Code gar nicht aus, sondern liefert nur
die /quotierte Struktur/ als Ergebnis zurück, zu der der Code
entfaltet werden soll.
Aber wenn wir unsere Prozedur nur ein klein wenig anpassen, können wir
unsere Prozedur zu einem /Makro/ machen: einer besonderen Art von
Prozedur, mit der der Compiler Code umschreibt.
Alles, was wir tun müssen, ist:
#+begin_src scheme
(define-macro (when test . rumpf)
`(if ,test
,(cons 'begin rumpf)))
#+end_src
Was wir tun mussten, war nur, =define= zu =define-macro= umzubennen!
Jetzt weiß Scheme, dass damit Code umgeschrieben werden soll.
Damit können wir neue Arten von Syntax-Formen definieren.
=define-macro= zeigt sehr klar auf, was Makros in Lisp und Scheme
tun: Sie bearbeiten Struktur.
Auf diese Weise eine Listenstruktur selbst aufzubauen, ist, wie man in
Common Lisp Makros schreibt.
Aber in Scheme schreibt man Makros im Allgemeinen auf andere Weise.
Scheme-Makros sehen jedoch sehr ähnlich aus:
#+begin_src scheme
(define-syntax-rule (when test rumpf ...)
(if test
(begin rumpf ...)))
#+end_src
=define-syntax-rule= benutzt /Pattern Matching/ (deutsch
Mustervergleich), um Makros zu implementieren. Das erste Argument an
=define-syntax-rule= beschreibt das Muster, das vom Nutzer angegeben
wird, dann beschreibt das zweite Argument die Schablone, wie es
umgeschrieben wird.[fn:other-macro-builders]
Wir sehen, dass =rumpf ...= sowohl im Muster als auch in der Schablone
steht; die Auslassungspunkte =...= im Muster stehen für mehrere
Ausdrücke, die aus der Eingabe des Nutzers genommen werden, und in der
Schablone zeigt das =...=, wo sie wiederholt werden sollen.
Es zeigt sich, dass es für diesen Mechanismus unnötig ist, selbst zu
quotieren; Scheme ist schlau und kümmert sich für uns darum.
Zusammengefasst ist bei der Scheme-Version der Syntax-Definitionen
weniger offensichtlich, wie sie intern abläuft, im Vergleich zur
Version mit =define-macro=.
Andererseits taucht beim Umschreiben von Syntax öfters ein Problem
mit etwas auf, dass sich /Hygiene/ nennt:
Wir erwarten, dass den Rumpf nach dem Umschreiben einer Syntax-Form /
Makro nicht temporäre Bezeichner von außen stören.
Auf die Debatte gehen wir im Primer nicht ein, aber die Makros von
Common Lisp und Scheme haben beide merkliche Nachteile, wobei es in
Scheme viel wahrscheinlicher ist, dass sich Makros „hygienisch“
korrekt verhalten. Es ist leicht, simple Syntax-Formen zu schreiben,
aber wenn sie kompliziert werden, wird es schwerer, sie zu schreiben
und durchzublicken, wie sie intern funktionieren.
Das ist der Grund, warum wir, obwohl Sie den =define-macro=-Ansatz in
Scheme wahrscheinlich niemals benutzen werden, diesen zeigen, damit
Sie die Idee hinter „Code, der Code schreibt“ verstehen.
Nachdem wir so weit gekommen sind, dass wir wissen, wie man neue
Syntax auf Scheme-Art erzeugt, wollen wir mal sehen, ob wir uns jetzt
das Leben leichter machen können.
Schauen wir uns noch einmal an, wie wir =for-each= zuvor benutzt
haben:
#+begin_src scheme
REPL> (for-each (lambda (str)
(display
(string-append "Ich liebe "
(string-upcase str)
" einfach!!!\n")))
'("Erdbeeren" "Bananen" "Trauben"))
; zeigt an:
; Ich liebe ERDBEEREN einfach!!!
; Ich liebe BANANEN einfach!!!
; Ich liebe TRAUBEN einfach!!!
#+end_src
Das klappt, aber es ist umständlicher als nötig.
Dass wir =lambda= hinschreiben müssen, ist ein unnötiges Detail!
Eine kleine neue Syntaxdefinition macht den Code sauber:
#+begin_src scheme
(define-syntax-rule (for (item lst) body ...)
(for-each (lambda (item)
body ...)
lst))
#+end_src
Schauen wir mal:
#+begin_src scheme
REPL> (for (str '("Erdbeeren" "Bananen" "Trauben"))
(display
(string-append "Ich liebe "
(string-upcase str)
" einfach!!!\n")))
; zeigt an:
; Ich liebe ERDBEEREN einfach!!!
; Ich liebe BANANEN einfach!!!
; Ich liebe TRAUBEN einfach!!!
#+end_src
Es klappt! Das ist viel lesbarer.[fn:c-style-for]
Wir müssen hier nicht aufhören. Spritely Goblins enthält eine Technik
namens =methods= und wir zeigen sie als ein Beispiel für ein Makro.
Diese Version ist ein wenig vereinfacht:
#+begin_src scheme
(define-syntax-rule (methods ((method-id method-args ...)
body ...) ...)
(lambda (method . args)
(letrec ((method-id
(lambda (method-args ...)
body ...)) ...)
(cond
((eq? method (quote method-id))
(apply method-id args)) ...
(else
(error "No such method:" method))))))
#+end_src
Wir können daran sowohl sehen, wie viel Beispiele für Mustervergleich
im Stil von Scheme ausdrücken können, als auch, wie verwirrend mehrere
Schichten mit Auslassungspunkten (also =...=) sein können. Es stellt
eine kleine Herausforderung dar, zu erkennen, wie das
Code-Umschreibesystem damit umgeht, alles zu entpacken.
Aber denken wir später weiter nach und sehen uns erst ein Beispiel für
die Nutzung an:
#+begin_src scheme
REPL> (define (make-enemy name hp)
(methods
((get-name)
name)
((damage-me weapon hp-lost)
(cond
((dead?)
(format #t "~a ist leider bereits tot!\n" name))
(else
(set! hp (- hp hp-lost))
(format #t "Du greifst ~a an und machst ~a Schaden!\n"
name hp-lost))))
((dead?)
(<= hp 0))))
REPL> (define hobgob
(make-enemy "Hobgoblin" 25))
REPL> (hobgob 'get-name)
; => "Hobgoblin"
REPL> (hobgob 'dead?)
; => #f
REPL> (hobgob 'damage-me "Keule" 10)
; zeigt an: Du greifst Hobgoblin an und machst 10 Schaden!
REPL> (hobgob 'damage-me "Schwert" 20)
; zeigt an: Du greifst Hobgoblin an und machst 20 Schaden!
REPL> (hobgob 'damage-me "Sellerie" 2)
; zeigt an: Hobgoblin ist leider bereits tot!
REPL> (hobgob 'dead?)
; => #t
#+end_src
Da geht noch mehr.
Wir können Scheme um [[http://minikanren.org/][Logikprogrammierung]] erweitern oder [[https://wingolog.org/archives/2011/08/30/the-gnu-extension-language][um einen
eigenen Mustervergleich]], etc etc etc.
Tatsächlich machen wir gleich im nächsten Unterabschnitt Gebrauch von
dem Mustervergleich, der in Guiles Standardbibliothek mit dabei ist.
Weil die Syntax von Lisp/Scheme erweiterbar ist, können wir
Funktionalitäten, die Programmiersprachen auszeichnen, nachrüsten,
indem wir sie als Bibliothek implementieren, statt dass wir getrennte
neue Untersprachen bräuchten.
Mehrere Problembereiche können in ein System zusammengefasst werden.
Deswegen sagt man, die Sprachen aus der Lisp-Sprachfamilie bieten
Unterstützung für /composable domain specific languages/, also
domänenspezifische Sprachen, die miteinander verknüpft werden können.
Man gewinnt an Freiheit.
In anderen Programmiersprachen sind die Nutzer gezwungen, am Altar der
Implementierer ihrer Programmiersprache zu flehen, sie mögen bestimmte
Funktionalitäten in die nächste offizielle Veröffentlichung der
Sprache aufnehmen, während die Funktionalitäten in den Händen eines
Lispers / Schemers mit nur wenigen Zeilen hinzufügbar sind.
Darin steckt wahre Macht.
Aber das war nicht alles.
Im nächsten Abschnitt werden wir Scheme an sich freilegen, so dass wir
selbst die zugrunde liegenden Mechanismen konfigurieren und mit ihnen
experimentieren können, wofür man überraschend wenig Code schreiben
muss.
[fn:homoiconic] Weil Lisps /abstrakter Syntaxbaum/ aus denselben
Datenstrukturen gemacht ist, wie sie auch die Anwender zum
Programmieren benutzen, und weil der Syntaxbaum ebenbildlisch so
aussieht wie die /Oberflächensyntax/ der Sprache, nennen wir sie
/homoikonisch/.
/Homoikonizität/ ist eine Eigenschaft, die Lisp von anderen
unterscheidet, und der Grund für einen Großteil von Lisps
Erweiterbarkeit.
[fn:other-macro-builders] Tatsächlich ist selbst =define-syntax-rule=
Zucker. Folgendes ist äquivalent:
#+begin_src scheme
(define-syntax-rule (when test body ...)
(if test
(begin body ...)))
(define-syntax when
(syntax-rules ()
((when test body ...)
(if test
(begin body ...)))))
#+end_src
Auch lässt sich =define-syntax-rule= mittels =define-syntax= und
=syntax-rules= schreiben:
#+begin_src scheme
(define-syntax define-syntax-rule
(syntax-rules ()
((define-syntax-rule (id pattern ...) template)
(define-syntax id
(syntax-rules ()
((id pattern ...)
template))))))
#+end_src
Es gibt einen ganzen Zoo anderer Syntax-Formen zum Umschreiben von
Syntax in den meisten Scheme-Implementierungen und meist unterscheiden
sie sich von Scheme zu Scheme, allerdings gehören =define-syntax= und
=syntax-rules= zum Scheme-Standard.
[fn:c-style-for] Eine spaßige Übung für unsere Leser: Versuchen Sie,
eine for-Schleife wie in C zu schreiben!
Hier ist C-Code:
#+begin_src c
// C-Version von for:
// for ( Init; Bedingung; Inkrement ) {
// Anweisung(en);
// }
for (i = 0; i < 10; i = i + 2) {
printf("i ist: %d\n", i);
}
#+end_src
Versuchen Sie, Folgendes zum Funktionieren zu bringen:
#+begin_src scheme
(for ((i 0) (< i 10) (+ i 2))
(display (string-append "i ist: " (number->string i) "\n")))
#+end_src
* Scheme in Scheme
Wir präsentieren eine lauffähige Implementierung von Scheme,
geschrieben in Scheme:
#+begin_src scheme
(use-modules (ice-9 match))
(define (env-lookup env name)
(match (assoc name env)
((_key . val)
val)
(_
(error "Variable nicht gebunden:" name))))
(define (extend-env env names vals)
(if (eq? names '())
env
(cons (cons (car names) (car vals))
(extend-env env (cdr names) (cdr vals)))))
(define (evaluate expr env)
(match expr
;; Unterstützung für die eingebauten Datentypen
((or #t #f (? number?))
expr)
;; Quotierung
(('quote quoted-expr)
quoted-expr)
;; Variable nachschlagen
((? symbol? name)
(env-lookup env name))
;; Bedingungen
(('if test consequent alternate)
(if (evaluate test env)
(evaluate consequent env)
(evaluate alternate env)))
;; Lambdas (Prozeduren)
(('lambda (args ...) body)
(lambda (. vals)
(evaluate body (extend-env env args vals))))
;; Prozeduraufrufe (-anwendungen)
((proc-expr arg-exprs ...)
(apply (evaluate proc-expr env)
(map (lambda (arg-expr)
(evaluate arg-expr env))
arg-exprs)))))
#+end_src
Wenn wir Kommentare, leere Zeilen und den Import des
Mustervergleichers nicht zählen (der nicht unbedingt notwendig ist,
aber einfacher), macht das bloß 30 Code-Zeilen.
Obwohl dieser Evaluator nur das Notwendigste enthält, ist er doch
vollständig genug, als dass man damit alles Vorstellbare berechnen
kann.
(Und ja, man könnte auch noch so einen Scheme-Evaluator in diesem
Scheme-Evaluator schreiben!)[fn:metacircular]
Unser Evaluator nimmt zwei Argumente: einen Scheme-Ausdruck =expr=
und eine Umgebung („environment“) namens =env=.
Weil die Struktur von Scheme so lispig ist, können wir, wie wir
gelernt haben, ganze Code-Bereiche einfach quotieren.
(Und genau das machen wir auch.)
Die =env= im zweiten Argument ist eine assoziative Liste, die Symbole
als Namen auf die zugehörige Prozedur abbildet.
Sie sollten es mit eigenen Augen sehen.
Machen wir ein bisschen einfache Arithmetik. Dazu legen wir einige
Mathe-Prozeduren in der Standardumgebung ab:
#+begin_src scheme
(define math-env
`((+ . ,+)
(- . ,-)
(* . ,*)
(/ . ,/)))
#+end_src
Wir können beobachten, dass unser erstes Beispiel zur
„Substitutionsmethode“ in dieser Umgebung richtig funktioniert.
#+begin_src scheme
REPL> (evaluate '(* (- 8 (/ 30 5)) 21)
math-env)
; => 42
#+end_src
Das sieht aus wie dieselbe Antwort, die wir damals in unserem Programm
hatten!
Auch das Erschaffen eines Lambdas und dessen Anwendung klappt. Hier
ein Lambda, das eine Zahl quadriert:
#+begin_src scheme
REPL> (evaluate '((lambda (x)
(* x x))
4)
math-env)
; => 16
#+end_src
Wunderbar, es funktioniert.
Beschäftigen wir uns mit etwas Fortgeschrittenerem.
Wenn wir nur zwei Operatoren haben, =+= und ===, ist das genug, um die
Fibonacci-Folge auszurechnen:
#+begin_src scheme
REPL> (define fib-program
'((lambda (prog arg) ; "boot"
(prog prog arg))
(lambda (fib n) ; "main", das Hauptprogramm
(if (= n 0)
0
(if (= n 1)
1
(+ (fib fib (+ n -1))
(fib fib (+ n -2))))))
10)) ; Argument
REPL> (define fib-env
`((+ . ,+)
(= . ,=)))
REPL> (evaluate fib-program fib-env)
; => 55
#+end_src
Es sieht aus wie Magie. Aber es läuft! Der Evaluator führt die
zugrunde liegende Berechnung aus und eine einfache Additionsprozedur
(positiver und negativer Zahlen) und eine Prozedur zum Testen auf
numerische Gleichheit reichen dazu aus, und diese zwei Prozeduren
haben wir verfügbar gemacht.
Es ist notwendig, dass sich das Hauptprogramm selbst aufrufen kann.
Aus diesem Grund beginnen wir mit der ersten Prozedur (als =boot=
markiert), die ein Programm und ein Argument nimmt und diese Prozedur
auf sich selbst und dem Argument aufruft[fn:cheap-y]
Die zweite Prozedur (markiert als Hauptprogramm =main=) nimmt sich
selbst als Argument =fib= entgegen (übergeben von unserer
Boot-Prozedur) sowie =n= als weiteres Argument (auch übergeben von der
Boot-Prozedur) … und dann läuft es!
Unser Evaluator berechnet rekursiv die Fibonacci-Folge.
Und unser Evaluator ist gut verständlich.
Nehmen wir ihn in seine Einzelabschnitte auseinander.
#+begin_src scheme
(define (env-lookup env name)
(match (assoc name env)
((_key . val)
val)
(_
(error "Variable nicht gebunden:" name))))
#+end_src
Der Anfang ist leicht.
Wir definieren Umgebungen als assoziative Listen, deshalb sucht
=env-lookup= lediglich nach einem passenden Namen in der Liste.
Was neu hinzukommt, wird zuerst gefunden, und so wird eine erneute
Definition in einem Geltungsbereich tiefer im Programm die des äußeren
Geltungsbereichs /überschatten/.
Sehen wir uns das einmal an:
#+begin_src scheme
REPL> (env-lookup '((foo . neueres-foo)
(bar . bar)
(foo . älteres-foo))
'foo)
; => 'neueres-foo
#+end_src
Jetzt folgt ein Hilfsmittel:
#+begin_src scheme
(define (extend-env env names vals)
(if (eq? names '())
env
(cons (cons (car names) (car vals))
(extend-env env (cdr names) (cdr vals)))))
#+end_src
=extend-env= nimmt eine Umgebung und eine Liste von Namen sowie eine
parallel laufende Liste von Werten.
Das erleichtert uns später den Code, der Prozeduren definiert.
Probieren wir es aus, dann ist alles klar:
#+begin_src scheme
REPL> (extend-env '((foo . wert-von-foo))
'(bar quux)
'(wert-von-bar wert-von-quux))
; => ((bar . wert-von-bar)
; (quux . wert-von-quux)
; (foo . wert-von-foo))
#+end_src
Jetzt geht’s an den Evaluator. Die äußere Schale von =evaluate= ist
so aufgebaut:
#+begin_src scheme
(define (evaluate expr env)
(match expr
( ...) ...))
#+end_src
=evaluate= nimmt zwei Argumente an:
- =expr=: welcher Ausdruck ausgewertet werden soll
- =env=: in welcher Umgebung wir den Ausdruck auswerten
Im Rumpf von =evaluate= teilen wir eine andere Verhaltensweise zu, je
nachdem, auf welches Muster / Pattern der Ausdruck =expr= passt. Wir
benutzen =match= aus [[https://www.gnu.org/software/guile/manual/html_node/Pattern-Matching.html][Guiles Syntax für Pattern Matching]] (deren Modul
wir oben importiert haben).
Kurz gefasst, wenn ein == passt, hören wir auf, die
Muster zu durchsuchen, und es wird nur == ausgewertet (mit
den Bindungen aus ==, falls vorhanden).
Also sehen wir uns jetzt nach und nach jedes Muster an, das wir in
=evaluate= unterstützen.
Los geht’s mit etwas Einfachem:
#+begin_src scheme
;; Unterstützung für die eingebauten Datentypen
((or #t #f (? number?))
expr)
#+end_src
Das =or= bedeutet, irgendeines der enthaltenen Muster kann passen.
Die ersten zwei sind die Werte wahr und falsch aus Scheme.
Die Klammern mit dem Symbol =?= am Anfang zeigen an, dass ein Prädikat
passen muss, in diesem Fall =number?=.
Wenn eines davon passt, liefern wir genau denselben Ausdruck =expr=,
der passen muss, wieder zurück. Wir leihen uns also die Booleschen
und Zahlenwerte direkt von der zugrunde liegenden
Scheme-Implementierung aus.
Mit anderen Worten, dadurch funktioniert:
#+begin_src scheme
REPL> (evaluate #t '())
; => #t
REPL> (evaluate #f '())
; => #f
REPL> (evaluate 33 '())
; => 33
REPL> (evaluate -2/3 '())
; => -2/3
#+end_src
Das war leicht!
Noch ein Leichtes:
#+begin_src scheme
;; Quotierung
(('quote quoted-expr)
quoted-expr)
#+end_src
Erinnern Sie sich, dass ='foo= nur die Kurzform ist von =(quote foo)=,
genau wie ='(1 2 3)= die Kurzform ist von =(quote (1 2 3))=.
Mit diesem Muster kümmern wir uns um alle Listen, die losgehen mit dem
Symbol ='quote= und einem zweiten Element mit dem zu quotierenden
Ausdruck.
Anders gesagt, hierdurch funktioniert:
#+begin_src scheme
REPL> (evaluate ''foo '())
; => foo
REPL> (evaluate ''(1 2 3) '())
; => (1 2 3)
REPL> (evaluate (quote (quote (1 2 3))) '())
; => (1 2 3)
#+end_src
Die letzten beiden sind dasselbe. Beachten Sie, wir quotieren
zweimal: Einmal, weil wir das gesamte evaluierte Programm quotieren,
und einmal als Teil des quotierten Programms, in dem wir einen
Ausdruck quotieren möchten.
So weit, so gut. Das nächste ist wieder eher leicht:
#+begin_src scheme
;; Variable nachschlagen
((? symbol? name)
(env-lookup env name))
#+end_src
Der Teil mit =(? symbol? name)= bindet =name= an die passende Komponente.
(In diesem Fall wird =name= an denselben Wert gebunden wie dem in
=expr=, noch bevor wir matchten, aber =name= zu schreiben ist ein
wenig leserlicher.)
Der Rumpf, also der ==, ist recht einfach.
Was =env-lookup= tut, haben wir schon gesehen.
Mit anderen Worten, wenn wir ein Symbol sehen (das natürlich nicht
quotiert ist, sonst hätten wir uns darum gekümmert), dann schlagen wir
den entsprechenden Wert in der Umgebung =env= nach.
Das heißt, es funktioniert:
#+begin_src scheme
REPL> (evaluate 'x '((x . 33)))
; => 33
#+end_src
Es leistet aber auch, dass wir bei Lambda-Anwendungen definierte
Variable nachschlagen können:
#+begin_src scheme
REPL> (evaluate '((lambda (x) x) 33) '())
; => 33
#+end_src
Auch wenn wir noch gar nicht bei =lambda= angekommen sind!
Aber so gut wie.
Als Nächstes, Bedingungen, was sich als recht einfach herausstellt:
#+begin_src scheme
;; Bedingungen
(('if test consequent alternate)
(if (evaluate test env)
(evaluate consequent env)
(evaluate alternate env)))
#+end_src
Anders ausgedrückt passt eine Liste mit dem Symbol ='if= am Anfang und
nach dem ='if= drei Unterausdrücken, die während des match-Rumpfs an
die Variablen =test=, =consequent=, und =alternate= gebunden sind.
Wir benutzen das =if=, das uns das zugrunde liegende Scheme
bereitstellt, und werten zunächst =test= in der aktuellen Umgebung
=env= aus (das ist eine Rekursion!). Mithilfe des =if= vom
Wirts-Scheme wird entschieden und die Folgerung =consequent= oder die
Alternative =alternate= in der Umgebung =env= ausgewertet, wozu wieder
=evaluate= rekursiv aufgerufen wird.
Nun denn, jetzt zu den Prozeduren. Das ist ein bisschen
komplizierter, aber auch nicht zu sehr:
#+begin_src scheme
;; Lambdas (Prozeduren)
(('lambda (args ...) body)
(lambda (. vals)
(evaluate body (extend-env env args vals))))
#+end_src
Hier geht es im Muster um eine Liste mit ='lambda= am Anfang, als
zweitem Listenelement den Argumenten, und den Rumpf binden wir ans
englische Wort für Rumpf, =body=.
Dann liefern wir als Ergebnis eine Prozedur zurück, die beim
Evaluieren mit der richtigen Anzahl Argumente aufgerufen werden
kann.[fn:arity-mismatch]
Im Inneren der Prozedur, die wir zurückliefern, wird =evaluate=
rekursiv aufgerufen auf den Ausdruck =body= aus dem Lambda, das wir
hier mit match vorfinden, aber in einer neuen, erweiterten Umgebung,
wo auch die Namen in =args= gebunden sind an die Werte =vals= des
Prozeduraufrufs.[fn:lexical-scope-happens-here]
Anders gesagt, damit funktioniert:
#+begin_src scheme
REPL> ((evaluate '(lambda (x y) x) '())
'first 'second)
; => first
REPL> ((evaluate '(lambda (x y) y) '())
'first 'second)
; => second
#+end_src
Da bleibt nur noch eines … Prozeduren anzuwenden!
#+begin_src scheme
;; Prozeduraufrufe (-anwendungen)
((proc-expr arg-exprs ...)
(apply (evaluate proc-expr env)
(map (lambda (arg-expr)
(evaluate arg-expr env))
arg-exprs)))
#+end_src
Dieses Puzzlestück passt auf alle Arten von Prozeduranwendungen!
Wenn wir bis hierhin gekommen sind, passt das Muster auf jede Liste
mit ein oder mehr Listenargumenten, was auf eine Prozedur hinweist,
der Argumente übergeben werden.
Wir werten aus, was =proc-expr= ist, denn es ist die Prozedur, die wir
auswerten wollen, und dabei gelten die aktuellen Argumente, wozu wir
=evaluate= rekursiv mit der aktuellen Umgebung =env= aufrufen.
Wir sammeln zudem all die =arg-expr=-Argumentausdrücke ein, die an die
Prozedur übergeben werden, indem wir =evaluate= rekursiv auf jedes
aufrufen, mit der aktuellen Umgebung, =env=.
Anders gesagt, damit funktioniert:
#+begin_src scheme
REPL> (evaluate '(* (- 8 (/ 30 5)) 21)
math-env)
; => 42
#+end_src
Und wenn wir alles zusammensetzen, verleiht uns das nicht nur die
Macht, die Fibonacci-Folge zu berechnen, sondern auch jedes andere
berechenbare Problem, das wir und vorstellen können!
Um fair zu sein, haben wir uns dazu ein gutes Stück von Schemes Macht
ausgeliehen, aber auch nicht so viel, wie man denkt … besonders im
Vergleich zu dem, was sich viele anhand anderer Sprachen
implementierte Sprachen leihen (ganz, ganz besonders viel weniger als
sich beispielsweise Clojure von Java ausleiht oder sich so ziemlich
jede beliebte Sprache von der C-Standardbibliothek
leiht).[fn:lisp-read]
Auch ist unser Evaluator zu unvollständig, um irgendeinem der
Scheme-Standards zu genügen.
Da hinzukommen, wäre ohne allzu viel Mehraufwand möglich, und zur
Demonstration reicht er aus.
Zumal es /uns überlassen/ ist, wie mächtig wir die Sprache machen
wollen. Das hängt alles davon ab, in welcher Anfangsumgebung wir Code
evaluieren.
*Unsere Sprache kann zudem Sicherheit über Capabilitys bieten!*
Abgesehen davon, dass man in eine Endlosschleife laufen und übermäßig
Ressourcen wie Speicher oder Rechenleistung verbrauchen kann, ist es
unmöglich, etwas wirklich Schlimmes mit unserer Sprache anzurichten.
Dennoch ist es unsere Entscheidung, wie viel Macht wir ihr geben.
Wenn wir das wollen, können wir eine Umgebung mit veränderlichen
Zellen oder mit Dateisystemzugriff zur Verfügung stellen.
Das ist unsere Sache.
Und mit winzigen Anpassungen können wir unserem Evaluator
verschiedenste wunderbare Kunststücke beibringen.
Wir können neue Syntax einführen.
Wir können neue Syntax einführen, um neue Syntax einzuführen (Makros)!
Wir können die Auswertungsreihenfolge ändern, ein statisches Typsystem
einführen und noch viel mehr.
Unser Versprechen war, dass Sie am Ende dieser Anleitung Scheme
erlernt hätten.
Wenn Sie einmal hier angekommen sind, haben Sie weitaus mehr erreicht,
denn Sie sind nicht bloß Benutzer von Scheme, sondern ein Gestalter
von Scheme.
Die Macht gehört Ihnen![fn:more-reading]
[fn:metacircular] Die Idee, eine Sprache aufbauend auf einer ähnlichen
Wirtssprache zu implementieren, nennt sich „metazirkulärer Evaluator“.
Forscher auf dem Gebiet der Informatik mögen sie, denn damit lassen
sich Varianten im Entwurf von Programmiersprachen erkunden, ohne dass
man das gesamte System neu erfinden müsste.
[fn:cheap-y] Zu Demonstrationszwecken haben wir ein kompliziertes
Beispiel genommen, wo wir auf ein Problem stoßen: Fibonacci, wie wir
es implementiert haben, ist selbstrekursiv!
Aber selbstrekursive Funktionen kriegen wir sonst nur mit Techniken
wie =letrec= hin, das wir nicht bereitgestellt haben.
Als Ausweg übergeben wir an das Hauptprogramm (die zweite Prozedur)
sie selbst und benutzen dazu die erste Prozedur.
Deshalb wird die erste Prozedur im Kommentar auch "boot" genannt.
Es handelt sich um eine Art eingeschränkten Y-Kombinator, einer
Bootstrapping-Technik. (Wir meinen nicht das Unternehmen, das
Unternehmen gründet, aber da kommt dessen Name her.) Mit dem
Y-Kombinator wird dasselbe gemacht, nur allgemeiner. Solche
Emulatoren wie unserer sind immer ähnlich wie Y.
[[https://dreamsongs.com/Files/WhyOfY.pdf][The Why of Y]] ist ein hübscher und knapper Artikel zum Y-Kombinator und
wie man aus praktischen Gründen darauf kommen könnte, ähnlich zu
unserem gedanklichen Pfad. [[https://mitpress.mit.edu/books/little-schemer-fourth-edition][The Little Schemer]] beendet seine schöne
Reise auch mit dem Schreiben eines metazirkulären Evaluators wie bei
uns, und dort wird darauf eingegangen, wie man Y herleiten könnte.
[fn:arity-mismatch] Wenn ein Benutzer die Prozedur mit der falschen
Anzahl an Argumenten aufruft, verursacht das eine Fehlermeldung, die
aber nicht sagt, was los ist.
Denken Sie darüber nach, wie wir eine bessere Meldung bringen könnten.
(Hinweis: Wenn sich die =length= von =args= und =vals= unterscheidet,
stimmt ’was nicht!)
[fn:lexical-scope-happens-here] Hier fällt die Entscheidung zwischen
lexikalischer und dynamischer Bindung. Erweitern tun wir nicht die
Umgebung von da, wo aufgerufen wird, sondern die Umgebung des letzten
Ausdrucks, in der das Lambda definiert worden ist. Das ermöglicht,
dass die Techniken aus dem Abschnitt über Closures (Abschlüsse)
funktionieren.
Man kann „lexikalische Bindung“ beschreiben als dass die Bindungen
„von da kommen, wo der Code geschrieben ist“. Das bietet Sicherheit
über Capabilitys, ist explizit und garantiert die Eigenschaft, dass
man „nur auf das Zugriff hat, was man auch hat“. Bei „dynamischer
Bindung“ gälte hingegen, dass die Bindungen „von da kommen, wo der
Aufruf stattfindet“ (und wo der Aufruf davon stattfindet und so
weiter, den aktuellen Aufruf-Stack hinauf). Bindungen sind implizit
und weithin zugänglich (ambient im Sinne von „Ambient Authority“).
Dynamische Bindung hat somit öfter schwerwiegende Sicherheitsnachteile
(es ist kein Zufall, dass sie an Zugriffskontrolllisten, ACLs,
erinnert).
[fn:lisp-read] Es ist bemerkenswert, dass wir die Mächtigkeit in
=quote= so ausspielen konnten, dass wir einen Interpretierer zustande
gebracht haben, ohne überhaupt Syntax in Textform zu parsen: Wir haben
lediglich unsere Datenstrukturen quotiert!
Tatsächlich geht es in der Mehrzahl der Lehrbücher zum Entwurf von
Programmiersprachen viel zu sehr um das Parsen der Textform einer
Sprache, und das verleitet zur Illusion, das Wichtige an der Struktur
einer Sprache hätte zu tun mit der /Oberflächensyntax/. Das ist
eigentlich niemals wahr.
Jedes vorstellbare Paradigma einer Programmiersprache ist darstellbar
als einfacher symbolischer Ausdruck, so wie in Scheme.
In Wirklichkeit sind sich Code und Daten viel ähnlicher als es scheint.
Natürlich stimmt es, dass wir für eine nützliche Programmiersprache
auch Programme aus Quelldateien auf Datenträgern lesen können wollen.
=read= ist ein Teil des Scheme-Standards (und auch in fast jedem
anderen Lisp enthalten), also können wir im Allgemeinen einfach davon
Gebrauch machen.
=read= selber in Scheme zu implementieren, ist aber auch leicht. Dazu
ist ungefähr so viel Code nötig, wie wir für =evaluate= gebraucht
haben.
[fn:more-reading] Sie haben es nicht nur bis zum letzten Abschnitt
geschafft, sondern bis zur letzten Fußnote! Wir dürfen also annehmen,
dass Sie sich wirklich für solche Dinge interessieren, deshalb nennen
wir Ihnen noch weitere Ressourcen:
- Mehr darüber, wie Evaluatoren wie der, den wir oben geschrieben
haben, funktionieren, und etwas über die Geschichte dahinter,
erfahren Sie in William Byrds großartigem Vortrag:
[[https://www.youtube.com/watch?v=OyfBQmvr2Hc][The Most Beautiful Program Ever Written]].
- [[https://mitpress.mit.edu/sites/default/files/sicp/index.html][Structure and Interpretation of Computer Programs]] (kurz SICP oder
„Wizard Book“) enthält ausführlichere Fassungen fast aller Themen,
die wir auch besprochen haben. Eine deutsche Übersetzung „Struktur
und Interpretation von Computerprogrammen“ hat Susanne
Daniels-Herold verfasst. In SICP findet man des Weiteren, wie man
eine Ereignisschleife aufbaut, Lösungsprogramme für Beschränkungen,
Evaluatoren wie unseren, Evaluatoren für Logikprogramme sowie
Compiler in effizienteren Maschinencode. Es gibt keine bessere
Möglichkeit, etwas über die Natur von Berechnungen zu lernen, als
SICP zu studieren. Dazu eignet sich, zwischen dem Lesen des
Quellbuchs und [[https://www.youtube.com/watch?v=-J_xL4IGhJA&list=PLE18841CABEA24090][dem Schauen der Vorlesungen aus den 1980ern]] zu
wechseln. Die englische Version von SICP ist verfügbar als
gedrucktes Buch, als „info“-Handbuch, das man bequem aus Emacs
heraus lesen kann, oder als HTML, aber wenn Sie eh schon mit einem
Web-Browser lesen, raten wir deutlich zu [[https://sarabander.github.io/sicp/html/][dieser Fassung]].
- [[https://mitpress.mit.edu/books/little-schemer-fourth-edition][The Little Schemer]] ist ein unterhaltsames Buch, das als Gespräch
geschrieben wurde. Darin finden sich viele nützliche
Unterrichtseinheiten auf spielerische, puzzleartige Art, garniert
mit entzückenden Illustrationen. Es gibt zahlreiche
Nachfolgebücher aus dieser Reihe, wo andere Gebiete der Informatik
mit Tiefgang erkundet werden.
- [[https://mitpress.mit.edu/books/software-design-flexibility][Software Design for Flexibility]] führt viele Ideen aus SICP fort und
baut darauf weiter auf. Man kann es als den wahren Nachfolger von
SICP auffassen.
- Ganz klar lernt man aber sehr viel über Scheme oder andere Lisps,
indem man sie einfach benutzt. Wir schätzen es, wenn Sie zu
[[https://spritelyproject.org/][Spritely]] oder [[https://guix.gnu.org/][Guix]] beitragen, wo Sie Ihre Fähigkeiten einbringen
können!
Gute Reise!