zurück zum Artikel

Prolog oder die unendlichen Weiten der Logik

Hans Nikolaus Beck
Prolog oder die unendlichen Weiten der Logik

(Bild: pixabay / CC0 Creative Commons)

Wissensverwaltung und Schlüsse ziehen sind Standardprobleme der Künstlichen Intelligenz, die in der heutigen IT-Welt Furore macht. Schon vor Jahrzehnten führten diese Probleme zur Entwicklung einer Programmiersprache, die so ganz anders zu sein scheint: Prolog.

Diese Sprache ist nicht nur ein Spielzeug abgehobener Theoretiker, sie gewinnt gerade angesichts von Big Data und Künstlicher Intelligenz an Bedeutung. Trekkies wissen es: nicht nur, dass es in der Zukunft kein Geld und keine Krankheiten mehr gibt, in Gene Roddenberrys Zukunftsvision muss niemand mehr programmieren. Mr. Data beschreibt dem Computer sein Problem. Wir Programmierer der Jetzt-Zeit müssen uns stattdessen mit Algorithmen und Datenstrukturen befassen. Schritt für Schritt geben wir der Maschine vor, was diese zu tun hat. Verteilte Speicherung von Big-Data-Wolken bringen zwar mehr Komfort. Aber der Mainstream-Programmierer lebt in prozeduralen Sprachen der von-Neumann-Welt, die da sagt "tue dies", dann "tue das".

Prolog ermöglicht es, schon heute ein bisschen Mr. Data zu sein. Der Prolog-Programmierer definiert Fakten, gibt logische Regeln vor und stellt Fragen. Ein Prolog-Programm laufen zu lassen bedeutet nichts anderes, als eine Antwort auf eine Frage logisch abzuleiten. Was so banal klingt, birgt eine mächtige Programmiersprache in sich.

Nehmen wir an, wir wollen ein kleines Kartenspiel ähnlich Siebzehn und Vier entwickeln. Eine dafür sinnvolle Wissensbasis könnte sein:

card(herz, 10, 10).
card(herz, bube, 11).
card(herz, dame, 12).
card(herz, koenig, 13).
card(herz, ass, 14).
win(dame, bube).
win(koenig, dame).
win(ass, koenig).

Diese Ausdrücke sind Fakten. Sie sagen: zu einer Karte gehören die Elemente herz, bube und die Zahl 11. card() ist ein sogenannter Funktor, und steht für ein Prädikat oder eine Relation. win(dame, bube) entspräche der Prädikatsaussage dame gewinnt oder sticht gegen bube. Was diese Argumente des Funktors bedeuten (nämlich Kartenfarbe, Kartenname und Punktwert) ist aus logischer Sicht zunächst irrelevant.

Die Kleinschreibweise zeigt Prolog an, dass es sich um feste Symbole handelt. Im Prolog-Kontext spricht man von Atomen. Eine Frage-Sitzung mit SWI-Prolog [1] könnte sich mit diesen Fakten wie in Abbildung 1 gestalten.

Abbildung 1: Eine einfache Prolog Sitzung

Abbildung 1: Eine einfache Prolog Sitzung

Auf die erste Frage ?- card(herz, bube, 11). antwortet Prolog true, also "wahr". Das angefragte Prädikat ist in der Wissenbasis enthalten, also ein Fakt und somit wahr. ?- card(caro, bube, 11). würde das System entsprechend mit false beantworten.

Interessanter sind aber Variablen, in Prolog groß geschrieben. In der zweiten Frage ist mit Point eine solche Variable enthalten. Sie bedeutet: welchen Punktwert hat die Karte Herz-Dame? Prolog versucht, eine Belegung oder Instanzierung der Point-Variable zu finden, so dass der Ausdruck einem bekannten Fakt entspricht. Bei Erfolg wird die Instanzierung ausgegeben. Der gesuchte Punktwert ist hier 12. In Prolog geht es also immer um die Frage, ob ein Ausdruck (in Prolog Term genannt) auf bekannte Fakten zurückgeführt werden kann.

Fakten allein würden aber nicht weiterhelfen. Regeln erlauben, logisch aus bekannten Fakten neue Antworten zu bestimmen. Für unser Spiel brauchen wir beispielsweise auch Gewinnregeln. In Siebzehn und Vier ist das Ziel, nur so viele Karten zu ziehen, dass die Summe der Punkte 21 erreicht. Zumindest soll man so dicht wie möglich an 21 herankommen. Angenommen, wir hätten diese Distanz zu 21 für jeden Spieler bereits berechnet. Mit

% Distance1 ist Abstand zu 21 für Spieler 1
% Distance2 ist Abstand zu 21 für Spieler 2
% letztes Argument ist die Spielernummer
winner(Distance1, Distance2, 1) :-
Distance1 < Distance2.
winner(Distance1, Distance2, 2) :-
Distance1 > Distance2.

beschreiben wir Prolog: winner(....,1) ist wahr, wenn Distance1 < Distance2 wahr ist.

Regeln haben allgemein die Struktur A :- B1, B2, B3. A, der sogenannte Regelkopf, ist zutreffend, wenn B1 UND B2, UND B3 wahr sind. Die Bs, auch Klauseln genannt, formen den Regelrumpf und können selbst wieder Fakten oder Regeln sein. In dieser Weise lassen sich logische Strukturen eines Problems leicht formulieren.

Nimmt man Prädikate als Relationen, kann man auch Transitivität ausdrücken:

winAlso(X,Y) :-
win(X, dame),
win(dame, Y).

Bedeutet: Wenn eine Karte X die Dame schlägt, diese aber eine andere Karte Y, so schlägt X auch Y (siehe Abbildung 2). Auch dies ist nur logische Struktur, bisher war nichts Prozedurales nötig. Das ist praktisch, wenn man erst einmal die Struktur seines Problems verstehen will.

Abbildung 2: Abfrage eines transitiven Sachverhalts

Abbildung 2: Abfrage eines transitiven Sachverhalts

Die oben verwendete Punktedistanz soll nun berechnet werden. Berechnung ist doch etwas Prozedurales, oder nicht? Denn die Punkte aller Karten eines Spielers sind zu summieren und danach der Abstand zu 21 zu bestimmen. Also:

% Teste ob 21 überschritten ist
% Distance ist im folgenden ein Abstand eines Punktwertes zu 21
% cardsTest(+Kartenliste, -Abstand zu 21 (ggf negativ))
cardsTest(Cards, Distance) :-
sumCards(Cards, Sum),
Distance is 21 - Sum.

cardsTest ist wahr, wenn sumCards(...) und die letzte Zeile wahr sind. Distance is 21 - sum ist aber keine prozedurale Anweisung! Eben sowenig ist cardsTest(...) eine Funktion. Und wo ist überhaupt der Rückgabewert?

Betrachten wir den Aufruf der Regel cardsTest näher. Angenommen, Cards sei mit einer Liste von card(...) Strukturen instanziert. Distance wäre unbelegt. Zunächst wird die Regel sumCards(..) im Regelrumpf auf wahr geprüft. Wenn ja, wird die Variable Sum mit eben dieser Punktesumme instanziert, zum Beispiel "10". Die folgende Klausel kann nur wahr werden, wenn Distance zu 11 instanziert wird. Alle Klauseln des Regelrumpfes sind wahr, und so ist der Kopf cardsTest() es auch. Was also prozedural aussieht, ist logische Struktur pur!

Da war noch die Frage nach einem Rückgabewert. Jede freie Variable eines Regelkopfes versucht Prolog zu instanzieren, damit die Regel wahr wird beziehungsweise einem bekannten Fakt entspricht. In unserem Beispiel würde Distance instanziert werden und so eine Art Rückgabewert bilden. Es ist gute Praxis, die Intention der Variablen in den Kommentaren anzuzeigen, wie im nachfolgenden Beispiel.

Etwas ungenau könnte man sagen: In Prolog beschreibt man logische Strukturen. Dort, wo Informationen fehlen (egal wo), versucht Prolog, eine Instanzierung zu finden. Was auf den ersten Blick verwirren mag, ist Prologs Potenzial für Prototyping. Es erlaubt, ein Problem Top-Down zu beschreiben. Beispiel: Eine weitere Gewinnregel soll festlegen, Spieler 1 habe gewonnen, wenn Spieler 2 über 21 kommt. In Prolog:

% Kommentar zu den Argumenten wie SWI Prolog
% + bedeutet "input", soll instanziert sein
% - bedeutet "output", wird instanziert durch die vorliegende Regel
% +Distance1, +Distance2, -Spielernummer
winner(Distance1, Distance2, 1) :-
cardsOk(Distance1, ok),
cardsOk(Distance2, loser).

Die Prüfung der Überschreitung steckt in der Klausel cardsOK(...). Diese kann später durch Einführung einer entsprechenden Regel detailliert werden. Prolog braucht Details erst, wenn der Wahrheitswert von cardsOK() bestimmt werden soll.

Rekursion, Listen und Bäume sind aus der prozeduralen Welt gut bekannt. In Prolog sind sie ein wichtiges und effizientes Gestaltungsmittel. Für die Berechnung der Punktesumme aller Karten könnte man schreiben:

% sumCards(+Liste von Karten als Kartenstrukturen, +Punkte vorher, -Punkte summiert)
sumCards([C|Cards], Sum, Sum3) :-
sumPoints(C, Sum, Sum2),
sumCards(Cards, Sum2, Sum3).

Der Term [C|Cards] gibt ein Muster vor. Es steht für eine Liste [..], die ein erstes Element C enthält, und die restliche Liste Cards. Die Punkte der Karte C werden auf die Summe Sum aufsummiert. Nun folgt die Rekursion: Für die restliche Liste Cards wird wieder dieselbe Regel sumCards(...) angewendet. Prolog verfügt über Mechanismen, soll eine rekursive Struktur intern als leichtgewichtige lineare Aufrufsequenz umzusetzen.

Damit das Ganze stoppt, muss noch eine Bedingung formuliert werden, wenn es keine Restliste mehr gibt:

sumCards([], Sum, Sum).

Wird also die Regel sumCards(..,..,..) mit leerer Liste aufgerufen, so ändert sich an der Summe nichts.

Um Listen und Bäume tummeln sich viele Algorithmen. In Prolog lassen sich die Algorithmen als logische Struktur beschreiben. Nehmen wir das Prädikat append(...), das zwei Listen aneinander fügt. Die Implementation dieses Prädikats verdeutlicht das wunderbar:

append([], List, List).
append([Element|Restliste], AlteListe, [Element | Neueliste]) :-
append(Restliste, AlteListe, NeueListe).

Die wichtigste Datenstruktur ist natürlich die Faktenbasis selbst. Ein Prolog-Programm kann sie direkt manipulieren. Das Prädikat asserta(Term) fügt einen Term an den Anfang der Wissensbasis ein. retract(Term) entfernt den Term wieder.

drawCard(Farbe, Name, Card) :-
Card = card(Farbe, Name, _),
retract(Card).

Dieser Code bildet das Ziehen einer Karte aus dem Deck nach, indem es diese aus der Wissensbasis löscht. Hier noch ein wichtiger Hinweis: das _ steht für eine unbenannte Variable. Auf sie kann nicht zugegriffen werden. Sie zeigt hier nur an, dass der Funktor card drei Argumente hat. Es ist die logische Variante von "don't care".

Nicht zu vergessen ist auch: Jedes Prädikat hat eine Datenstruktur. Weil Argumente von Funktoren geschachtelt werden können, sind beliebige hierarchische Strukturen denkbar. Das Fakt

newPlayer(Num, player(Num, [])).

beschreibt eine Player-Struktur, die selbst wieder eine Liste in sich trägt. Mit ?- newPlayer(1, Player). wird Player zu player(1, []) instanziert. Übrigens ist das ein Prolog-typisches Generator Pattern.

Prozedurales Denken kann in Prolog hinderlich sein. Folgende Regel schreibt vor, eine Karte aus der Wissensbasis zu ermitteln und der Kartenliste Field anzuhängen.

% addCard(+Spielfeld als Kartenliste, +Kartenfarbe, +Name, -Spielfeld mit neuer Karte
addCard(Field, Farbe, Name, Field2) :-
A = card(Farbe, Name, _),
append(Field, [A], Field2).

Prolog sagt dazu:

Abbildung 3: Eine prozedurale Falle

Abbildung 3: Eine prozedurale Falle

Überraschung! Field2 sollte die neue Karte enthalten. Statt dessen lautet der Eintrag card(herz, bube, _3498). Der Punktwert fehlt. Das Missverständnis liegt in dem Ausdruck A = card(...). Dies ist keine Zuweisung wie bei prozeduralen Sprachen. A = card(...) ist wahr (und nur darum geht es!), wenn A mit card(herz, bube, _) instanziert werden kann. Und es kann, denn ein card(herz, bube, "und irgend etwas") ist Teil der Wissensbasis. "_" bedeutet nur, da muss etwas sein. Aber der Inhalt spielt keine Rolle und muss somit auch nicht instanziert werden. Aus diesem Grund wird auch nur card(herz, bube, _) der Liste hinzugefügt.

Ein Fehler ist das aber nicht. Eine Liste [Kopf|_] darf unvollständig sein und kann weiterverarbeitet werden. Unvollständige Datenstrukturen sind jedoch ein elegantes Mittel von Prolog, weil sie ein Problem in seiner Struktur allgemein beschreiben lassen.

Wir haben gesehen, Prolog versucht, eine Klausel A auf einen bekannten Fakt B zurückzuführen. Dies ist erfolgreich, wenn A und B gleich aussehen. Im Allgemeinen werden sie verschieden sein. Der Unifikationsalgorithmus in Prolog ermittelt, ob durch Instanzieren von Variablen und Termersetzungen A und B deckungsgleich gemacht werden können. Dazu wieder ein Beispiel:

sumPoints(card(_,_,Point), Sum, Sum2) :-
Sum2 is Sum + Point.

Findet sich nun im Rumpf einer anderen Regel

... :- 
...
sumPoints(OneCard, 0, Sum2),
...

So versucht Prolog, OneCard mit card(_,_,Point) in Einklang zu bringen. Wäre OneCard mit einer Zahl instanziert, so wäre das nicht möglich. Eine Zahl und card(...) sind verschiedene Dinge. Die Regel sumPoints(...) wäre nicht zutreffend, also falsch.

Wenn nun OneCard mit card(herz, bube, 11) instanziert wäre, so würde Prolog jetzt card(herz, bube, 11) mit card(_,_,Point) abgleichen. 1. Schritt: Der Funktorname stimmt überein, die Zahl der Argumente auch. 2. Schritt: Wenn Point mit 11 instanziert wird, sehen beide Terme identisch aus, sie sind unifiziert.

winner([C|Cs1], [C2|Cs2], Winner) :- ...

Würde es funktionieren, wenn diese Regel mit winner([], [], Winner) abgefragt würde? Die Unifikation würde versuchen, [] mit [C|Cs1] zu unifizieren. Das muss fehlschlagen, denn es gibt keine Termersetzung, die aus C|Cs1 "nichts" macht.

Dieser Fall müsste mit einer eigenen Regel abgefangen werden:

winner( [], [], _). 

Was andere Sprachen mit Typprüfung vollziehen, wird in Prolog mit Unifikation und logischen Strukturen implementiert. Für den Programmierer fühlt es sich letztlich wie Pattern Matching an.

Wie findet nun Prolog eine zutreffende Regel? Letztlich werden die Regeln durchprobiert. Aber dies geschieht nach einem Mechanismus, der sich Backtracking nennt. Dies sei an folgenden Gewinnregeln illustriert:

winner(Distance1, Distance2, 1) :-
cardsOk(Distance1, ok),
cardsOk(Distance2, loser).

winner(Distance1, Distance2, 2) :-
cardsOk(Distance2, ok),
cardsOk(Distance1, loser).

winner(Distance1, Distance2, 1) :-
Distance1 < Distance2.

winner(Distance1, Distance2, 2) :-
Distance1 > Distance2.

Nehmen wir an, Spieler 1 hätte die 21 um 2 überschritten, Spieler 2 wäre 4 Punkte unter 21. Die Prolog-Frage dazu wäre ?- winner(-2, 4, Winner). Um eine Instanz und damit eine Antwort für Winner zu finden, geht Prolog wie in der folgenden Abbildung vor:

Abbildung 5: Ein Beispiel für Backtracking

Abbildung 5: Ein Beispiel für Backtracking

Prolog wählt die erste der oben stehenden Regeln und instanziert die Variablen entsprechend der Eingabe. Im Körper der Regel stehen zwei cardsOK()-Regeln. Die erste kann nicht wahr werden. Da es keine alternative Regel mit dem Kopf cardsOK(Arg1, Arg2) gibt, geht Prolog zurück zur aufrufenden Regel, also winner(...). Hiervon gibt es alternative Regeln, nämlich die zweite Formulierung in oben stehender Liste. Diese wird versucht und führt zu einem True. Damit kann der Aufruf wahr gemacht werden, wenn Winner mit "2" instanziert wird.

Backtracking bedeutet also, in der Regelhierarchie vom Regelrumpf zurück zum Regelkopf zu gehen (dem "Aufrufenden"), um alternative Regeln (und Unifikationen) zu finden. Es entspricht einer Tiefensuche im Regelbaum.

Aus dem bisher beschriebenen wird deutlich, dass Prolog ein Programmier-Janus mit zwei Gesichtern ist. Das eine ist das der logischen Struktur, Regeln und Fakten. Unifikation ist der logische Mechanismus, Lücken in dieser Struktur zu füllen. Aber Prolog hat auch eine prozedurale Seite. Nehmen wir wieder unsere Gewinnbedingungen.

winner(Distance1, Distance2, 1) :- 
...
winner(Distance1, Distance2, 2) :-
....
usw -> siehe Listing

Gemäß der Session in der nachfolgenden Abbildung 4 gibt Prolog mehrere Gewinner gleichzeitig aus. Der Grund ist einfach. Das ";" am Zeilenende fordert Prolog auf, weitere Instanzen für die Eingangsfrage zu ermitteln. Das Backtracking kann also nicht nur im "false"-Falle wie im obigen Beispiel angewendet werden. Es ist auch das Verfahren, wenn mehrere Lösungen gewollt sind. Möchte man das nicht, kann mit dem Cut Operator "!" das Backtracking gestoppt werden.

Abbildung 4: Mehrdeutigkeiten in Prolog

Abbildung 4: Mehrdeutigkeiten in Prolog

Dem aufmerksamen Leser wird auch auffallen, dass die Lösungen in der Reihenfolge ausgegeben werden, wie die entsprechenden Regeln im Code stehen. Deren Reihenfolge ist relevant, wie oben schon gesehen. Im ungünstigsten Fall kann das zu Endlosschleifen führen, wie in diesem Beispiel:

% addFact(+Kartenfarbe, Endnummer, -Laufender Index)
addFact(Farbe, End, I) :-
asserta(card(Farbe, I, I)),
I2 is I + 1,
addFact(Farbe, End, I2).

% Abbruchbedingung
addFact(_, End, End).

Würde man dies ausführen, ließe sich Prolog nur noch mit "Ctrl-C" unterbrechen. Die obere, und damit immer zuerst geprüfte, Regel enthält die Rekursion, die Abbruchbedingung in der darunter stehenden Regel würde nie erreicht. Auch die Klauseln in einem Regelkörper werden der Reihe nach von der ersten zur letzten geprüft.

Zu beachtende Reihenfolgen in Regeln und in Regelkörpern geben Prolog ein prozedurales Gesicht. Für die Beschreibung der Lösung ist es zunächst nicht so entscheidend. Geht es jedoch an Effizienz und Korrektheit, ist es ratsam, diese Seite von Prolog zu beachten. Andererseits zeigt es auch, das Prolog nicht so weit entfernt ist von dem vertrauten Prozeduralen.

Logische Strukturen und Pattern Matching machen Prolog zu einer idealen Sprache für Compiler- und Interpreter-Bau. Der folgende Code soll das illustrieren. Es geht darum, das Kartenspiel jetzt interaktiv zu gestalten.

% A, P sind player Strukturen player(Nummer, Spielfeld), je für aktiven und passiven Spieler
play(A, P, go) :-
A = player(Num, _),
format("Player ~d <playCard> or <stop> ", [Num] ),
read(Command),
do(Command, A, P, A2, P2, Finish),
nextPlayer(A2, P2, A3, P3),
play(A3, P3, Finish).

play(_, _, stop).

do(stop, A, P, A, P, Finish) :-
call(stop, A, P, Finish).

do(playCard, A, P, A2, P, Finish) :-
call(playCard, A, A2, Finish).

do(showDeck, A, P, A, P, go) :-
showDeck(L),
format("Current Deck: ~p\n", [L]).

play liest einen Befehl von der Kommandozeile ein. Die passende do(...)-Regel führt dann die notwendigen Aktionen aus (prozedurale Sicht) oder wendet weitere Regeln an (logische Sicht). Eine übliche Technik ist es, den Kontext der Ausführung vor und danach mitzugeben. In dem Beispiel ist das die player(...)-Struktur, die auch das Spielfeld eines Spielers enthält. Nach Ausführung des Kommandos werden aktiver und passiver Spieler vertauscht, und es geht in die nächste Runde per Rekursion.

Das call(FunktorName, Arg1, Arg2...)-Prädikat gibt Prolog dynamische Aspekte, ähnlich den interpretierenden Sprachen. call wirkt, als würde statt dessen die Regel funktorName(Arg1, Arg2,...) geschrieben stehen. Ein sehr inspirierendes Beispiel für Prolog als Prototypensprache ist ein Papier von Joe Amstrong und Kollegen [2]. Es beschreibt, wie Erlang als Prototyp in Prolog begann. Prolog kann auch wunderbar sich selbst interpretieren oder der Prolog-Interpreter um neue Funktionen erweitert werden. Hier sei auf die Literaturliste verwiesen.

In den obigen Beispielen wurde die Schreibweise A < B verwendet. Prolog kennt aber eigentlich nur Funktoren, so dass dieser Ausdruck <(A, B) lauten müsste. Hier hilft die Definition von Operatoren. Eine solche Definition wäre zum Beispiel durch die Direktive :- op(Priorität, Praefix/Postfix/Infixmuster, "<"). möglich. Mit dieser Technik lassen sich leicht lesbare Sprachkonstrukte für den Interpreterbau erstellen.

Auch sehr schön ist die Möglichkeit, Funktoren "zusammenzubauen". Das Prädikat functor(Term, Name, N) ist wahr, wenn Term ein Funktor mit Name name ist, mit N Argumenten. Oder: functor(Term, meinFunc, 3) instanziert Term mit Funktor meinFunc(_,_,_) usw. Ergänzt wird dies durch das Prädikat arg(N, Term, Value), mit dem sich der Wert des N-ten Argumentes eines Terms (und damit Funktors) ermitteln lässt.

Für Prolog ist schon länger ein Standard verfügbar, der ISO/IEC 13211-1:1995. Der Interessierte kann aus einem Angebot von freien und kommerziellen Implementationen wählen. Das hier verwendete SWI-Prolog bietet neben einer ISO-Prolog-Implementation viele zusätzliche Pakete. Hierzu gehören Webserver und eine RDF-Bibliothek samt Triplestore. Auch gibt es Weiterentwicklungen von Prolog für Parallelität, Constraint-Programming oder Probabilistische Programmierung. Wikipedia bietet eine gute Übersicht [3].

Viele Prolog-Systeme sind als Compiler für eine Warren Abstract Machine (WAM) ausgeführt. Ähnlich wie in Smalltalk [4] und anderen Sprachen ist dies eine virtuelle Maschine mit für Prolog günstigem Instruktionssatz. Erstmals beschrieben wurde sie 1983 von dem Informatiker David H.D. Warren in einem Paper [5].

Ist Prolog so anders? Sicher, ein Prolog-Programm ist scheinbar völlig anders aufgebaut. Aber da es neben der logischen Beschreibung auch eine prozedurale Sicht bietet, zeigt sich Prolog als Programmiersprache im besten Sinne. Über logische Strukturen nachzudenken wird wunderbar unterstützt, ist aber auch Pflicht. Wer sich mit Prolog beschäftigt, gewinnt eine andere Perspektive auf die Beschreibung von Problemen und Lösungen. Für Prototyping ist Prolog geradezu ideal. Nicht zuletzt ist Prolog eine zeitgemäße und (bei entsprechender Programmierung) effiziente Programmiersprache, die eine nähere Betrachtung wert ist.

Hans Nikolaus Beck [6]
befasst sich unter anderem mit Lernen durch Computerspiele und Serious Games beziehungsweise Gamification. Als langjähriger Entwickler hat er Erfahrung in diversen Programmiersprachen gesammelt.

Der im Artikel vorgestellte Sourcecode ist bei GitHub verfügbar [7].

  1. Joe Amstrong: A History of Erlang. [8]
  2. Joe Amstrong et al, 1992: Use of Prolog for developing a new programming language [9]
  3. Richard A. O'Keefe: The Craft of Prolog, MIT Press, Cambridge 1990
  4. Ivan Bratko: Prolog Programming for Artificial Intelligence, Pearson Education Ltd, Essex, England. 4. Auflage 2012
  5. Sterling, Shapiro: The Art of Prolog, MIT Press, Cambridge, 3. Auflage 1999
  6. Boizumault, Djamboulian, Fattouh: The Implementation of Prolog, Princeton University Press, Princeton, 1993
  7. Uwe Schöning: Logik für Informatiker, Spektrum Akademischer Verlag GmbH, Heidelberg, 5. Auflage 2000

(map [10])


URL dieses Artikels:
https://www.heise.de/-4070313

Links in diesem Artikel:
[1] http://www.swi-prolog.org/
[2] http://citeseerx.ist.psu.edu/viewdoc/download;jsessionid=9107E657BF6ECC845454FA529A04AF00?doi=10.1.1.34.3972&rep=rep1&type=pdf
[3] https://en.wikipedia.org/wiki/Comparison_of_Prolog_implementations
[4] https://www.heise.de/hintergrund/Wenn-Objekte-Smalltalk-machen-3999014.html
[5] http://www.ai.sri.com/pubs/files/641.pdf
[6] mailto:hnbeck@educational-concepts.biz?subject=Anfrage%20zum%20Prolog-Artikel%20bei%20Heise%20Developer
[7] https://github.com/hnbeck/PrologSampleGame
[8] http://webcem01.cem.itesm.mx:8005/erlang/cd/downloads/hopl_erlang.pdf
[9] http://citeseerx.ist.psu.edu/viewdoc/download;jsessionid=9107E657BF6ECC845454FA529A04AF00?doi=10.1.1.34.3972&rep=rep1&type=pdf
[10] mailto:map@ix.de