www.r-krell.de
Webangebot für Schule und Unterricht, Software, Fotovoltaik und mehr

Willkommen/Übersicht  >  Informatik  >   Prolog-Seite

Informatik mit Prolog

Kurzer Abriss über den Einsatz von Prolog (Edinburgh-Standard)
als alternative Programmiersprache im SII-Unterricht

Diese Seite kann durchaus auch als Ergänzung zu meinen Seiten „Informatik mit Java" angesehen werden. Insbesondere gibt es Bezüge zu den Wegsuchen in Graphen auf der Seite „Informatik mit Java, Teil g)" und Bezüge zu den Kellern auf der Seite "Informatik mit Java, Teil e)". Tatsächlich empfiehlt der SII-Lehrplan neben der Arbeit mit imperativen, objektorientierten Programmierkonzepten (wie sie z.B. mit Java verwirklicht werden) auch die Einführung in ein alternatives Programmier-Paradigma. Das „Programmmieren mit Logik" mit Prolog ist eine solche sinnvolle und lohnenswerte Alternative.

Auf dieser Seite finden Sie:PROLOG = PROgramming in LOGic

sowie

Eine vollständige Übersicht aller meiner Java-Seiten gibt's auf der Informatik-Hauptseite!


zum Seitenanfang / zum Seitenende

Prolog-Konzept

Prolog ist nicht mit herkömmlichen Programmiersprachen zu vergleichen: Es gibt keine Zuweisungen, keine Kontrollstrukturen, keine verschiedenen Datentypen (mit Ausnahme der Prolog-Liste). Alles, was man kennt und normalerweise für nötig hält, gibt es nicht. Vielmehr wurde Prolog entwickelt, um Leuten das Erstellen von Expertensystemen zu ermöglichen, die nicht programmieren können und normale Programmiersprachen auch nicht erlernen wollen. Dem Computer sollen der normalen menschlichen Logik entsprechend Fakten und Regeln (Oberbegriff: Klauseln) mitgeteilt werden. Um eine Ausgabe zu erhalten, müssen geeignete Fragen an das System gestellt werden. Wie das System dabei zu einer richtigen Antwort kommt, muss -- so zumindest die Idee -- nicht die Sorge des Anwenders sein: schließlich hat er einen intelligenten Computer vor sich, der aufgrund der erworbenen/eingegebenen Wissenbasis selbstständig die richtige Lösung finden soll. Der Entwurf von Algorithmen oder Klassenhierarchien ist für Prolog also völlig unnötig!


zum Seitenanfang / zum Seitenende

1. Familien-Stammbaum

Fakten

In vielen Prolog-Büchern wird das Konzept mit einem Stammbaum illustriert. Angesichts vieler „Patchwork"-Familien heute vielleicht nicht mehr die Regel, aber noch vorstellbar: hier sind die Eltern von Kindern miteinander verheiratet und alle Ehepaare haben auch gemeinsame Kinder!

Grafik: Familienstammbaum
Um dem Computer bzw. dem Prolog-System diesen Stammbaum bekannt zu machen, kann er in Form von Fakten-Klauseln eingegeben werden, die -- das ist in Prolog Bedingung -- ebenso wie Konstanten mit einem Kleinbuchstaben anfangen (mit Großbuchstaben fangen hingegen die Variablen an, die gleich in den Fragen und später auch in Regeln auftauchen werden). Die Namen der Klauseln sind frei wählbar, dürfen aber keine Umlaute enthalten. Hier sollen Klauseln der Gestalt „kindVon (klaus, monika)." dem System mitteilen, dass Klaus ein Kind von Monika ist. Natürlich hätte man das auch mit einer Klausel namens „xyz (klaus, monika)." machen können, dann wird aber das Prolog-Programm für den Menschen schwerer lesbar. Im hier verwendeten fix-Prolog dürfen zwischen dem Klauselnamen, den Klammern, Kommas und Konstanten Leerzeichen stehen (und müssen auf jeden Fall um das Subtraktions-Zeichen stehen, um es vom negativen Vorzeichen zu unterscheiden). Wer die Beispiele in anderen Systemen ausprobieren will, etwa in SWI-Prolog oder in tuProlog, muss die Leerzeichen aber entfernen bzw. eweglassen, weil sie dort unzulässig sind. Alle Klauseln müssen immer mit einem Punkt enden. Sind die Fakten fertig eingegeben und in einer Textdatei gespeichert, können sie mit reconsult bzw. der Schaltfläche „Datenbasis erneuern" dem fix-Prolog-System mitgeteilt werden. In tuProlog kennt das System die eingegebene Datenbasis (hier "Theory" genannt) automatisch. Danach kann das System eingetippte Fragen beantworten. Einige Beispiele werden nachfolgend gezeigt:
Bildschirmfoto fix-Prolog mit Stammbaum-Klauseln und Fragen
bzw. das gleiche Beispiel im Februar 2014 in tuProlog für Java, Version 2.7.2:
Bildschirmfoto: Stammbaum in tuProlog

Fragen

Bei den Fragen gibt es verschiedene Möglichkeiten: Man kann ohne Variablen beispielsweise „?- kindVon (caudia, petra)." fragen und erhält als Antwort ein Yes, weil Prolog eine passende Klausel in seiner Datenbasis findet. Hingegen ist Claudia kein Kind von Sylvia -- jedenfalls findet Prolog nichts dergleichen und antwortet mit No. Enthält die Frage Variablen (die mit Großbuchstaben beginnen), so antwortet Prolog mit einer möglichen Variablenbelegung -- und gibt nach „Weitersuchen? Ja" -- auch noch jeweils eine weitere erlaubte Möglichkeit aus. So werden z.B. für E nacheinander beide Elternteile von Claudia gefunden (nochmal „Weitersuchen? Ja" hätte dann aber die Antwort No zur Folge, weil es keinen dritten Elternteil gibt) oder auch sämtliche Kinder von Klaus. Gibt es nur Variablen, gibt Prolog nacheinander alle zueinander passenden Kinder (K) und Elternteile (E) der Datenbasis aus. Die Reihenfolge von Klauseln in der Datenbasis ist zwar oft egal, wir merken aber, dass Prolog in genau dieser Reihenfolge nach Lösungen bzw. Antworten für gestellte Fragen sucht.

Regeln (und weitere Fragen)

Spannend ist die Erweiterung des Stammbaums um Regeln: Will man z.B. wissen, wer mit wem verheiratet ist, könnte man dem Computer das durch Fakten wie „verheiratet (monika, ulrich)." beibringen. Hier kann man aber auch ausnutzen, dass im Beispiel alle Paare Kinder haben, und zwei Personen P1 und P2 offenbar genau dann verheiratet sind, wenn sie (mindestens ein) gemeinsames Kind K haben:

  verheiratet (P1, P2)
  :- kindVon (K, P1),
     kindVon (K, P2).

Das Komma am Ende der zweiten Zeile wirkt als logische Und-Verknüpfung: die beiden letzten Zeilen müssen gleichzeitig erfüllt werden. Und weil in den beiden letzten Zeilen der gleiche Variablenname K vorkommt, muss es sich auch in beiden Fällen um das gleiche Kind handeln. Das ist wichtig, weil nur mit solchen gleichen Variablennamen Zusammenhänge hergestellt werden können. Das erinnert an die QBE-Fragetechnik bei Datenbanken (d.h. z.B. den 'query-by-example'-Abfragen in Paradox-Datenbanken), wo Verknüpfungen zwischen verschiedenen Tabellen über gleichnamige Variablen -- dort Beispielelemente genannt -- hergestellt wurden.

In ähnlicher Weise kann man Regeln für die Enkel-Großelternteil-Beziehung, für Geschwister und viele weitere Verwandtschaftsverhältnisse aufstellen: indem im Rumpf der Regel (nach dem „:-"-Zeichen) eine oder mehrere nach außen nicht sichtbare [d.h. im Kopf (vor dem „:-"-Zeichen) nicht verwendete und in der Antwort nicht ausgegebene] Variable mehrfach vorkommt/vorkommen.

Möchte man Elternteile nach Mutter und Vater unterscheiden, muss man dem System zumindest einmal alle Vertreter/Vertreterinnen eines Geschlechts durch Fakten mitteilen, d.h. z.B. „weibl (monika).  weibl (petra).  weibl (sylvia)." usw. aufführen. Das andere Geschlecht könnte dann per Regel aus allen Personen bestehen, die nicht weiblich sind, also

  maennl (P)
  :- not weibl (P).

Achtung: Durch entsprechende Fragen stellt ma n schnell fest, dass jetzt nicht nur die oben aufgeführten Männer, sondern auch der dort unbekannte Hans-Peter („?- maennl (hanspeter)." => Yes), aber auch Uschi oder 783 angeblich männlich sind! Jedenfalls beantwortet Prolog z.B. die Frage „?- maennl (783)." mit einem klaren Yes, weil 783 eben nicht in der Datenbasis von weiblich steht, d.h. es kein Faktum der Form „weibl (783)." gibt! Bei der nachfolgenden Regel

  vaterVon (V, K)
  :- kindVon (K, V),
     maennl (V).

stört das allerdings nicht, denn durch die „kindVon"-Klausel im Rumpf werden nur Personen für V ausprobiert, die in einer „kindVon"-Klausel an zweiter Stelle stehen (also auch im Stammbaum vorkommen). Man könnte sich also bequem alle Väter ausgeben lassen -- wenn die Kindernamen dabei gar nicht interessieren, fragt man mit der anonymen Variablen „_" und erhält auf „?- vaterVon (Papa, _)" mit Weitersuchen alle Namen von Vätern als mögliche Belegungen der Variablen Papa ausgegeben (zugehörige Lösungen für die anonyme Variable werden nicht gezeigt -- sie kann aber auch durch Mehrfachgebrauch innerhalb von Regeln oder Fragen keine Verbindung herstellen). Ohne Weiteres lassen sich übrigens durch die Frage „?- weibl (F)." alle eingegebenen Frauen nacheinander als Antworten erhalten, während die Frage „?- maennl (M)." den Computer ratlos lässt: Angesichts der unendlich vielen, für ihn unbekannten Gegenstände des Universums, die nicht als Namen in den paar weiblich-Regeln auftauchen, weiß Prolog nicht, was es alles für M einsetzten könnte oder soll und antwortet daher mit einem schlichten No.



Eine rekursive Regel (im Vorgriff auf Kommendes)

Wer will, kann übrigens auch den Stammbaum noch etwas erweitern und schon hier (statt erst in den Kapiteln 4 und 5, s.u.) ein rekursives Prädikat einführen. Angenommen, es gäbe außer den abgebildeten drei Generationen fünf oder mehr Generationen im Stammbaum, und man würde sich dafür interessieren, wer Nachfahre (Abkömmling) von wem ist. Eine naheliegende und durchaus richtige, wenn auch etwas umständliche Lösung wäre:

  nachfahreVon (Jung, Alt)         /* Kinder sind Nachfahren */
  :- kindVon (Jung, Alt).       

  nachfahreVon (Jung, Alt)         /* Enkel sind Nachfahren: .. */ 
  :- kindVon (Jung, Zwischen),     /* .. Jung = Enkel, Zwischen = Elternteil, der.. */
     kindVon (Zwischen, Alt).      /* .. seinerseits Kind vom Großelternteil Alt ist */

  nachfahreVon (Jung, Alt)         /* Urenkel sind Nachfahren .. */
  :- kindVon (Jung, Z1),           /* .. über zwei Zwischen-Generationen Z1 und Z2 */ 
     kindVon (Z1, Z2),
     kindVon (Z2, Alt).

  ...                              /* weitere Regeln für Ururenkel, Urururenkel,... */

Vorstehend muss man für jede weitere Generation eine zusätzliche Regel angeben. Der Gedanke, z.B. bei der dritten Nachfahren-Regel (für Urenkel) eine Zeile Schreibarbeit zu sparen, indem man statt 2 mal „kindVon" einmal die Nachfahren-Regel für Enkel benutzt, könnte weiter gedacht werden: Schon bei der Nachfahren-Regel für Enkel könnte man statt einer Kind-Regel „kindVon (Zwischen, Alt)" die erste Nachfahren-Regel benutzen, die ja die Kind-Beziehung beinhaltet:

  nachfahreVon (Jung, Alt)         /* Kinder sind direkte Nachfahren */
  :- kindVon (Jung, Alt).          /* Erste, einfache Regel unverändert wie oben */

  nachfahreVon (Jung, Alt)         /* weitere, indirekte Nachfahren */     
  :- kindVon (Jung, Zwischen), 
     nachfahreVon (Zwischen, Alt). /* rekursiver Aufruf */

Damit werden aber alle weiteren Regeln überflüssig. Denn im rekursiven Aufruf in der letzten Zeile kann nicht nur die erste Nachfahren-Regel verwendet werden: Prolog setzt in die letzte Zeile auch die zweite Nachfahren-Regel selbst ein (die ja genauso heißt), so dass der rekursive Aufruf auch Enkelkinder beinhalten kann (und somit die zweite Regel insgesamt Urenkel liefert). Kann die zweite Regel aber Urenkel liefern und wird in der letzten Zeile verwendet, so kann die zweite Regel als Ganzes auch Ururenkel beschreiben. Und wenn die zweite Regel Ururenkel erzeugt und wieder in die letzte Zeile eingesetzt werden kann, dann... usw.: Die Rekursion kann also, ausgehend von einem einfachen Start (der ersten Nachfahren-Regel für die direkten Nachfahren, die Kinder) und einer zweiten, sich selbst aufrufenden Regel (rekursiver Aufruf in der letzten Zeile zusammen mit dem „kindVon"-Aufruf in der vorletzten Zeile, der immer eine Generation dazu fügt) beliebig viele Generationen überspannen, und ist daher universeller und auch noch schneller programmiert als die erste Lösung mit eigenen Regeln für jede neue Generation. Weitere Rekursionen begegnen uns unten in den Kapiteln 4 und 5 -- zunächst wird's aber wieder etwas anschaulicher:


zum Seitenanfang / zum Seitenende

2. Datenbank Zugfahrplan



Aufgaben „Prolog am Bahnhof":

Lösungen zu den vorstehenden Aufgaben:

/* Prolog am Bahnhof   www.r-krell.de */

/* Aufgabe 1 */
   endstation (ic721, hamburg).
   endstation (re4567, essen).
   endstation (re457, koeln).

/* Aufgabe 2 */
   abfahrt (ic721, 8, 15).
   abfahrt (re4567, 8, 21).
   abfahrt (re457, 9, 56).

/* Aufgabe 3 */
   gleis (ic721, 18).
   gleis (re4567, 14).
   gleis (re457, 7).

/* Aufgabe 4 */
   gleisZiel (Gleis, Ziel)
   :- gleis (Zug, Gleis),
      endstation (Zug, Ziel).

/* Aufgabe 5 */
   zielZeit (Ziel, Std, Min)
   :- endstation (Zug, Ziel),
      abfahrt (Zug, Std, Min).

/* Aufgabe 6 */
   eintrag (Std, Min, Zug, Ziel, Gleis)
   :- abfahrt (Zug, Std, Min),
      endstation (Zug, Ziel), 
      gleis (Zug, Gleis).

Hier wurde nochmal demonstriert, dass Verknüpfungen verschiedener Klauseln durch Variable geschehen. (In Kapitel 1 wurde schon die ähnliche QBE-Verknüpfung von Datenbanktabellen erwähnt.)


zum Seitenanfang / zum Seitenende

3. Kombinationsrätsel

Wie schon in den Kapiteln 1 und 2 gesehen, probiert Prolog automatisch oder nach „Weitersuchen? Ja" sämtliche Möglichkeiten durch, die die Datenbasis bereit stellt. Diese Eigenschaft kann benutzt werden, um Rätsel „durch Probieren" zu lösen. Zu beachten ist allerdings, dass man die Bedingungen möglichst früh anbringt (also ein Probieren nach der 'branch-&-bound'-Strategie durchführt) -- so hat es keinen Sinn, im nachfolgenden Rätsel auch noch die Autos zwei bis vier zu besetzen, wenn schon im ersten Auto eine falsche Person sitzt: Man kommt dadurch nicht mehr zu einer gültigen Gesamtlösung und vergeudet nur unnötig Speicherplatz und Rechenzeit. Die schlechtere 'brute-force'-Strategie würde jeweils erst eine vollständige Kombination aufstellen (d.h. hier alle vier Autos z.B. beim ersten Versuch alle mit Anette besetzten) und dann prüfen, ob diese Kombination zulässig ist.

Die Kinder Anette, Bernd, Claudia und Dieter fahren auf der Kirmes Auto-Scooter. Jedes Kind sitzt allein in einem eigenen Wagen. Die Autos sind rot, grün, gelb und blau. Anette sitzt nicht im roten Auto. Im grünen Auto sitzt ein Junge. Das Kind im blauen Auto ist die Schwester von Bernd. Dieter fährt im gelben Wagen. Schreibe ein Prolog-Programm, das auf die Frage
   ?- loese (Rot, Gruen, Gelb, Blau).
z.B. Rot = claudia, Gruen = bernd, Gelb = dieter und Blau = anette antwortet
!

Durch die Aufgabenstellung wird schon ein Lösungs-Ansatz nahegelegt: Offenbar müssen Prolog die beteiligten Personen bekannt gegeben werden, damit das System dann für die (mit Großbuchstaben beginnenden) Variablen Rot, Gruen, Gelb und Blau diese Personen einsetzen kann, d.h. ausprobieren kann, ob z.B. Anette (bzw. in Prolog: anette) im roten Auto sitzen darf. Wenn/da dies laut Rätsel nicht erlaubt ist, brauchen die anderen Autos gar nicht erst besetzt werden, sondern das System soll für das rote Auto eine andere Person ausprobieren. Erst wenn hier ein erlaubter Insasse gefunden ist, wird -- gemäß der branch-&-bound-Strategie -- das grüne Auto besetzt, wobei die Person im roten Auto natürlich nicht noch gleichzeitig im grünen Auto sitzen kann. Ist auch das grüne Auto erlaubt besetzt, kann man das gelbe und schließlich das blaue Auto besetzen. Das folgende Prolog-Programm macht dies:

/* Auto-Scooter-Rätsel   --   fix-Prolog   --   www.r-krell.de */

person (anette).
person (bernd).
person (claudia).
person (dieter).

loese (Rot, Gruen, Gelb, Blau)
:- person (Rot), 
/* Prolog kann damit für Rot die 4 Personen durchprobieren! */
     Rot \= anette, 
/* Anette darf nicht im roten Auto sitzen */
   person (Gruen), Gruen \= Rot,
  /* auch im grünen Auto eine der 4 Personen, aber eine andere als in rot */
     Gruen \= anette, Gruen \= claudia, 
/* kein Mädchen, sondern ein Junge im grünen Auto! */ 
   Gelb = dieter, Gelb \= Rot, Gelb \= Gruen,
/* Dieter steht fest, darf aber nicht schon in früherem Auto sitzen */ 
   person (Blau), Blau \= Rot, Blau \= bernd, Blau \= dieter.
     /* die Schwester ist kein Junge. Deswegen braucht auch nicht noch Blau \= Gruen oder Blau \= dieter stehen! */

Prolog findet dann genau die oben schon angegebene Lösung. Auf „Weitersuchen? Ja" folgt No, d.h. es gibt keine weitere Lösung dieses Rätsels. Das „\="-Zeichen ist übrigens das Prolog-Ungleichheitszeichen; offenbar hat hier jede Programmiersprache ihre eigene Syntax („!=", „<>", ...).


zum Seitenanfang / zum Seitenende

4. Prolog-Listen und deren rekursive Bearbeitung

Bisher hatten wir nur mit einzelnen Variablen zu tun, die ohne jede Typddeklaration Konstanten wie anette oder hamburg oder auch (ganze) Zahlen (wie Gleisnummer, Stunden- oder Minutenzahl im Kapitel 2) aufnehmen konnten. Durch Aneinanderreihung mehrerer Variablen in einer Klausel (wie etwa bei eintrag im Kapitel 2, Aufgabe 6) konnte eine Art Klasse bzw. Objekt (oder ein Record für Pascal-Programmierer) nachgebildet werden. Wie beim Abriss über das Prolog-Konzept bereits angedeutet, lassen sich in Prolog aber auch beliebige Inhalte noch zu einer (Prolog-)Liste zusammenfassen.
Dies geschieht einfach dadurch, dass man die Elemente in eckige Klammern setzt. So sind [a, 4, 6+9, „hallo", du] oder [a1, b2] oder auch [ ] Beispiele für Listen, wobei [ ] die leere Liste darstellt. Natürlich können Listen auch wiederum Listen als Elemente enthalten, wie etwa die dreielementige Liste [ 2, [24, 7, [xyz], 1], plus], deren zweites Element selbst eine Liste ist (die ihrerseits als drittes Element eine einelementige Liste mit dem Inhalt xyz enthält).

Listentrenner „|"

Als einzige Möglichkeit, auf einzelne Elemente einer Liste zuzugreifen, gibt es in Prolog den Listentrenner „|", der eine Liste in ein (oder mehrere) Element(e) vor dem Trennstrich und in eine Liste mit dem Rest zerlegt. Das bedeutet, Elemente können immer nur vorne aus einer Liste heraus genommen werden. Will man ein hinteres Element isolieren, muss man zunächst alle vorangehenden Elemente mit dem Listentrenner entfernen, bis das gesuchte Element vorne in der Restliste steht und damit zugänglich ist. Da man die Prologprädikate in beliebiger Richtung benutzen kann, lassen sich umgekehrt einzelne Elemente mit dem Listentrenner auch vorne hinzufügen wie im nachfolgenden Prädikat test3 (vgl. auch Kapitel 1, wo kindVon zum Auffinden eines Kindes ebenso wie zum Feststellen des Vaters verwendet werden konnte, je nach dem, welcher der beiden „Parameter" eine Variable war). Hier also zunächst einige Übungen für den Gebrauch des Listentrenners:

beispiel ([a, 4, 6+9, 'Hallo?', du]). /* Beispielliste für Tests */

test1 (A, B)
:- beispiel (Liste), /* oder statt dieser 2 Zeilen kurz.. */
   Liste = [A|B].    /* .. nur :- beispiel ([A|B]). */

test2 (X, Y, Rest)
:- beispiel ([X, Y | Rest]).

test3 (Element, NeueListe)
:- beispiel (AlteListe),
   NeueListe = [Element | AlteListe].

Die verschiedenen Klauseln führen zu folgenden Ergebnissen:

Einige rekursive Listen-Prädikate

Wie gerade gesehen, ist der Zugriff aufs erste Element leicht (vgl. test1). Das letzte Element einer Liste kann hingegen nur rekursiv gewonnen werden. Es ist erreicht, wenn die Liste nur noch ein Element enthält, d.h. die Form [ LetztesElement ] hat. Hat die Liste hingegen mehr als ein Element, so müssen die vorderen sukzessive einzeln abgeschnitten werden. Da die abgeschnittenen Elemente nicht gebraucht werden, wäre auch eine anonyme Variable „_" dafür möglich. Das letzte Element jeder Liste ist natürlich das letzte Element der Restliste, wie man wieder an der gleichen Variablen L sieht. Würde man übrigens versuchen, das letzte Element einer leeren Liste zu ermitteln, erhält man als Antwort No: Die leere Liste kommt in den Klauseln letztesElement nicht vor!

In ähnlicher Weise können rekursiv die Elemente gezählt werden. Als einfachster Fall und damit gleichzeitig als Rekursionsende bietet sich das Zählen der Elemente einer leeren Liste an, die keine bzw. 0 Elemente enthält (Man könnte auch mit einer einelementigen Liste und der Anzahl 1 anfangen, erhält dann aber für die leere Liste die Anzahl No statt 0). Jede längere Liste enthält jeweils ein Element mehr als ihre Restliste, sodass durch Addition von 1 zur Mächtigkeit der Restliste die jeweilige Elementzahl erhalten wird. Auch hier muss das Rekursionsende vor der anderen gleichnamigen Regel stehen, weil Prolog die Datenbasis immer von oben abarbeitet und zuerst auf das Rekursionsende treffen muss, um nicht in eine Endlosrekursion zu verfallen. Und das Schlüsselwort „is" führt die Berechnung durch, weil in Prolog etwa „7 = 3 + 4" nicht gilt, weil links eine Zahl, aber rechts ein Term steht. „7 is 3+4" ist hingegen richtig, weil der Term rechts ausgewertet wird und dann sein Ergebnis 7 mit dem linken Ausdruck 7 verglichen wird. Apropos Rechnung: Es ist wichtig, verschiedene Variablen zu verwenden und sich nicht an die Zuweisung imperativer Sprachen anzulehnen, die „z = z + 1" erlauben -- ein solche oder auch die Aussage „Z is Z + 1" wäre in Prolog auf jeden Fall falsch, weil rechts vom Gleichheitszeichen 1 mehr steht als links, d.h. „=" oder „is" nie im mathematischen Sinne gelten kann. In Prolog wird aber logisch-mathematisch programmiert, und das Gleichheitszeichen wird nicht als Zuweisungszeichen wie in imperativen Sprachen missbraucht bzw. verwendet.

Im dritten nachfolgenden Beispiel geht es um die größte Zahl innerhalb einer Liste aus ganzen Zahlen. Einfach, direkt lösbar und damit als Rekursionsende geeignet ist wieder der Fall einer einelementigen Liste: die einzige Zahl ist gleichzeitig auch die größte. Bei längeren Listen muss das vordere Element abgeschnitten werden, um rekursiv zum einfachen Fall zu kommen. Jetzt ist aber nicht automatisch das Maximum der langen Liste gleich dem Maximum der Restliste: War das abgeschnittene Element größer als das Restmaximum, so ist statt dessen das abgeschnittene Element das Gesamtlistenmaximum. Entsprechend gibt's also zwei Regeln für das Abschneiden des ersten Elements!

Im vierten Prädikat soll überprüft werden, ob ein bestimmtes Element (mindesten einmal) in der Liste vorkommt. Taucht es irgendwann vorne (d.h. an der zugänglichen Stelle) in der Prolog-Liste auf, ist die Antwort Yes (egal, wie der Rest der Liste aussieht, für den daher hier die anonyme Variable eingesetzt werden konnte). Sonst kann man versuchen, rekursiv durch Abschneiden vorderer Elemente zu diesem sofort lösbaren Fall zu kommen. Möglicherweise gibt es aber irgendwann nichts mehr abzuschneiden, weil die Liste leer ist und man alle Elemente überprüft hat: Dann findet Prolog keine Regel „istElement" mehr, in der eine leere Liste vorkommt, und antwortet mit No.

Zum Schluss ein heiteres Beispiel: Eine Liste soll als lustig gelten, wenn sie mindestens dreimal die Konstante „ha" enthält. Es ist eine Klausel nötig, um die Lacher zu zählen. Und in einer weiteren Klausel wird entschieden, ob die Liste lustig ist. Dabei kann „mindestens 3" wahlweise mit „L > 2" oder mit „L >= 3" übersetzt werden. Gibt es weniger als drei Lacher, ist das Ergebnis No -- z.B. auf die Frage „?- heiterkeit ([a, 4, 6+9, 'Hallo?', du], Stimmung).", wo No erscheint (weil eben nicht Stimmung = lustig geantwortet werden kann).

Hier jetzt die beschriebenen Prädikate im Prolog-Text:

/* Einige rekursive Listenprädikate wie vorstehend erläutert - fix-Prolog - www.r-krell.de */

letztesElement ([L], L).      /* Das einzige Element ist auch das letzte */
letztesElement ([X|Rest], L)
:- letztesElement (Rest, L).  /* Letztes Element der Liste ist letztes El. vom Rest */

elementZahl ([], 0).          /* Die leere Liste hat 0 Elemente */
elementZahl ([_|Rest], Z)
:- elementZahl (Rest, ZR),    /* Die Liste mit anonym abgeschnittenem vorderen Element ..*/
   Z is ZR + 1.               /* .. hat ein Element mehr als die Restliste */

maximum ([M], M).             /* Die einzige Zahl ist auch die größte */
maximum ([X|Rest], MR)        /* Ist das vordere Element X kleiner als das Maximum MR ..*/
:- maximum (Rest, MR),        /* .. der Restliste, so ist MR das Gesamtmaximum */
   X < MR.
maximum ([X|Rest], X)         /* Ist hingegen X größer als das Restmaximum MR, .. */
:- maximum (Rest, MR),        /* .. so ist X das Gesamtmaximum */
   X >= MR.

istElement (El, [El|_]).      /* Steht das gesuchte El vorne in der Liste, ist alles klar */
istElement (El, [X|Rest])     
:- X \= El,                   /* Steht vorne in der Liste mit X etwas anderes als El, ..*/
   istElement (El, Rest).     /* .. dann muss man El im Rest suchen */

lachZahl ([], 0).             /* In einer leeren Liste gibt's keine Lacher */
lachZahl ([ha|Rest], L)       /* Jedes ha wird gezählt, d.h. die Liste mit vorderem ha ..*/
:- lachZahl (Rest, LR),       /* .. hat einen Lacher mehr als die Restliste */
   L is LR + 1.
lachZahl ([X|Rest], LR)       /* Jedes andere Element X zählt nicht, d.h. die Lachzahl ..*/
:- X \= ha,                   /* .. LR bleibt unverändert und wird als Ergebnis .. */
   lachZahl (Rest, LR).       /* .. nach oben durch gereicht! */
heiterkeit (Liste, lustig)    /* Nur Listen mit der Lachzahl L >= 3 sind lustig. */  
:- lachZahl (Liste, L),       /* Für andere Listen gibt's keine Heiterkeits-Klausel ..*/
   L >= 3.                    /* und damit das Ergebnis No */

Dass durch den Listentrenner leicht das erste Element einer Liste abgetrennt werden kann, während die hinteren Elemente der Liste nur schwerer zugänglich sind, erinnert an den abstrakten Datentyp Keller (Stack), wie er auf meiner Java-Seite e) oder im Programmtext des Java-Applets zum Zentralabi 2014 beschrieben wird. Die im folgenden Bild gezeigten Kommentare sollen dazu anregen, den Keller in Prolog zu realisieren, sodass z.B. auch die gezeigte Frage zum gewünschten Erfolg führt:

Bildschirmfoto: Keller-Programm in tuProlog

In tuProlog bedeutet "K0 / []", dass die Variable K0 mit dem leeren Keller [ ] identifiziert wurde. In fix-Prolog würde K0 = [ ] angezeigt.


zum Seitenanfang / zum Seitenende

5. Wegsuche in Graphen und für Rätsel oder Spiele

Inzwischen wissen wir genug über Prolog, um damit die Wegsuche (und zwar die Tiefensuche) in einem Graphen realisieren zu können. Der Graph selbst kann durch die explizite Angabe aller Kanten mitgeteilt werden. So kann etwa der Graph, der auf meiner Seite „Informatik mit Java, Teil g) Graphen und Wegsuchen" als erstes abgebildet ist, unter Verzicht auf die Kantengewichte einfach wie folgt dargestellt werden -- Adjazenzmatrizen oder -listen sind nicht nötig!

/* Graphdarstellung und Wegsuche (Tiefensuche) in Prolog -- www.r-krell.de */

kante (0, 1).     /* Alle neun Kanten aufzählen */
kante (0, 4).
kante (0, 5).
kante (1, 2).
kante (1, 5).
kante (2, 3).
kante (2, 4).
kante (3, 4).
kante (4, 5).

kante (K1, K2)     /* bidirektionaler Graph: Kanten gehen in beide Richtungen */
:- K1 > K2,        /* Um das Erzeugen bereits oben angegebener Kanten durch Rekursion */
   kante (K2, K1). /* .. zu verhindern, wurde hier zusätzlich K1 > K2 gefordert */

...

Für die Wegsuche muss man bekanntlich (wie auf meinen Java-Seiten bei der Tiefensuche erläutert) eine Besuchtliste führen, um nicht in Zyklen zu geraten und Wegstücke mehrfach zu durchlaufen. Zunächst kommt nur der Startknoten in die Besuchtliste. Ist man mit nicht schon am Ziel, so betritt man vom gerade aktuellen Knoten den nächsten unbesuchten Nachbarn und sucht von dort nach dem gleichen Verfahren wieder den Weg zum Ziel (wobei jetzt auch der Nachbar besucht wurde, also der Einfachheit halber vorne an die Besuchtliste angefügt wird. Und „nach dem gleichen Verfahren" legt natürlich die Rekursion nahen!). Bei Sackgassen arbeitet Prolog automatisch mit Backtracking, d.h. hierfür ist kein Programmtext nötig. Erreicht man das Ziel, stehen daher in der Besuchtliste nur die Knoten des erfolgreichen Weges und man kann die Besuchtliste als Wegbeschreibung ausgeben. Da spätere Knoten vorne angefügt wurden, muss man die Knoten allerdings von hinten nach vorne lesen, um vom Start zum Ziel zu gelangen. Findet Prolog keinen Weg, so erhält man z.B. auf die Frage „?- wegsuche (0, 7)." vom Startknoten 0 zum in den Kanten nicht auftauchenden Knoten 7 die Antwort 0. Gibt es hingegen einen Weg, so kann man mit „Weitersuchen? Ja" auch noch andere Wege zwischen den gleichen Start- und Zielknoten ausgeben lassen. Die Wegsuche kommt sinnvollerweise in die gleiche Prologdatei, in der bereits die Kanten stehen, führt also die gerade angefangene Datei fort.

...
/* Eine mögliche vielfach verwendbare Tiefensuche in Prolog -- www.r-krell.de */

wegsuche (Start, Ziel)
  :- weg(Start, Ziel, [Start]).

weg (Ziel, Ziel, Besuchtliste)   /* Letzter Knoten und Ziel identisch? Ziel erreicht: .. */
  :- write (Besuchtliste), nl.   /* Ausgabe der Besuchtliste, anschließend neue Zeile */

weg (Knoten, Ziel, BesuchtListe) /* noch nicht am Ziel (weil sonst vorstehende Klausel): */
  :- kante (Knoten, Nachbar),    /* Dann zum nächsten freien Nachbarn gehen und .. */
     not istElement (Nachbar, BesuchtListe),
     weg(Nachbar, Ziel, [Nachbar|BesuchtListe]). /* .. rekursiv vom Nachbarn weiter */

istElement (El,[El|_]).          /* wie bereits im Kapitel 4 besprochen */
istElement (El,[_|Rest])
 :-istElement (El,Rest). 

Wirklich vorteilhaft ist, dass die Kanten keineswegs wie im vorstehenden Beispiel explizit angegeben werden müssen. Dies eröffnet die Möglichkeit, leicht alle jene Rätsel und Spiele mit Prolog lösen zu können, wo man einzelne Züge macht -- sofern die Zugregeln als Regeln für Kanten formuliert werden können. Als Beispiel sei ein Nim(m)-Spiel gezeigt:

Anfangs liegen einige Streichhölzer auf dem Tisch. Die Spieler a und b müssen abwechselnd 1 bis 3 Hölzer nehmen. Verloren hat, wer das letzte Holz nehmen muss.

Liegen anfangs z.B. 11 Hölzer auf dem Tisch (H1 = 11) und Spieler a ist am Zug (Sp1=a), so könnte a drei Hölzer nehmen und damit die Hölzchenzahl auf H2 = 8 reduzieren, wobei dann Spieler Sp2 = b dran ist. Natürlich kann man nie mehr Hölzer nehmen, als da sind (H2 >= 0). Das führt zum abgebildeten Graphen und kann wie folgt in Prolog formuliert werden (wobei die Anfangszahl der Hölzchen im Programmtext noch völlig offen bleibt und erst bei der Frage gewählt werden braucht):

/* Zugregeln bzw. Kanten für ein Nim(m)-Spiel */

abwechselnd (a,b).Nimm-Graph (Teilansicht)
abwechselnd (b,a).

kante ([H1, Sp1], [H2, Sp2])
:- abwechselnd (Sp1, Sp2),
   H2 is H1 - 3,
   H2 >= 0.
kante ([H1, Sp1], [H2, Sp2])
:- abwechselnd (Sp1, Sp2),
   H2 is H1 - 2,
   H2 >= 0.

kante ([H1, Sp1], [H2, Sp2])
:- abwechselnd (Sp1, Sp2),
   H2 is H1 - 1,
   H2 >= 0.

...

Bei „..." wird dann die ganz normale Wegsuche angeschlossen, wie sie einen Kasten früher angegeben wurde! Damit kann dann z.B. auf die Frage „?- wegsuche ([11,a], [1,V])." der Verlierer V gefunden und zuvor die passende Zugfolge ausgedruckt werden, die zu diesem Ergebnis führt. „Weitersuchen? Ja" liefert auch noch andere mögliche Spielverläufe (und mögliche andere Verlierer):

Nimm-Spiel in fix-Prolog (Bildschirmfoto)


zum Seitenanfang / zum Seitenende

Literaturhinweise

Da ich mich selbst bereits vor längerer Zeit in Prolog eingearbeitet habe, bin ich bei der Literatur nicht unbedingt auf dem neuesten Stand. Trotzdem möchte ich einige (z.T. schon ältere, aber durchaus noch aktuelle und empfehlenswerte) Werke nennen:


zum Seitenanfang / zum Seitenende

Verweise

Bevor Sie einen der folgenden Verweise (zuletzt aktualisiert im Februar 2014) anklicken, sollten Sie erst meine Seite zu Ihren Favoriten hinzufügen (Internet Explorer) oder ein Lesezeichen setzen (Firefox, Chrome, Opera,...), damit Sie anschließend sicher hierher zurück finden! Natürlich kann ich für den Inhalt fremder Seiten keine Verantwortung übernehmen. Wenn ein Verweis nicht funktioniert, dort inzwischen andere Inhalte zu sehen sind oder wenn Sie mir weitere gute Seiten empfehlen können, bitte ich um eine kurze Nachricht!

Hinweis: die fremden Seiten werden in einem neuen Browser-Fenster geöffnet. Geschieht beim Anklicken eines Verweises scheinbar nichts, wird das neue Fenster vermutlich vom aktuellen Fenster verdeckt -- bitte per Task-Leiste ins neue Fenster wechseln!

„Prolog" Artikel zur Programmiersprache Prolog auf Wikipedia
fix-Prolog Dieses Programm, von dem meine Schule vor Jahren eine Lizenz erworben hatte, wird nicht mehr vertrieben und ist unter Windows 8.1 auch nicht mehr lauffähig. Als guter Ersatz bietet sich das nachstehende tuProlog an:
tuProlog (2p)
tuprolog.unibo.it
Schöner, kostenloser und kompakter Prolog-Compiler einschließlich Editor bzw. Entwicklungsumgebung von der Universität Bologna -- leicht zu handhaben; Versionen für verschiedene Betriebssysteme. Gute Erfahrungen habe ich mit der oben auch abgebildeten Version, die eine Java-JRE voraussetzt (lauffähige Datei 2p.jar) und keine Installation benötigt. Download nur 6,8 MB; Handbuch (engl.) 4,4 MB.
SWI-Prolog Kostenloser Prolog-Compiler mit vielen Möglichkeiten (Achtung: wie oben in Kapitel 1 erwähnt, müssen Leerzeichen aus meinen Klauseln entfernt werden, damit die Beispiele mit SWI-Prolog „laufen").
Editor für SWI-Prolog Gerhard Röhners empfehlenswerter, kostenloser Editor als Entwicklungsumgebung für SWI-Prolog (inkl. Hinweisen zur Installation auch von SWI-Prolog) (Download ca. 1,7 MB))
J-Prolog-Editor für SWI-Prolog eine -- nicht mehr gepflegte -- schwächere Alternative zu Röhners Editor
Informatik mit Prolog Inhaltsübersicht und Bezugsquelle für G. Röhners Buch „Informatik mit Prolog" mit Materialien für Grund- und Leistungskurse (s.o., Literaturhinweise)
Arbeitsbuch Prolog pdf-Datei von Göhner/Hafenbrak: „Arbeitsbuch Prolog" (s.o., Literaturhinweise). (ca. 700 kB)
Prolog, u.a. mit Graphen, Tiefensuche, Automaten, Grammatik & .. Im umfangreichen und gut strukturierten Informatik-Angebot des Hohenstaufen-Gymnasiums in Kaiserslautern wird nicht nur die Tiefensuche in Prolog (s.o., Kapitel 5) fundiert mit Beispielen vorgestellt. Hier lohnt das Stöbern! (Oben auf die verschiedenen Themen in [..] klicken!)
KI mit Prolog Auf den Webseiten des Winfried-Gymnasiums in Fulda gibt's viel Lohnendes zu Prolog; u.a. werden auch die üblichen Sortierverfahren in Prolog programmiert
Datenbanken mit Prolog Auf dieser Seite der Uni Würzburg werden Tabellen und die üblichen Datenbankoperationen in Prolog übersetzt bzw. realisiert
Datenbanken in Prolog Philipp Hauer zeigt an konkreten Beispielen Datenbanken und SQL-Übersetzungen in Prolog
Grundkurs Prolog (f. Linguisten) Schöne Einführung in Prolog (pdf-Datei mit 117 Seiten, 313 kB) mit vielen allgemeinen und weiteren zusätzlichen Beispielen für die Sprachverarbeitung
hadersbeckprolog Gute Darstellung der Arbeitsweise von Prolog (pdf-Datei, 75 S., 309 kB)
Prolog-Skript Vorlesungsskript zu Prolog mit Beispielen, manche Aufgaben wurden aus dem o.g. "Arbeitsbuch Prolog" von Göhner/Hafenbrak übernommen
nosco KI&Prolog Prolog an/mit vielen Beispielen erklärt - Skript und Übungen
Uni-Erlangen (Beisp. & Übungen) Prolog mit Beispielen und Übungen, z.T. aus der Linguistik: die vielen pdf-Dateien sind von der genannten Startseite aus erreichbar.
Prolog-Einführung Eine Powerpoint-Präsentation zu Prolog -- am Stück (Prolog.PPT) oder alle Seiten einzeln als Bilder -- vergleicht Prolog mit anderen Programmierstilen
JProlog Von der Uni Leuven (Belgien) gibt's einen 1996 ganz in Java geschriebenen Prolog-Compiler komplett mit allen Java-Quelltexten -- zum kostenlosen Gebrauch und für alle, die wissen wollen, wie man eine Prolog-Umgebung herstellen kann

Ihnen gefällt meine Prolog-Seite bzw. mein Web-Angebot? Sie wollen auch andere auf diese Seite aufmerksam machen? Mit dem Verweis „per e-Mail weiter empfehlen" ganz unten auf dieser Seite können Sie eine e-Mail mit der URL versenden!



zurück zur Informatik-Hauptseite


zum Anfang dieser Seite
Willkommen/Übersicht  -  Was ist neu?  -  Software  -  Mathematik  -  Physik  -  Informatik  -   Schule: Lessing-Gymnasium und -Berufskolleg  -  Fotovoltaik  -  & mehr  -  Kontakt: e-Mail,  News-Abo, Gästebuch, Impressum  -  Grußkarten, site map, Download und Suche

Diese Seite ist Teil des Webangebots http://www.r-krell.de. Sie können diese Seite per e-Mail weiter empfehlen (tell a friend).