voriges Kapitel | Startseite | Inhalt | nächstes Kapitel

© 2001 François Bry
Dieses Lehrmaterial wird ausschließlich zur privaten Verwendung angeboten. Eine nichtprivate Nutzung (z.B. im Unterricht oder eine Veröffentlichung von Kopien oder Übersetzungen) dieses Lehrmaterials bedarf der Erlaubnis des Autors.


Zusammengesetzte Terme und rekursive Datentypen


  1. Zusammengesetzte Datenstrukturen
  2. Rekursive Programme
  3. Rekursive DatentypenVariablen, Substitutionen und Antworten
  4. Ein rekursiver Datentyp: die Liste
  5. Entwicklung von rekursiven Programmen am Beispiel von delete
  6. Unifikation mit Occur-Check

1. Zusammengesetzte Datenstrukturen

P24 spezifiziert eine Beziehung zwischen 8 Objekten. P25 spezifiziert aber eine 3-stellige Beziehung. Diese zweite Darstellung "bringt zusammen, was zusammen gehört." Sie vereinfacht den Zugriff auf die gewünschte Information, wie zum Beispiel im folgenden Programm
Als Vorteile eine strukturierten Darstellung können genannt werden:
  1. Die kompaktere Darstellung.
  2. Die Modularität.
Als Nachteil muß aber der Zwang angesehen werden, alle Daten in eine festgelegte Hierarchie zwingen zu müssen.
Die Symbole, die zur Strukturierung in PROLOG verwendet werden, heißen Funktionssymbole. Sie werden auch Funktoren genannt. Ein Funktionssymbol wird in PROLOG nicht ausgewertet. Es stellt lediglich einen Termkonstruktor dar.
Die Stelligkeit ist in PROLOG Teil der Definition eines Funktionssymbols. Es ist also zulässig, im gleichen Programm das gleiche Zeichen mit verschiedenen Stelligkeiten zu verwenden wie etwa f(a) und f(a, b, c).
Man erinnere sich daran, daß jegliche n-stellige Beziehung mittels n-1 Beziehungen der Stelligkeit 2 dargestellt werden kann.

zum Seitenanfang

Variablenfreie Fakten


  • Die Symbole, die zur Strukturierung in PROLOG verwendet werden, heißen Funktionssymbole. Sie werden auch Funktoren genannt. Ein Funktionssymbol wird in PROLOG nicht ausgewertet. Es stellt lediglich einen Termkonstruktor dar.
  • Die Stelligkeit ist in PROLOG Teil der Definition eines Funktionssymbols. Es ist also zulässig, im gleichen Programm das gleiche Zeichen mit verschiedenen Stelligkeiten zu verwenden wie etwa f(a) und f(a, b, c).
  • Man erinnere sich daran, daß jegliche n-stellige Beziehung mittels n-1 Beziehungen der Stelligkeit 2 dargestellt werden kann.

Rekursive Programme


  • Ein PROLOG-Programm induziert wie folgt eine Abhängigkeitsrelation zwischen Relationssymbolen: Ein Relationssymbol, das das Relationssymbol des Kopfes einer Regel R ist, hängt von allen Relationssymbolen der Rumpfliterale von R ab.
  • Eine PROLOG-Programm heißt rekursiv, wenn diese Abhängigkeitsrelation einen Zyklus enthält.
Beispiel für ein rekursives Programm (aus dem Kapitel 2):
12
verbunden(X, Y) :- kante(X, Y). verbunden(X, Y) :- kante(X, Z), verbunden(Z, Y). kante(k1, k2). kante(k2, k3). kante(k3, k1).

3. Rekursive Datentypen

Die folgende Definition der natürlichen Zahlen liefert ein anschauliches Beispiel für einen rekursiven Datentyp.
Man könnte die Regel plus(X, null, X) :- natuerliche_zahl(X). zu der Definition von plus im Programm P28a hinzufügen. Das wäre korrekt, aber redundant: auf die Anfrage plus(null,null,Z) würde die Antwort Z = null einmal mit der bisherigen ersten Regel und einmal mit der zusätzlichen Regel berechnet. Ebenso überlappt sich die zusätzliche Regel mit der letzten Regel.
Nach Hinzufügen dieser Regel könnte man die letzte Regel auch abändern zu plus(X, nf(Y), nf(Z)) :- plus(X, Y, Z). und die Definition wäre immer noch korrekt, wenn auch redundant. Das gleiche gilt, wenn die obige Regel nicht als Ersatz für die letzte dient, sondern zusätzlich als vierte Regel hinzugefügt wird, nur ist die Redundanz dann noch größer.
Hiermit beobachtet man etwas, das in der PROLOG-Programmierung häufig vorkommt: Die Anwendungen von Regeln können sich überlappen, was zu Redundanzen führen kann.
Das Programm P30 ist insofern ineffizient, als bei jeder Berechnung der Fakultät einer Zahl die Fakultät der Vorgängerzahl wiederberechnet wird. Wir werden sehen, wie die Technik der Memoisierung eingesetzt werden kann, um wiederholte Berechnungen zu vermeiden, ohne dabei die leichtverständliche Spezifikation eines Programms verändern zu müssen.

zum Seitenanfang

Rekursive Datentypen


Die folgende Definition der natürlichen Zahlen liefert ein anschauliches Beispiel für einen rekursiven Datentyp.
27
natuerliche_zahl(null). natuerliche_zahl(nf(Z)) :- natuerliche_zahl(Z). :- natuerliche_zahl(Z). Z = null More? (;) ; Z = nf(null) More? (;) ; Z = nf(nf(null)) More? (;) ; Z = nf(nf(nf(null))) More? (;) ; Z = nf(nf(nf(nf(null)))) More? (;) :-

4. Ein rekursiver Datentyp: die Liste

In PROLOG wird das Funktionssymbol  .  besonders unterstützt, das sogenannte Listenfunktionsymbol.
Eine PROLOG-Liste besteht entweder aus der leeren Liste [], oder aus einem Paar der Gestalt .(Kopf, Rumpf) wobei Rumpf eine Liste ist. Die Liste .(a, .(b, .(c, []))) kann der Einfachheit halber auch [a, b, c] dargestellt werden.
Ein Listenpaar .(Kopf, Rumpf) kann auch [Kopf | Rumpf] dargestellt werden.
Die folgende Sitzung gibt einen Überblick über die Nutzung von Listen in PROLOG:
:- atom([]). yes. :- X = .(a, []). X = [a] yes. :- X = .(a, .(b, .(c, []))) . X = [a, b, c] yes. :- X = .(a, R). X = [a | R] R = R yes. :- X = .(a, .(b, .(c, R))) . X = [a, b, c | R] R = R yes. :- [a, b, c] = [K | R]. K = a R = [b, c] yes. :- [a, b, c, d, e] = [K1, K2 | R]. K1 = a K2 = b R = [c, d, e] yes. :- [H | T] = .(K, R). H = K T = R K = K R = R yes. :- [] = [K | R]. no (more) solution. :-
Listen können wie folgt spezifiziert werden. Dabei tritt ein häufiges Problem der Darstellung von Variablen in Beweisen auf.
tahiti(bry): % eclipse ECRC Common Logic Programming System [sepia opium megalog] Version 3.4.5, Copyright ECRC GmbH, Thu Jun 16 11:08 1994 [eclipse 1]: [user]. liste([]). liste([K | R]) :- liste(R). *** Warning: Singleton variables in clause 2 of liste/1: K user compiled traceable 168 bytes in 0.00 seconds yes. [eclipse 2]: liste([a, b, c]). yes. [eclipse 3]: liste(L). L = [] More? (;) ; L = [K] More? (;) ; L = [K, K] More? (;) ; L = [K, K, K] More? (;) yes. [eclipse 4]:
Es entspricht nicht der Definiton von linearen Resolutionsbeweisen, daß die gleiche Variable K mehrmals in der aufgebauten Liste vorkommt. Die Programmklausel mit der Variablen K wird zwar in der Tat immer wieder verwendet, jedoch muß jedesmal eine neue Variante dieser Klausel benutzt werden, in der K umbenannt ist in eine Variable, die bisher nirgends vorkommt.
Das obige Phänomen beruht in Wirklichkeit nur auf einer vereinfachten Darstellung bei der Ausgabe, die in vielen Prolog-Systemen üblich (wenn auch fraglich) ist. Das PROLOG-System Eclipse hat einen Modus, unter dem diese Vereinfachung der Darstellung nicht vorgenommen wird:
tahiti(bry): % eclipse ECRC Common Logic Programming System [sepia opium megalog] Version 3.4.5, Copyright ECRC GmbH, Thu Jun 16 11:08 1994 [eclipse 1]: set_flag(output_mode, "QPmV"). yes. [eclipse 2]: [user]. liste([]). liste([K | R]) :- liste(R). *** Warning: Singleton variables in clause 2 of liste/1: K user compiled traceable 168 bytes in 0.00 seconds yes. [eclipse 3]: liste(L). L = [] More? (;) ; L = [K_g684] More? (;) ; L = [K_g684, K_g692] More? (;) ; L = [K_g684, K_g692, K_g700] More? (;) yes. [eclipse 4]:
Es folgen einige nützliche PROLOG-Programme zur Listenbehandlung:
31
member(M, [M | _]). member(M, [_ | T]) :- member(M, T). append([], L, L). append([K|R], L1, [K|L2]) :- append(R, L1, L2). :- append([a,b,c], [d, e], L). L = [a, b, c, d, e] yes. :- append(L1, [d, e], [a, b, c, d, e]). L1 = [a, b, c] More? (;) ; no (more) solution. :- append([a, b, c], L2, [a, b, c, d, e]). L2 = [d, e] yes. :- append(L1, L2, [a, b, c, d, e]). L1 = [] L2 = [a, b, c, d, e] More? (;) ; L1 = [a] L2 = [b, c, d, e] More? (;) ; L1 = [a, b] L2 = [c, d, e] More? (;) ; L1 = [a, b, c] L2 = [d, e] More? (;) ; L1 = [a, b, c, d] L2 = [e] More? (;) ; L1 = [a, b, c, d, e] L2 = [] yes. :-
32
praefix(P, L) :- append(P, _, L). suffix(S, L) :- append(_, S, L). subliste_1(Sub, L) :- praefix(P, L), suffix(Sub, P). subliste_2(Sub, L) :- suffix(S, L), praefix(Sub, S). subliste_3(Sub, L) :- praefix(Sub, L). subliste_3(Sub, [_|R]) :- subliste_3(Sub, R). member_1(X, L) :- subliste([X], L).
33
naive_reverse([], []). naive_reverse([K|L], R_K) :- naive_reverse(L,R), append(R, [K], R_K).
34a
reverse(L, RL) :- hilfsreverse(L, [], RL). hilfsreverse([K|R], Akk, L) :- hilfsreverse(R, [K|Akk], L). hilfsreverse([], L, L).
Der folgende Beweis illustriert die Arbeitsweise des nichtnaiven reverse
< reverse([a, b, c, d], RL) > < hilfsreverse([a, b, c, d], [], RL) > < hilfsreverse([b, c, d], [a], RL) > < hilfsreverse([c, d], [b, a], RL) > < hilfsreverse([d], [c, b, a], RL) > < hilfsreverse([], [d, c, b, a], RL) > < hilfsreverse([], [d, c, b, a], [d, c, b, a]) >
34b
length([], 0). length([K|R], L1) :- length(R, L), L1 is L + 1.

zum Seitenanfang

Ein rekursiver Datentyp: die Liste


  • In PROLOG wird das Funktionssymbol  .  besonders unterstützt, das sogenannte Listenfunktionsymbol.
  • Eine PROLOG-Liste besteht entweder aus der leeren Liste [], oder aus einem Paar der Gestalt .(Kopf, Rumpf) wobei Rumpf eine Liste ist. Die Liste .(a, .(b, .(c, []))) kann der Einfachheit halber auch [a, b, c] dargestellt werden.
  • Ein Listenpaar .(Kopf, Rumpf) kann auch [Kopf | Rumpf] dargestellt werden.

5. Entwicklung von rekursiven Programmen am Beispiel von delete

Bei der Entwicklung eines PROLOG-Programms werden oft das prozedurale und das deklarative Denken kombiniert. Betrachten wir das Beispiel der Entfernung eines Elementes aus einer Liste:
delete(Liste, Element, NeueListe).
NeueListe ist eine Liste, die gleich wie Liste ist, außer daß sie kein Vorkommen von Element enthält. Es soll also gelten:
delete([a,b,c], b, [a,c]). delete([a,b,c,b], b, [a,c]). delete([a,b,c], e, [a,b,c]).
Der rekursive Teil der Definition lautet:
delete([K|R], E, L) :- K = E, delete(R, E, L). delete([K|R], E, [K|L]) :- K \= E, delete(R, E, L).
Der Basisfall ist offenbar:
delete([], _, []).
Man überzeugt sich, daß die Rekursion notwendigerweise terminiert: Bei jedem rekursiven Aufruf enthält das erste Argument ein Element weniger.

zum Seitenanfang

Entwicklung von rekursiven Programmen am Beispiel von delete


Bei der Entwicklung eines PROLOG-Programms werden oft das prozedurale und das deklarative Denken kombiniert. Betrachten wir das Beispiel der Entfernung eines Elementes aus einer Liste:
delete(Liste, Element, NeueListe).
NeueListe ist eine Liste, die gleich wie Liste ist, außer daß sie kein Vorkommen von Element enthält. Es soll also gelten:
delete([a,b,c], b, [a,c]). delete([a,b,c,b], b, [a,c]). delete([a,b,c], e, [a,b,c]).
Der rekursive Teil der Definition lautet:
delete([K|R], E, L) :- K = E, delete(R, E, L). delete([K|R], E, [K|L]) :- K \= E, delete(R, E, L).
Der Basisfall ist offenbar:
delete([], _, []).
Man überzeugt sich, daß die Rekursion notwendigerweise terminiert: Bei jedem rekursiven Aufruf enthält das erste Argument ein Element weniger.

6. Unifikation mit Occur-Check

Wie bereits im Kapitel 2 erwähnt, leistet die Unifikation von PROLOG keinen "Occur-Check"-Test. Einige PROLOG-Systeme geben jedoch die Möglichkeit, diesen Test einzuschalten.
Die folgende Sitzung zeigt, warum das Fehlen des Occur-Check nicht immer annehmbar ist:
:- a = f(a). no (more) solution. :- X = f(X). X = f(f(f(f(f(f(f(f(f(f(f(...))))))))))) yes. :- p(X, X) = p(Y, f(Y)). X = f(f(f(f(f(f(f(f(f(f(f(...))))))))))) Y = f(f(f(f(f(f(f(f(f(f(f(...))))))))))) yes. :- X = f(X), X = a. no (more) solution. :-
Beim Aufruf X = f(X). wird kein unendlicher Term aufgebaut, sondern lediglich der "Unifikator" {X = f(X)}. Bei der Darstellung des Wertes von X unterbricht das PROLOG-System die Darstellung ab einer gewissen Tiefe -- ohne natürlich dabei zu erkennen, daß es sich um einen "zyklischen Term" handelt.
Die Unifikation ohne Occur-Check kann wie folgt implementiert werden:
36
% Unifikation ohne Occur-check unify(X, Y) :- var(X), var(Y), X = Y. unify(X, Y) :- var(X), nonvar(Y), X = Y. unify(X, Y) :- nonvar(X), var(Y), X = Y. unify(X, Y) :- nonvar(X), nonvar(Y), atomic(X), atomic(Y), X = Y. unify(X, Y) :- nonvar(X), nonvar(Y), zg_term(X), zg_term(Y), unify_zg_term(X, Y). zg_term(X) :- functor(X, F, N), N \= 0. unify_zg_term(X, Y) :- X =.. [F|Xargumente], Y =.. [F|Yargumente], unify_list(Xargumente, Yargumente). unify_list([], []). unify_list([K1|R1], [K2|R2]) :- unify(K1, K2), unify_list(R1, R2).
Die folgende Sitzung erläutert die verwendeten PROLOG-Systemprädikate:
:- functor(f(a), F, N). F = f N = 1 yes. :- functor(f(a,b,g(c,d)), F, N). F = f N = 3 yes. :- functor(a, F, N). F = a N = 0 yes. :- f(a) =.. [F | Argumente]. F = f Argumente = [a] yes. :- f(a,b,g(c,d)) =.. [F | Argumente]. F = f Argumente = [a, b, g(c,d)] yes. :- a =.. [F | Argumente]. F = a Argumente = [] yes. :- unify( f(X,b), f(a,Y) ). X = a Y = b More? (;) ; no (more) solution. :- unify(X, Y). X = Y Y = Y More? (;) ; no (more) solution. :- unify(a, b). no (more) solution. :- unify(f(X), f(a)). X = a More? (;) ; no (more) solution. :- unify(f(X), f(f(X))). X = f(f(f(f(f(f(f(f(f(f(f(...))))))))))) yes. :-
37
% Unifikation mit Occur-check -- 1 unify(X, Y) :- var(X), var(Y), X = Y. unify(X, Y) :- var(X), nonvar(Y), not occurs_in(X, Y), X = Y. unify(X, Y) :- nonvar(X), var(Y), not occurs_in(Y, X), X = Y. unify(X, Y) :- nonvar(X), nonvar(Y), atomic(X), atomic(Y), X = Y. unify(X, Y) :- nonvar(X), nonvar(Y), zg_term(X), zg_term(Y), unify_zg_term(X, Y). zg_term(X) :- functor(X, F, N), N \= 0. unify_zg_term(X, Y) :- X =.. [F|Xargumente], Y =.. [F|Yargumente], unify_list(Xargumente, Yargumente). unify_list([], []). unify_list([K1|R1], [K2|R2]) :- unify(K1, K2), unify_list(R1, R2). occurs_in(X, Y) :- atomic(Y), !, fail. occurs_in(X, Y) :- var(Y), !, Y == X. occurs_in(X, [H|T]) :- !, occurs_in_list(X, [H|T]). occurs_in(X, Y) :- nonvar(Y), zg_term(Y), Y =.. [F|Yargumente], occurs_in_list(X, Yargumente). occurs_in_list(X, [H|_]) :- occurs_in(X, H). occurs_in_list(X, [_|T]) :- occurs_in_list(X, T).
Die folgende Sitzung bezieht sich auf Programm P37:
:- occurs_in(X, f(Y)). no (more) solution. :- unify(X, f(Y)). X = f(Y) Y = Y yes. :- unify(X, f(X)). no (more) solution. :- unify(X, [A, B, X, C]). no (more) solution. :- unify(X, [A, B, C, D]). X = [A, B, C, D] A = A B = B C = C D = D yes. :-

zum Seitenanfang

Entwicklung von rekursiven Programmen am Beispiel von delete


Wie bereits im Kapitel 2 erwähnt, leistet die Unifikation von PROLOG keinen "Occur-Check"-Test. Einige PROLOG-Systeme geben jedoch die Möglichkeit, diesen Test einzuschalten.
Die folgende Sitzung zeigt, warum das Fehlen des Occur-Check nicht immer annehmbar ist:
:- a = f(a). no (more) solution. :- X = f(X). X = f(f(f(f(f(f(f(f(f(f(f(...))))))))))) yes. :- p(X, X) = p(Y, f(Y)). X = f(f(f(f(f(f(f(f(f(f(f(...))))))))))) Y = f(f(f(f(f(f(f(f(f(f(f(...))))))))))) yes. :- X = f(X), X = a. no (more) solution. :-
Beim Aufruf X = f(X). wird kein unendlicher Term aufgebaut, sondern lediglich der "Unifikator" {X = f(X)}. Bei der Darstellung des Wertes von X unterbricht das PROLOG-System die Darstellung ab einer gewissen Tiefe -- ohne natürlich dabei zu erkennen, daß es sich um einen "zyklischen Term" handelt.
voriges Kapitel | Startseite | Inhalt | nächstes Kapitel

Letzte Änderung: 18. August 1998

Valid
	     XHTML 1.0!