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.
Dieser Teil baut das Auswertungsmodell und den Umgang mit Daten, Listen und Funktionen schrittweise auf. Sein Abschlussprojekt ist eine kleine Preisrechnung.
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.
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.
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.
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)).
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.
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.
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))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?
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.
(+ 1 (* 2 3)) → 7; (7 8 9) →
(7 8 9).(setq A 10): A → 10;
'A → A; '(A 2) →
(A 2); (1 2) → (1 2). Zitat
schützt auch das A in der Liste.(cons 'berg '(wald wiese)) →
(berg wald wiese); car davon →
berg; cdr davon →
(wald wiese).(de doppelt (N) (* 2 N)) und
(de plus-drei (N) (+ N 3));
(plus-drei (doppelt 4)) → 11.(summe NIL) → 0, (summe '(5))
→ 5. Eine Lösung für die Länge:
(de anzahl (Lst) (if Lst (+ 1 (anzahl (cdr Lst))) 0)).(de doppelt (N) (* 2 N)), dann
(mapcar doppelt (3 4 5)). Anonym:
(mapcar '((N) (* 2 N)) (3 4 5)). Jeweils
(6 8 10).21; innerhalb von let sieht
zeige den Wert 20. Nach Ende von
let wird Wert wieder zu 4.(roh A (+ 4 5)) → A;
(berechnet A (+ 4 5)) → 2. Der zweite Ausdruck
wird von beiden Funktionen nicht benutzt.(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.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.
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 8Die 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?
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.
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)) # NILBei == 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) # wahrPicoLisp 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?
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)) # 26Als 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.
@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.
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.
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.
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 ) # 17Ein 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 ) # 0Auch 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 7Das 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.
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) # 9This 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.
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?
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?
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)) # 13Lies 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).
(setq Praefix 'alt);
(de anzeige (Wort) (list Praefix Wort)); im
let entsteht (neu wald), danach ist
Praefix wieder alt.(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.if-Ausdrücke ergeben nein. Eine
Zahl, auch 0, ist in PicoLisp wahr; teste beispielsweise
mit (> Anzahl 0).(apply + (mapcar '((N) (* N N)) (filter '((N) (> N 4)) (2 7 4 9))))
→ 130 (49 + 81).(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.(out "kurs-werte.l" (println '(3 5 7))); dann
(setq Gelesen (in "kurs-werte.l" (read))); schließlich
(apply + Gelesen) → 15.3 ergibt der falsche Basisfall 12: Die
Rückgaben sind 2, 2, 4,
12. trace zeigt den Startfehler am innersten
Aufruf (fakultaet 0).(de nicht-negativ-oder-abbruch (N) (if (< N 0) (throw 'Ungueltig N) N));
(catch 'Ungueltig (nicht-negativ-oder-abbruch 0)) →
0 ohne Abbruch.(dm doppelpreis> () (* 2 (get This 'preis)));
(setq ZweiterArtikel (new '(+Artikel) 'preis 4));
(send 'doppelpreis> MeinArtikel) → 18,
(send 'doppelpreis> ZweiterArtikel) →
8.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.tea. Die Regel für John
braucht einen Wert @X, der sowohl wine als
auch food mag; über Mary ist nur tea
bekannt.(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.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.
PicoLisp Reference: Auswertung, Datentypen, Vergleiche, Objekte, Datenbank und Pilog.
PicoLisp Tutorial: Funktionen, Entwicklungswerkzeuge, Ein- und Ausgabe, Skripte.
PicoLisp FAQ: Unterschiede zu anderen Lisps und dynamische Bindung.
Alphabetische Funktionsreferenz: Details einzelner Funktionen; prüfe für abweichende Installationen die zugehörige lokale Referenz.