Projekt (Prolog)Baut auf Katas: Rekursion aufIm Handbuch: Terminal: Dateien und Verzeichnisse
image: teaser.webp image_alt: "Ein Terminal, in dem SWI-Prolog läuft: die Anfragen nach den Großeltern von Wilhelm Friedemann Bach und nach allen Nachkommen von Johann Ambrosius Bach, mit den Listen der Namen, die Prolog findet" kind: projekt language: Prolog duration: 3 Doppelstunden curriculum: "Oberstufe: Vertiefung Applikative Programmierung (logische Programmierung: Fakten, Regeln, Anfragen, Rekursion, Listen, Backtracking)" topics: [programmiersprachen, rekursion, listen, funktional] requires: [kata-recursion] age: Oberstufe reference: [terminal]

Logisch programmieren mit Prolog

In Prolog schreibt man nicht auf, was der Computer der Reihe nach tun soll, sondern was wahr ist: Fakten über eine Familie und Regeln, was Großeltern, Geschwister und Vorfahren sind. Dann stellt man Fragen, und Prolog sucht die Antworten selbst. Du fragst die Musikerfamilie Bach über fünf Generationen aus, lässt Listen rückwärts rechnen und färbst die Bezirke Berlins mit vier Farben.

1. Was Prolog anders macht

Alain Colmerauer (1941–2017), französischer Informatiker, Professor in Marseille. Er wollte, dass ein Computer Fragen auf Französisch versteht und aus Sätzen wie »Katzen töten Mäuse« und »Tom ist eine Katze« selbst schließt, was Tom tut. Dafür schrieb er 1972 mit Philippe Roussel eine Sprache, in der ein Programm aus solchen Aussagen besteht: Prolog, von »programmation en logique«. Wie man logische Aussagen als Programm lesen kann, hatte Robert Kowalski in Edinburgh gezeigt, mit dem er 1971 und 1972 in Marseille zusammenarbeitete.

Das Foto zeigt ihn um 1970 in Montreal, in der Gruppe, mit der er an einem Programm zum Übersetzen zwischen Englisch und Französisch arbeitete; er hält gerade eine Ausgabe seiner Q-Systeme in der Hand, aus denen später Prolog wurde. Noch später baute er Prolog so aus, dass es auch mit Zahlen und Bedingungen rechnet, wie in einem Sudoku, das Prolog nicht durch Ausprobieren löst, sondern indem es unmögliche Zahlen streicht.

Ein Prolog-Programm besteht aus Fakten, die einfach gelten, und Regeln, die sagen, wann etwas gilt. Ein Programm läuft nicht von oben nach unten, sondern man stellt eine Anfrage, und Prolog sucht in den Fakten und Regeln, ob sie stimmt und für welche Werte.

2. Einrichten

Leg einen Ordner an, hol die Fakten über die Familie Bach und starte SWI-Prolog mit ihnen:

mkdir -p /workspace/prolog
cd /workspace/prolog
wget https://workspace.hackschule.de/prolog/starter/familie.pl
swipl familie.pl

Jetzt wartet Prolog hinter ?- auf eine Anfrage. Sie endet immer mit einem Punkt:

?- parent(johann_sebastian, wilhelm_friedemann).
true.

?- parent(johann_sebastian, X).
X = catharina_dorothea ;
X = wilhelm_friedemann ;
X = carl_philipp_emanuel .

Was klein beginnt, ist ein Name, ein Atom: johann_sebastian. Was groß beginnt, ist eine Variable: X. Gibt es mehrere Antworten, zeigt Prolog die erste und wartet: ; holt die nächste, Enter hört auf. Hast du familie.pl im Editor geändert, lädt make. sie neu; halt. beendet Prolog.

3. Fakten und Anfragen

Jede Zeile parent(johann_ambrosius, johann_sebastian). in familie.pl ist ein Fakt: Johann Ambrosius ist ein Elternteil von Johann Sebastian. Eine Anfrage mit einer Variablen fragt, wofür sie gilt, an jeder Stelle: parent(X, johann_sebastian) fragt nach den Eltern.

Vermutung 1Wer sind die Eltern?

Wie viele Antworten gibt Prolog auf ?- parent(X, johann_sebastian)., wenn du nach jeder mit ; nach der nächsten fragst?

Wer sind die Eltern?
Aufgabe 2Die Kinder von Anna Magdalena

In familie.pl fehlen die Kinder von Johann Sebastian und seiner zweiten Frau Anna Magdalena: gottfried_heinrich, elisabeth_juliane_friederica, johann_christoph_friedrich und johann_christian. Trag sie als Fakten ein, für beide Eltern.

Tipp

Ein Fakt sieht aus wie die anderen in der Datei: parent(anna_magdalena, johann_christian)., mit einem Punkt am Ende.

Nächster Tipp

Vier Kinder, zwei Eltern: acht neue Zeilen. Am besten stehen sie bei den anderen parent-Fakten.

4. Regeln

Eine Regel sagt, wann etwas gilt. :- liest man »wenn«, das Komma »und«:

son(S, P) :- parent(P, S), male(S).

»S ist ein Sohn von P, wenn P ein Elternteil von S ist und S männlich.« Eine Variable gilt in der ganzen Regel: Das S in male(S) ist dasselbe wie das in parent(P, S).

Aufgabe 3Vater, Mutter, Großeltern

Schreib drei Regeln in familie.pl: father(F, C), wenn F ein Elternteil von C und männlich ist, mother(M, C) ebenso mit weiblich, und grandparent(G, C), wenn G ein Elternteil eines Elternteils von C ist.

Tipp

Eine Regel hat einen Kopf und, nach :-, die Bedingungen, durch Komma getrennt: father(F, C) :- parent(F, C), male(F).

Nächster Tipp

Für die Großeltern gibt es dazwischen eine Person, die in beiden Bedingungen vorkommt: grandparent(G, C) :- parent(G, P), parent(P, C).

Vermutung 4Geschwister, erster Versuch

Jemand schreibt sibling(X, Y) :- parent(P, X), parent(P, Y).: Geschwister haben einen gemeinsamen Elternteil. Was antwortet Prolog auf ?- sibling(johann_sebastian, Y)., wenn man mit ; nach allen Antworten fragt?

Geschwister, erster Versuch

Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.

Johann Sebastian hat mit sich selbst einen gemeinsamen Elternteil, sogar zwei: Für P = johann_ambrosius passt Y = johann_sebastian, und für P = maria_elisabeth wieder. Jedes Geschwister kommt zweimal, einmal über den Vater und einmal über die Mutter. Prolog weiß nur, was dasteht, und nirgends steht, dass niemand sein eigenes Geschwister ist.

Aufgabe 5Geschwister

Schreib sibling(X, Y), das sagt, ob X und Y Geschwister sind: Sie haben einen gemeinsamen Elternteil und sind nicht dieselbe Person. Halbgeschwister zählen mit.

Tipp

Die Regel aus der Vorhersage ist fast richtig: parent(P, X), parent(P, Y) findet alle, die einen Elternteil mit X teilen, X selbst eingeschlossen.

Nächster Tipp

Eine dritte Bedingung schließt X selbst aus: X \= Y, verschieden.

5. Wie Prolog sucht

Wie findet Prolog die Großeltern von Wilhelm Friedemann? Es nimmt das erste Ziel, die Anfrage, und sucht von oben eine Klausel, deren Kopf dazu passt. Passen heißt: Die beiden lassen sich gleich machen, indem Variablen Werte bekommen. Das heißt Unifikation. Passt eine Regel, treten ihre Bedingungen an die Stelle des Ziels. Könnten weiter unten noch andere Klauseln passen, merkt sich Prolog die Stelle. Scheitert später ein Ziel, geht es zum letzten Merkpunkt zurück und probiert dort die nächste Klausel. Das heißt Backtracking.

Die Merkpunkte sind ein Stack: Prolog geht immer zum zuletzt gemerkten zurück. Darum findet es die Antworten in der Reihenfolge, in der die Fakten in der Datei stehen.

6. Rekursion

Ein Vorfahr ist ein Elternteil, oder ein Elternteil eines Vorfahren. Diese Regel benutzt sich selbst, wie eine rekursive Funktion. Auf die Reihenfolge kommt es dabei an:

Vermutung 6Zuerst die Rekursion
ancestor(A, D) :- ancestor(A, P), parent(P, D).
ancestor(A, D) :- parent(A, D).

Was passiert bei ?- ancestor(christoph, X).?

Zuerst die Rekursion

Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.

Für ancestor(christoph, X) nimmt Prolog die erste Klausel, und deren erstes Ziel ist wieder ancestor(christoph, P): Prolog nimmt die erste Klausel, und so fort, ohne je bei einem Fakt anzukommen. Jeder Aufruf kommt auf den Stack, bis der Speicher voll ist. Logisch ist die Regel richtig, aber Prolog sucht von links nach rechts und von oben nach unten. Erst einen Schritt gehen, dann weiterfragen: Das endet, weil die Familie endlich ist.

Aufgabe 7Vorfahren

Schreib ancestor(A, D): A ist ein Vorfahr von D, wenn A ein Elternteil von D ist, oder ein Elternteil eines Vorfahren von D, so weit zurück, wie die Familie reicht.

Tipp

Zwei Klauseln: eine für den einfachen Fall, ancestor(A, D) :- parent(A, D)., und eine für den Weg über eine Generation mehr.

Nächster Tipp

Die zweite geht erst einen Schritt hinunter und fragt dann weiter: ancestor(A, D) :- parent(A, P), ancestor(P, D). So kommt jede Rekursion eine Generation näher an D.

7. Listen

Eine Liste schreibt man [a, b, c]. Prolog sieht sie als Kopf und Rest: [a | [b, c]], und in einer Regel lässt sich beides auf einmal herausnehmen, [Kopf | Rest]. Die leere Liste ist []. Schreib die folgenden Prädikate in eine neue Datei:

cd /workspace/prolog
wget https://workspace.hackschule.de/prolog/starter/listen.pl
Aufgabe 8Ist es in der Liste?

Schreib in listen.pl das Prädikat is_in(X, List): X ist ein Element der Liste. Zwei Klauseln: X ist der Kopf der Liste, oder X ist im Rest.

Tipp

Die erste Klausel ist ein Fakt: is_in(X, [X | _]). Wo zweimal X steht, muss beide Male dasselbe stehen.

Nächster Tipp

Die zweite sieht im Rest nach: is_in(X, [_ | Rest]) :- is_in(X, Rest). Die leere Liste braucht keine Klausel: Dort ist nichts, und Prolog sagt false.

Aufgabe 9Wie lang ist die Liste?

Schreib size(List, N): N ist die Zahl der Elemente der Liste. Die leere Liste hat 0 Elemente, jede andere eines mehr als ihr Rest. Gerechnet wird mit is: N is M + 1.

Tipp

Ein Fakt für die leere Liste, size([], 0)., und eine Regel für eine Liste mit Kopf und Rest.

Nächster Tipp

Erst die Länge des Rests, dann eins mehr: size([_ | Rest], N) :- size(Rest, M), N is M + 1.

Aufgabe 10Zwei Listen hintereinander

Schreib joined(Xs, Ys, Zs): Zs ist die Liste Xs und dahinter Ys. Hängt man an die leere Liste etwas an, kommt das Angehängte heraus; sonst ist der Kopf von Xs auch der Kopf des Ergebnisses.

Tipp

Der Fakt: joined([], Ys, Ys). An eine leere Liste angehängt, bleibt Ys, wie es ist.

Nächster Tipp

Die Regel nimmt den Kopf mit: joined([X | Xs], Ys, [X | Zs]) :- joined(Xs, Ys, Zs).

Vermutung 11Rückwärts gefragt

Wie viele Antworten gibt ?- joined(Xs, Ys, [a, b, c]).?

Rückwärts gefragt

Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.

Vier: [] und [a, b, c], [a] und [b, c], [a, b] und [c], [a, b, c] und []. joined ist keine Funktion, die aus zwei Listen eine macht, sondern eine Beziehung zwischen drei Listen. Prolog kann jede Stelle fragen, die unbekannt ist. Dasselbe gilt für is_in: Mit einer Variablen an erster Stelle zählt es die Elemente auf.

8. Berlin in vier Farben

Jede Landkarte lässt sich mit vier Farben so färben, dass keine zwei Nachbarn dieselbe haben. Bewiesen haben das Kenneth Appel und Wolfgang Haken 1976, als einen der ersten großen Beweise mit Hilfe eines Computers. Hier färbt Prolog die zwölf Bezirke Berlins. Nachbarn sind Bezirke mit einer gemeinsamen Grenze:

Bezirk Variable Nachbarn
Charlottenburg-Wilmersdorf Cw Mitte, Reinickendorf, Spandau, Steglitz-Zehlendorf, Tempelhof-Schöneberg
Friedrichshain-Kreuzberg Fk Lichtenberg, Mitte, Neukölln, Pankow, Tempelhof-Schöneberg, Treptow-Köpenick
Lichtenberg Li Friedrichshain-Kreuzberg, Marzahn-Hellersdorf, Pankow, Treptow-Köpenick
Marzahn-Hellersdorf Mh Lichtenberg, Treptow-Köpenick
Mitte Mi Charlottenburg-Wilmersdorf, Friedrichshain-Kreuzberg, Pankow, Reinickendorf, Tempelhof-Schöneberg
Neukölln Nk Friedrichshain-Kreuzberg, Tempelhof-Schöneberg, Treptow-Köpenick
Pankow Pa Friedrichshain-Kreuzberg, Lichtenberg, Mitte, Reinickendorf
Reinickendorf Re Charlottenburg-Wilmersdorf, Mitte, Pankow, Spandau
Spandau Sp Charlottenburg-Wilmersdorf, Reinickendorf, Steglitz-Zehlendorf
Steglitz-Zehlendorf Sz Charlottenburg-Wilmersdorf, Spandau, Tempelhof-Schöneberg
Tempelhof-Schöneberg Ts Charlottenburg-Wilmersdorf, Friedrichshain-Kreuzberg, Mitte, Neukölln, Steglitz-Zehlendorf
Treptow-Köpenick Tk Friedrichshain-Kreuzberg, Lichtenberg, Marzahn-Hellersdorf, Neukölln

Du beschreibst nur, was gelten soll: Jeder Bezirk hat eine Farbe, und Nachbarn haben verschiedene. Wie man eine Färbung findet, überlegt sich Prolog selbst.

cd /workspace/prolog
wget https://workspace.hackschule.de/prolog/starter/bezirke.pl
Aufgabe 12Berlin in vier Farben

Schreib in bezirke.pl die Regel berlin(Cw, Fk, Li, Mh, Mi, Nk, Pa, Re, Sp, Sz, Ts, Tk), eine Variable für jeden Bezirk, wie in der Datei beschrieben. Jeder Bezirk bekommt eine der vier Farben, und keine zwei Nachbarn aus der Tabelle oben haben dieselbe.

Tipp

Jeder Bezirk bekommt seine Farbe mit colour(Mi). Zwei Nachbarn sind verschieden: Mi \= Pa. 24 Nachbarpaare stehen in der Tabelle.

Nächster Tipp

\= kann erst prüfen, wenn beide Seiten eine Farbe haben. Schreib jede Prüfung direkt hinter das zweite colour ihres Paares: Dann scheitert eine falsche Farbe sofort, statt erst, wenn alle zwölf gefärbt sind.

Ausprobieren: Schreib die Regel einmal so, dass sie erst alle zwölf Bezirke färbt und dann alle Nachbarpaare prüft, und frag mit time(aggregate_all(count, berlin(_, _, _, _, _, _, _, _, _, _, _, _), N)). nach der Zahl der Färbungen. Achte darauf, wie viele Schlüsse (»inferences«) Prolog jeweils braucht.

9. Ein Sudoku ohne Ausprobieren Extra

Ein Extra für Schnelle. Wer es auslässt, springt zu Wie geht es weiter?.

Prolog kann nicht nur ausprobieren. Mit library(clpfd) sagt man, welche Werte eine Variable haben darf und welche Bedingungen gelten, und Prolog streicht unmögliche Werte, bevor es rät. Ein Sudoku ist dann nur noch seine Regeln: jede Zelle eine Zahl von 1 bis 9, in jeder Zeile, jeder Spalte und jedem Block alle verschieden.

cd /workspace/prolog
wget https://workspace.hackschule.de/prolog/starter/sudoku.pl
Aufgabe 13Ein Sudoku ohne AusprobierenExtra

In sudoku.pl fehlen die Blöcke. Schreib blocks(Zeile1, Zeile2, Zeile3), das in drei Zeilen die drei Blöcke nebeneinander nimmt, die ersten drei Zahlen jeder Zeile, dann die nächsten drei und so weiter, und für jeden Block verlangt, dass seine neun Zahlen verschieden sind.

Tipp

Ein Fakt für drei leere Zeilen, blocks([], [], [])., und eine Regel, die von jeder Zeile drei Zahlen nimmt: blocks([N1, N2, N3 | Rest1], [N4, N5, N6 | Rest2], [N7, N8, N9 | Rest3]).

Nächster Tipp

Darin verlangt all_distinct([N1, N2, N3, N4, N5, N6, N7, N8, N9]) einen Block ohne doppelte Zahl, und blocks(Rest1, Rest2, Rest3) macht mit den nächsten drei Zahlen weiter.

10. Wie geht es weiter?

  • Grammatiken: Prolog wurde erfunden, um Sätze zu verstehen. Mit einer Grammatik in Prolog (DCG, -->) wird eine Grammatik wie die aus Grammatiken zu einem Programm, das Sätze prüft und erzeugt.
  • Wege: Ein Fakt für jede Verbindung zweier Bahnhöfe, und eine Regel für einen Weg, die sich merkt, wo sie schon war, damit sie nicht im Kreis läuft.
  • Verneinung: \+ parent(X, _) gilt, wenn Prolog keinen Beweis findet, nicht, wenn das Gegenteil bewiesen ist. Wer also nicht in der Datei steht, hat für Prolog keine Kinder.

11. Zum Nachschlagen

Programming in Prolog, William F. Clocksin und Christopher S. Mellish, 1981. Das Buch, mit dem eine Generation Prolog gelernt hat, von Fakten und Regeln bis zu Grammatiken.