PicoLisp: vom Einstieg zum fortgeschrittenen Programmieren

Ein zusammenhängender, deutschsprachiger Kurs in 21 Lektionen, zwei Kursteilen und mit Aufgaben, Lösungen und zwei Abschlussprojekten.

Voraussetzung: Du kennst grundlegende Lisp-Ausdrücke. PicoLisp-Vorkenntnisse brauchst Du nicht. Arbeitsweise: Lies eine Lektion, sage die Ergebnisse der Beispiele voraus, probiere sie in der REPL aus und vergleiche anschließend mit den Lösungen. Beginne bei Bedarf eine neue Sitzung mit pil +.

Im Text bezeichnet : die REPL-Eingabeaufforderung und -> deren Ergebnis; beide Zeichenfolgen gehören nicht zum eingegebenen Ausdruck. Beispiele innerhalb einer Lektion können aufeinander aufbauen. Die Beispiele wurden anhand offizieller PicoLisp-Dokumentation geprüft, aber bei der Erstellung dieses Kurses nicht in einer lokalen PicoLisp-Installation ausgeführt.

Inhaltsverzeichnis

Teil I: Grundlagen

Dieser Teil baut das Auswertungsmodell und den Umgang mit Daten, Listen und Funktionen schrittweise auf. Sein Abschlussprojekt ist eine kleine Preisrechnung.

1. Der erste Dialog mit der REPL

Bei einer globalen Installation startest Du mit pil +. Das + lädt auch die interaktive Entwicklungsumgebung. Die REPL zeigt : als Eingabeaufforderung. Mit (bye) oder Strg+D beendest Du sie. Bei einer lokalen Installation kann der Startbefehl anders aussehen; halte Dich dann an deren INSTALL-Datei.

$ pil +
: (+ 2 3)
-> 5
: (* (+ 2 3) 4)
-> 20

Lies (* (+ 2 3) 4) von innen nach außen: Erst wird (+ 2 3) zu 5, dann (* 5 4) zu 20. Eine Zahl ergibt bei der Auswertung sich selbst. Ein Symbol ergibt seinen Wert. Eine Liste wird gewöhnlich als Aufruf behandelt: vorn steht die Funktion, dahinter stehen die Argumente.

PicoLisp hat hier eine wichtige Ausnahme: Steht eine Zahl an erster Stelle einer Liste, wird diese Liste als Datenwert zurückgegeben.

(+ 1 2 3)   # Aufruf: ergibt 6
(1 2 3)     # Daten: ergibt (1 2 3)

Die zweite Zeile ist eine Besonderheit von PicoLisp, kein allgemeines Lisp-Gesetz. Kommentare beginnen mit # und reichen bis zum Zeilenende.

Versuch: Welche Ergebnisse erwartest Du für (+ 1 (* 2 3)) und (7 8 9)? Prüfe sie in der REPL.

2. Daten, Symbole und das Hochkomma

Ein Symbol wie A bezeichnet normalerweise seinen aktuellen Wert. Mit 'A verlangst Du hingegen das Symbol selbst. Das Hochkomma verhindert die normale Auswertung des folgenden Ausdrucks.

: (setq A 10)
-> 10
: A
-> 10
: 'A
-> A

Dasselbe gilt für Listen aus Symbolen:

: '(rot gruen blau)
-> (rot gruen blau)

Ohne Hochkomma würde PicoLisp versuchen, rot als Funktion aufzurufen. Das Hochkomma ist bei der numerischen Liste (1 2 3) entbehrlich, aber bei (rot gruen blau) entscheidend.

PicoLisp-Eigenheit: Hier ist ' eine Leseschreibweise für quote; quote gibt die gesamte unausgewertete Argumentfolge zurück. Deshalb verhalten sich einige Details anders als bei quote in Common Lisp. Für den Einstieg genügt: 'A liefert das Symbol A, '(a b) liefert die Datenliste (a b).

NIL bedeutet unter anderem „falsch“ und „leere Liste“. T ist ein üblicher wahrer Wert. In Bedingungen gilt jedes Ergebnis außer NIL als wahr, auch die Zahl 0. PicoLisp behandelt auch () und den leeren String "" beim Einlesen als NIL.

Versuch: Sage für A, 'A, '(A 2) und (1 2) die Ergebnisse voraus, nachdem (setq A 10) ausgeführt wurde. Achte besonders darauf, ob das A innerhalb der mit ' beginnenden Liste ersetzt wird.

3. Listen bauen und zerlegen

list wertet seine Argumente aus und setzt die Ergebnisse zu einer Liste zusammen. car nimmt das erste Element, cdr den Rest. cons setzt ein Element vor eine vorhandene Liste.

: (list (+ 1 2) 4)
-> (3 4)
: (car '(rot gruen blau))
-> rot
: (cdr '(rot gruen blau))
-> (gruen blau)
: (cons 'gelb '(rot gruen))
-> (gelb rot gruen)

Denke bei cdr an eine Liste, nicht an „das zweite Element“. Auf (rot gruen blau) angewandt liefert es (gruen blau). Das zweite Element bekämst Du mit (car (cdr ...)), kurz auch mit cadr.

Vergleiche nochmals Daten mit Auswertung:

: (list 1 (+ 1 2))
-> (1 3)
: '(1 (+ 1 2))
-> (1 (+ 1 2))

Im ersten Fall wird der innere Aufruf ausgeführt. Im zweiten Fall bleibt er Teil der Daten.

Versuch: Baue aus '(wald wiese) mit cons die Liste (berg wald wiese). Bestimme danach car und cdr dieser neuen Liste.

4. Werte binden und Funktionen definieren

setq setzt den Wert eines Symbols. Für eine vorübergehende Bindung benutze let:

: (setq Preis 8)
-> 8
: (let Preis 5 (+ Preis 2))
-> 7
: Preis
-> 8

Der ursprüngliche Wert von Preis ist nach let wieder da. Für mehrere Bindungen gibt es beispielsweise (let (A 2 B 3) (+ A B)). Die symbolischen Namen in diesem Kurs sind großgeschrieben, um lokale Variablen leicht von Funktionsnamen zu unterscheiden; PicoLisp unterscheidet Groß- und Kleinschreibung.

Eine Funktion definierst Du mit de. Das letzte ausgewertete Stück ihres Rumpfs ist ihr Rückgabewert:

: (de quadrat (N) (* N N))
-> quadrat
: (quadrat 6)
-> 36
: (quadrat (+ 2 3))
-> 25

Hier ist (N) die Parameterliste. Das Argument wird vor dem Aufruf ausgewertet und vorübergehend an N gebunden. Eine Funktion ohne Argumente verwendet () als Parameterliste, zum Beispiel (de antwort () 42).

Versuch: Definiere doppelt, sodass (doppelt 7) den Wert 14 liefert. Schreibe danach plus-drei, das einen Wert um 3 erhöht. Prüfe (plus-drei (doppelt 4)).

5. Entscheidungen und Rekursion

if wertet zunächst die Bedingung aus. Ist ihr Ergebnis nicht NIL, wird der erste Zweig ausgeführt, sonst der zweite.

: (if (> 5 3) 'gross 'klein)
-> gross
: (if (> 2 3) 'gross 'klein)
-> klein

Setze mit diesem Werkzeug mehrere Konzepte zusammen:

(de summe (Zahlen)
   (if Zahlen
      (+ (car Zahlen) (summe (cdr Zahlen)))
      0 ) )

(summe '(2 3 4)) ergibt 9. Lies den ersten Schritt als 2 + summe(3 4), den nächsten als 3 + summe(4). Bei der leeren Liste ist Zahlen gleich NIL; dann liefert die Funktion 0 und die Rekursion endet. Der Basisfall gehört zu jedem rekursiven Entwurf.

Diese Funktion verändert ihre Eingabeliste nicht. Sie zeigt, wie sich Listenstruktur und Kontrollfluss direkt aufeinander abbilden lassen.

Versuch: Was liefern (summe NIL) und (summe '(5))? Ändere summe zu anzahl, sodass '(a b c) die Anzahl 3 liefert.

6. Funktionen sind auch Werte

Für einen festen, kurzen Rechenschritt musst Du nicht immer einen Namen definieren. PicoLisp verwendet an der entsprechenden Stelle eine Funktionsliste ohne lambda-Schlüsselwort. Das Hochkomma hält diese Liste zunächst als Daten fest; mapcar benutzt sie anschließend als Funktion.

: (mapcar '((N) (* N N)) (1 2 3 4))
-> (1 4 9 16)

Der erste Teil '((N) (* N N)) beschreibt eine Funktion mit Parameter N und Rumpf (* N N). mapcar ruft sie für jedes Listenelement auf und sammelt die Ergebnisse. Mit benannter Funktion geht es ebenso:

(de quadrat (N) (* N N))
(mapcar quadrat (1 2 3 4))  # (1 4 9 16)

In PicoLisp kann man eine Liste mit Zahl am Anfang hier unmittelbar als Daten übergeben. mapcar kann auch mehrere Listen parallel verarbeiten:

: (mapcar + (1 2 3) (4 5 6))
-> (5 7 9)

Versuch: Verwandle (3 4 5) mit mapcar in (6 8 10). Löse es einmal mit einer benannten Funktion und einmal mit einer anonymen Funktionsliste.

7. Eine zentrale Eigenheit: dynamische Bindung

Bei let und Funktionsparametern bindet PicoLisp Symbole dynamisch. Eine aufgerufene Funktion kann während des Aufrufs einen zeitweilig gebundenen Wert sehen, obwohl der Name nicht in ihrem eigenen Quelltext von einem äußeren let umgeben ist.

: (de zeige () Wert)
-> zeige
: (setq Wert 4)
-> 4
: (let Wert 9 (zeige))
-> 9
: (zeige)
-> 4

Der Aufruf von zeige findet innerhalb der dynamischen Bindung Wert = 9 statt. Anschließend wird der vorherige Wert 4 wiederhergestellt. Wer lexikalische Bindung aus anderen Lisps erwartet, muss hier bewusst umdenken. Für leicht lesbaren Code: Übergib Werte als Argumente, wenn die Funktion ausdrücklich von ihnen abhängt. Dynamische Bindung kannst Du dann gezielt einsetzen, wenn Du sie verstanden hast.

PicoLisp verwendet außerdem einen Wertplatz pro Symbol: Ein Symbol hat nicht gleichzeitig einen getrennten Variablenwert und eine gleichnamige Funktionsdefinition. Vermeide deshalb, einen Funktionsnamen versehentlich mit setq als gewöhnliche Variable zu überschreiben.

Versuch: Was gibt der folgende Ausdruck zurück? Erkläre, weshalb zeige danach wieder den Wert 4 liefert.

(let Wert 20 (+ (zeige) 1))

8. Eine weitere Eigenheit: Argumente selbst auswerten

Bislang stand bei de eine Liste von Parametern, etwa (X Y): Die übergebenen Argumente werden ausgewertet. Ein einzelnes Parametersymbol bedeutet etwas anderes: Es erhält die komplette Liste der unausgewerteten Ausdrücke.

: (setq A 2)
-> 2
: (de roh Ausdruecke (car Ausdruecke))
-> roh
: (roh (+ 1 2) A)
-> (+ 1 2)

roh liefert den ersten übergebenen Ausdruck als Daten. Er wurde nicht zu 3 berechnet. Möchtest Du ihn in der Funktion ausdrücklich auswerten, geht das mit eval:

: (de berechnet Ausdruecke (eval (car Ausdruecke)))
-> berechnet
: (berechnet (+ 1 2) A)
-> 3

Dieses Werkzeug ist mächtig, aber für Alltagsfunktionen meist unnötig. Für normale Funktionen verwende (X Y) oder (X) als Parameterliste. Merke Dir die drei Formen:

Form nach dem Funktionsnamen Bedeutung
(X Y) feste Anzahl ausgewerteter Argumente
Ausdruecke sämtliche Ausdrücke unausgewertet als Liste
@ beliebig viele ausgewertete Argumente; Zugriff unter anderem mit next

Die @-Form kannst Du später vertiefen. Für den Einstieg reicht es, sie beim Lesen fremder Programme zu erkennen.

Versuch: Was liefert (roh A (+ 4 5))? Was liefert (berechnet A (+ 4 5))? Ändert der zweite übergebene Ausdruck eines der Ergebnisse?

9. Abschlussprojekt: kleine Preisrechnung

Schreibe die drei Funktionen aus den vorigen Lektionen zusammenhängend in eine Datei preise.l:

(de verdoppeln (Preis)
   (* 2 Preis) )

(de summe (Preise)
   (if Preise
      (+ (car Preise) (summe (cdr Preise)))
      0 ) )

(de rechnung (Preise)
   (summe (mapcar verdoppeln Preise)) )

Die Preise seien ganze Zahlen in einer selbstgewählten kleinsten Einheit, zum Beispiel Cent. Wenn Du diese Datei in der REPL mit (load "preise.l") lädst, kannst Du ausprobieren:

: (rechnung (3 5 2))
-> 20

Lies die Auswertung in drei Stufen: mapcar macht aus (3 5 2) die Liste (6 10 4). summe macht daraus 20. rechnung gibt diesen Wert zurück. Es ist eine kleine Rechnung, keine vollständige Preis- oder Währungsbibliothek.

Deine Aufgabe: Erweitere das Programm um (de plus-eins (N) (+ N 1)) und (de mit-zuschlag (Preise) ...), sodass (mit-zuschlag (3 5 2)) das Ergebnis 13 hat: Erst wird jeder Einzelwert um eins erhöht, dann werden die Werte summiert. Teste auch die leere Liste.

Lösungen zu Teil I

  1. (+ 1 (* 2 3)) → 7; (7 8 9) → (7 8 9).
  2. Nach (setq A 10): A → 10; 'A → A; '(A 2) → (A 2); (1 2) → (1 2). Zitat schützt auch das A in der Liste.
  3. (cons 'berg '(wald wiese)) → (berg wald wiese); car davon → berg; cdr davon → (wald wiese).
  4. (de doppelt (N) (* 2 N)) und (de plus-drei (N) (+ N 3)); (plus-drei (doppelt 4)) → 11.
  5. (summe NIL) → 0, (summe '(5)) → 5. Eine Lösung für die Länge: (de anzahl (Lst) (if Lst (+ 1 (anzahl (cdr Lst))) 0)).
  6. Benannt: (de doppelt (N) (* 2 N)), dann (mapcar doppelt (3 4 5)). Anonym: (mapcar '((N) (* 2 N)) (3 4 5)). Jeweils (6 8 10).
  7. 21; innerhalb von let sieht zeige den Wert 20. Nach Ende von let wird Wert wieder zu 4.
  8. (roh A (+ 4 5)) → A; (berechnet A (+ 4 5)) → 2. Der zweite Ausdruck wird von beiden Funktionen nicht benutzt.
  9. (de mit-zuschlag (Preise) (summe (mapcar plus-eins Preise))). Für (3 5 2) entsteht (4 6 3) und damit 13; für NIL ergibt sich 0.

Teil II: Vertiefung und Praxis

Du vertiefst die Sprache und lernst, Programme zu untersuchen, Daten zu verarbeiten und PicoLisps Objektmodell einzuordnen. Die Lektionen 10 bis 21 setzen Teil I voraus.

10. Dynamische Bindung bewusst verwenden

Aus Teil I kennst Du: let bindet einen Namen vorübergehend. Entscheidend ist, welcher Wert während eines Funktionsaufrufs aktiv ist.

(setq Faktor 2)

(de skaliere (N)
   (* N Faktor) )

(skaliere 4)                  # 8
(let Faktor 5 (skaliere 4))   # 20
(skaliere 4)                  # wieder 8

Die Funktion skaliere erhält Faktor nicht als Argument. Sie sieht die gerade aktive Bindung. Das macht dynamische Kontexte möglich, erzeugt aber auch unsichtbare Abhängigkeiten. Eine explizite Variante ist leichter getrennt zu prüfen:

(de skaliere-mit (N Faktor)
   (* N Faktor) )

Entscheidungsregel: Was das Ergebnis eines fachlichen Rechenschritts bestimmt, gib zunächst als Argument weiter. Eine dynamische Bindung lohnt sich für einen bewusst gemeinsamen Kontext eines Aufrufs, etwa eine temporär gewählte Einstellung.

Aufgabe 10: Definiere anzeige, die Praefix und ein übergebenes Symbol zu einer Liste zusammenfasst. Prüfe (let Praefix 'neu (anzeige 'wald)). Welchen Wert hat Praefix nach dem let, wenn es vorher auf 'alt gesetzt war?

11. Symbole haben einen Wert und Eigenschaften

Ein PicoLisp-Symbol kann neben seinem Wert benannte Eigenschaften tragen. setq setzt den Wert; put setzt eine Eigenschaft; get liest sie. Diese zwei Speicherorte sind unabhängig.

: (setq Baum 'eiche)
-> eiche
: (put 'Baum 'hoehe 12)
-> 12
: Baum
-> eiche
: (get 'Baum 'hoehe)
-> 12
: (show 'Baum)
Baum eiche
   hoehe 12
-> Baum

'Baum bezeichnet das Symbol selbst, damit put und get dessen Eigenschaft erreichen. Baum ohne Hochkomma liefert hier seinen Wert eiche. Eigenschaften sind besonders nützlich für kleine Objekte im Speicher. Überlade globale, benannte Symbole aber nicht versehentlich mit beliebigen Testdaten: Ihre Änderungen leben in der laufenden Sitzung fort.

Für ein frisches, anonymes Symbol kannst Du new benutzen:

(setq Eintrag (new))
(put Eintrag 'name "Eiche")
(put Eintrag 'hoehe 12)
(get Eintrag 'name)             # "Eiche"

Die von new angezeigte interne Kennung kann von Sitzung zu Sitzung abweichen; beurteile den Wert über get, nicht über seine gedruckte Kennung.

Aufgabe 11: Lege mit new einen zweiten Eintrag an. Speichere unter name den String "Birke" und unter hoehe die Zahl 7. Lies beide Eigenschaften wieder aus. Erkläre den Unterschied zwischen Eintrag und 'Eintrag.

12. Gleichheit, Wahrheit und numerische Fallen

Zwei Listen können denselben Inhalt haben, obwohl es verschiedene Listenobjekte sind. In PicoLisp prüft = inhaltliche Gleichheit, == Identität. Schreibe für Datenvergleiche zunächst =:

(= '(a b) '(a b))    # wahr
(= '(a b) '(a c))    # NIL

Bei == hängt das Ergebnis für getrennt erzeugte Listen von ihrer Objektidentität ab; verwende es, wenn genau diese Identität Dein Thema ist. Gleiche Zahlen und einige Symbole können trotz getrenntem Auftreten identisch sein. Wähle deshalb nicht zufällig einen dieser Operatoren anhand eines einzigen Beispiels.

NIL ist falsch; jeder andere Wert ist in Bedingungen wahr. Die Zahl 0 ist wahr. Das sieht man an =0: Bei einer Null gibt diese Funktion selbst 0 zurück und eignet sich trotzdem als Bedingung.

(if (=0 0) 'null 'anders)   # null
(if 0 'wahr 'falsch)        # wahr

PicoLisp arbeitet mit ganzen Zahlen; für Dezimaldarstellung gibt es eine Skalierung fester Genauigkeit. Für die Übungen mit Geld benutze ganze Cent, bis Du format, */ und die Genauigkeitskonvention verstanden hast.

Aufgabe 12: Was ergeben (if NIL 'ja 'nein) und (if (=0 3) 'ja 'nein)? Warum wäre (if Anzahl ...) kein geeigneter Test für „Anzahl ist größer als null“, wenn Anzahl eine Zahl sein muss?

13. Listen als Verarbeitungskette

Du kennst mapcar. Zwei verwandte Werkzeuge sind filter (Elemente anhand einer Bedingung behalten) und apply (eine Liste als Argumente einer Funktion übergeben).

(filter '((N) (> N 3)) (1 5 2 8))    # (5 8)
(mapcar '((N) (* N 2)) (5 8))        # (10 16)
(apply + (10 16))                     # 26

Als ein Ausdruck:

(apply +
   (mapcar '((N) (* N 2))
      (filter '((N) (> N 3)) (1 5 2 8)) ) )

Beim Lesen immer von innen beginnen: filtern → umformen → zusammenfassen. Schreibe Zwischenergebnisse getrennt in der REPL aus, wenn ein solcher Ausdruck schwer zu verstehen ist. filter prüft auf ein Ergebnis ungleich NIL, nicht auf einen besonderen Bool-Typ.

Aufgabe 13: Bilde aus (2 7 4 9) erst alle Zahlen größer als 4, dann ihre Quadrate und schließlich deren Summe. Erwartet wird 130.

14. Variable Argumentzahl mit @

In Teil I stand die @-Parameterform nur auf der Merkliste. Sie nimmt beliebig viele ausgewertete Argumente an. args sagt, ob noch eines bereitliegt; next nimmt es heraus.

(de summiere @
   (let Ergebnis 0
      (while (args)
         (setq Ergebnis (+ Ergebnis (next))) )
      Ergebnis ) )

(summiere)                  # 0
(summiere 2 3 (+ 1 4))      # 10

(args) ist die richtige Prüfung auf vorhandene Argumente: Ein übergebenes NIL ist immer noch ein vorhandenes Argument. Wenn Du nach (next) nur seinen Wahrheitswert prüfst, verlierst Du diese Unterscheidung. rest zeigt die noch ausstehenden Werte, arg erlaubt gezieltes Nachsehen. Im normalen Programmcode ist eine Liste als Argument oft einfacher; @ eignet sich, wenn die Aufrufstelle wirklich variabel sein soll.

Aufgabe 14: Schreibe groesste für mindestens ein numerisches Argument. Benutze (next) einmal für den Startwert und danach while. Welche Vorbedingung hat Deine Funktion? Eine leere Argumentfolge soll nicht stillschweigend als Zahl behandelt werden.

15. Ein- und Ausgabe geschieht im aktuellen Kanal

in bindet den Eingabekanal für seinen Rumpf, out den Ausgabekanal. Danach gilt wieder der vorherige Kanal. Das ist die Idee dynamischer Bindung aus Lektion 10, diesmal für I/O.

Das folgende Beispiel benutzt eine selbst angelegte Übungsdatei im aktuellen Arbeitsverzeichnis:

(out "kurs-zahlen.l"
   (println '(2 4 6)) )

(in "kurs-zahlen.l"
   (read) )                     # (2 4 6)

read liest einen Lisp-Ausdruck. Das Dateiformat ist damit Lisp-Datenrepräsentation, keine CSV-Datei. Für gewöhnliche Textzeilen gibt es line, etwa (in "kurs-zahlen.l" (line T)). out schreibt in die angegebene Datei; wähle für Experimente deshalb einen eigens angelegten Namen, dessen bisherigen Inhalt Du nicht brauchst.

Funktionen brauchen den Kanal nicht als eigenes Argument, solange sie innerhalb von in oder out aufgerufen werden. Das macht verschachtelte Lese-/Schreiboperationen bequem, aber das aktive Ziel ist dann auch ein Teil ihres Ausführungskontexts.

Aufgabe 15: Speichere die Liste '(3 5 7) in kurs-werte.l. Lies sie mit read wieder ein und berechne mit apply ihre Summe. Trenne Einlesen und Rechnen zunächst in zwei REPL-Ausdrücke.

16. Programme untersuchen statt Ergebnisse raten

Die REPL ist nicht nur ein Taschenrechner. pp zeigt eine definierte Funktion formatiert, show den Wert und die Eigenschaften eines Symbols. trace zeigt Aufrufe und Rückgaben einer Funktion, untrace beendet die Nachverfolgung.

(de fakultaet (N)
   (if (=0 N)
      1
      (* N (fakultaet (dec N))) ) )

(pp 'fakultaet)
(trace 'fakultaet)
(fakultaet 3)              # Ergebnis 6; zusätzlich Aufrufspur
(untrace 'fakultaet)

Beim Trace erkennst Du an der Einrückung die Tiefe der rekursiven Aufrufe. Für eine schwerere Stelle kannst Du debug nutzen und in der Haltestelle Variablen abfragen. Beende einen Debug-Versuch anschließend mit unbug. Die Debug-Werkzeuge werden mit pil + geladen.

Aufgabe 16: Verändere den Basisfall von fakultaet versuchsweise zu 2. Sage voraus, wie sich (fakultaet 3) ändert. Benutze trace, um die Stelle zu finden, an der das falsche Ergebnis entsteht, und stelle danach den Basisfall wieder her.

17. Abbruch, Fehler und Aufräumen

catch und throw bilden einen benannten, nicht lokalen Abbruch. Was nach einem ausgeführten throw im catch-Rumpf folgt, wird nicht mehr ausgewertet.

(catch 'Abbruch
   (throw 'Abbruch 17)
   99 )                        # 17

Ein Beispiel mit eigener Prüfung:

(de positiv-oder-abbruch (N)
   (if (> N 0)
      N
      (throw 'Ungueltig N) ) )

(catch 'Ungueltig
   (positiv-oder-abbruch 0)
   'alles-gut )                 # 0

Auch hier gilt: 0 ist ein Rückgabewert und darf nicht mit „kein Abbruch“ verwechselt werden. catch setzt zusätzlich @@ als Kennzeichen für einen erfolgten Abbruch beziehungsweise einen abgefangenen Fehler. finally führt seinen Aufräumausdruck auch bei throw aus:

(catch 'Ende
   (finally (prinl "Aufgeraeumt")
      (throw 'Ende 7) ) )       # druckt Aufgeraeumt; ergibt 7

Das ist kein Ersatz für genaue Fehlerbehandlung. Benenne den Abbruch nach seinem Zweck und fange ihn an einer Stelle, die sinnvoll darauf reagieren kann.

Aufgabe 17: Schreibe nicht-negativ-oder-abbruch: Negative Zahlen sollen unter dem Kennzeichen Ungueltig abgebrochen werden, 0 und positive Zahlen sollen unverändert zurückkommen. Prüfe insbesondere den Fall 0.

18. Das Objektmodell: Symbole, Klassen, Nachrichten

Die anonymen Symbole aus Lektion 11 sind bereits die Grundlage für Objekte. Klassen definieren Verhalten, Eigenschaften enthalten den Zustand. class legt eine Klasse an, dm eine Methode, new ein Objekt und send ruft eine Nachricht am Objekt auf.

(class +Artikel)

(dm preis> ()
   (get This 'preis) )

(setq MeinArtikel
   (new '(+Artikel) 'name "Seil" 'preis 9) )

(get MeinArtikel 'name)          # "Seil"
(send 'preis> MeinArtikel)       # 9

This bezeichnet während einer Methode das aktuelle Objekt. Weil wir keine spezielle Initialisierungsmethode T definiert haben, dienen die nach der Klassenliste folgenden Paare bei new als Eigenschaften. Eine weitere Klasse könnte ebenfalls die Nachricht preis> verstehen und anders beantworten: Das ist Polymorphie. Vererbung und Mehrfachvererbung existieren, sind für dieses erste Objekt aber noch nicht nötig.

Aufgabe 18: Ergänze eine Methode doppelpreis>, die mit (get This 'preis) rechnet. Lege ein zweites +Artikel-Objekt mit Preis 4 an und prüfe beide Objekte. Verwende neue Objekte, statt die Methode mit setq zu überschreiben.

19. Datenbank: wann Eigenschaften dauerhaft werden

Die Objekte der vorigen Lektion waren für die laufende Sitzung bestimmt. Die eingebaute PicoLisp-Datenbank verwendet externe Symbole: Deren Wert und Eigenschaften werden bei Zugriff geladen und bei einer Transaktion dauerhaft geschrieben. commit schreibt Änderungen; rollback verwirft Änderungen seit dem letzten Commit. Die Datenbank ist daher mehr als „out für Objekte“.

Die nächsten Begriffe bilden eine Landkarte, noch kein Befehlsskript für eine gemeinsam genutzte Datenbank:

Begriff Rolle
pool öffnet einen Datenbankkontext
externes Symbol dauerhafte Identität eines Datensatzes
+Entity typische Basisklasse einer persistenten Entität
rel beschreibt Eigenschaften und Beziehungen von Entitäten
dbSync, commit, rollback Synchronisierung und Transaktionsabschluss

Arbeite zuerst in einem separaten Übungsverzeichnis, bevor Du Datenbankbeispiele ausführst. Für mehr als eine Zeile persistenten Code sind das konkrete Schema, die Transaktionsgrenzen und die Indizes entscheidend. Die offizielle Referenz beschreibt außerdem Kurztransaktionen, etwa new!, sowie Beziehungen und Indizes. Beginne dort mit einem einzigen Entitätstyp und erweitere erst danach.

Aufgabe 19: Erkläre in zwei Sätzen, weshalb (new '(+Artikel) ...) aus Lektion 18 noch keinen persistenten Datensatz erzeugt. Wo liegt der Unterschied zwischen commit und rollback?

20. Pilog: Fragen statt Ablauf angeben

Pilog ist die in PicoLisp eingebaute Logiksprache mit Lisp-Schreibweise. be definiert Fakten oder Regeln; Namen mit @ stehen in Pilog für Variablen. Ein kleines Beispiel aus der offiziellen Referenz:

(be likes (John Mary))
(be likes (John wine))

(? (likes John @X))

Die Frage sucht Werte für @X, zu denen likes John @X passt. ? ist interaktiv und zeigt Lösungen einzeln an. Eine Regel kann andere Beziehungen als Voraussetzungen verwenden:

(be likes (John @X)
   (likes @X wine)
   (likes @X food) )

Damit kann Pilog weitere passende Werte erschließen, wenn die nötigen Fakten über diese Werte vorhanden sind. Die Großschreibung bei John und Mary macht aus ihnen hier keine Pilog-Variablen; dafür ist das führende @ entscheidend. Schreibe Regeln nur, wenn die beabsichtigte Abfrage wirklich deklarativ einfacher wird als eine normale Funktion.

Aufgabe 20: Ergänze das Faktum (be likes (Mary tea)). Welche Werte erwartest Du für die Anfrage (? (likes Mary @X))? Weshalb kann die gezeigte Regel damit allein noch keinen neuen Wert für likes John @X beweisen?

21. Abschlussprojekt: ein kleiner Artikelkatalog

Du setzt mehrere Lektionen zu einer Datei artikel.l zusammen. Die Basis speichert Artikel als anonyme Objekte im Speicher. So kannst Du erst das Verhalten prüfen; die Datenbank wäre später ein eigener, bewusster Schritt.

(class +Artikel)

(dm preis> ()
   (get This 'preis) )

(de artikel (Name Preis)
   (new '(+Artikel) 'name Name 'preis Preis) )

(de guenstiger-als (Grenze Artikel)
   (filter
      '((Obj) (< (send 'preis> Obj) Grenze))
      Artikel ) )

(de preissumme (Artikel)
   (if Artikel
      (apply + (mapcar '((Obj) (send 'preis> Obj)) Artikel))
      0 ) )

(setq Katalog
   (list
      (artikel "Seil" 9)
      (artikel "Fackel" 4)
      (artikel "Karte" 12) ) )

(preissumme Katalog)                      # 25
(preissumme (guenstiger-als 10 Katalog)) # 13

Lies die letzte Zeile als Anfrage: Wähle Artikel mit Preis unter 10, lies deren Preise, addiere sie. Das Ergebnis 13 kommt von 9 + 4; 12 scheidet aus. Die anonyme Funktion in guenstiger-als sieht dabei die dynamisch gebundene Grenze aus dem äußeren Aufruf; sie ist keine lexikalische Closure. Für eine leere Liste gibt preissumme ausdrücklich 0 zurück. Die Ausgabe einer Liste anonymer Objekte kann interne Kennungen enthalten; beurteile das Ergebnis über Eigenschaften und Preisfunktionen.

Deine Erweiterung: Schreibe teurer-als mit einem strikten > und danach namen, das aus einer Artikelliste die Namen als Liste zurückgibt. Prüfe:

(namen (teurer-als 8 Katalog))   # ("Seil" "Karte")
(preissumme NIL)                # 0

Überlege abschließend, welche zwei Dinge Du ändern müsstest, um den Katalog dauerhaft zu speichern. Trenne dabei Datei mit Lisp-Daten (Lektion 15) von persistentem Objekt mit Transaktion (Lektion 19).

Lösungen zu Teil II

  1. (setq Praefix 'alt); (de anzeige (Wort) (list Praefix Wort)); im let entsteht (neu wald), danach ist Praefix wieder alt.
  2. (setq Zweiter (new)); (put Zweiter 'name "Birke"); (put Zweiter 'hoehe 7); danach (get Zweiter 'name) und (get Zweiter 'hoehe). Zweiter liefert das gespeicherte Objekt, 'Zweiter das globale Symbol, das auf dieses Objekt zeigt.
  3. Beide if-Ausdrücke ergeben nein. Eine Zahl, auch 0, ist in PicoLisp wahr; teste beispielsweise mit (> Anzahl 0).
  4. (apply + (mapcar '((N) (* N N)) (filter '((N) (> N 4)) (2 7 4 9)))) → 130 (49 + 81).
  5. Eine Möglichkeit: (de groesste @ (let Bestes (next) (while (args) (setq Bestes (max Bestes (next)))) Bestes)). Vorbedingung: mindestens ein numerisches Argument. Prüfe sie an der Aufrufstelle oder ergänze eine bewusste Fehlermeldung, bevor Du (next) als Startwert verwendest.
  6. (out "kurs-werte.l" (println '(3 5 7))); dann (setq Gelesen (in "kurs-werte.l" (read))); schließlich (apply + Gelesen) → 15.
  7. Für 3 ergibt der falsche Basisfall 12: Die Rückgaben sind 2, 2, 4, 12. trace zeigt den Startfehler am innersten Aufruf (fakultaet 0).
  8. (de nicht-negativ-oder-abbruch (N) (if (< N 0) (throw 'Ungueltig N) N)); (catch 'Ungueltig (nicht-negativ-oder-abbruch 0)) → 0 ohne Abbruch.
  9. Nach der Klassendefinition: (dm doppelpreis> () (* 2 (get This 'preis))); (setq ZweiterArtikel (new '(+Artikel) 'preis 4)); (send 'doppelpreis> MeinArtikel) → 18, (send 'doppelpreis> ZweiterArtikel) → 8.
  10. new ohne Angabe für ein externes Symbol erzeugt hier ein anonymes Objekt im Speicher; ein Objekt der Klasse +Artikel ist nicht automatisch eine Datenbankentität. commit schreibt Änderungen externer Symbole; rollback verwirft noch nicht festgeschriebene Änderungen.
  11. Erwartet wird tea. Die Regel für John braucht einen Wert @X, der sowohl wine als auch food mag; über Mary ist nur tea bekannt.
  12. (de teurer-als (Grenze Artikel) (filter '((Obj) (> (send 'preis> Obj) Grenze)) Artikel)) und (de namen (Artikel) (mapcar '((Obj) (get Obj 'name)) Artikel)). Für dauerhafte Daten müsstest Du entweder ein eigenes Dateiformat samt Laden/Speichern entwerfen oder das Schema und die Transaktionen der eingebauten Datenbank aufsetzen.

Weiterlernen und Quellen

Du kannst eigenständig weiterarbeiten, wenn Du Auswertung und dynamische Bindung erklären, Listenfunktionen zusammensetzen, mit pp, show und trace Fehler verfolgen und zwischen Dateien und persistenten Objekten unterscheiden kannst. Das Objektmodell, die Datenbank und Pilog sind jeweils eigene Felder für eine weitere Vertiefung.