Corso di Rappresentazione ed Elaborazione della Conoscenza

(Intelligenza Artificiale II)

Corso di Laurea in Informatica

Prof.ssa Stefania Costantini

 

 

 

Metaprogrammazione e Metainterpreti in Prolog

 

Si può considerare il Prolog come costituito da piu' parti: il linguaggio (logicamente 'puro' ) delle clausole di Horn, e le estensioni realizzate dai predicati predefiniti. Tra questi ultimi, l'insieme dei predicati meta-logici ('var', 'functor', 'arg', 'univ', 'clause', 'assert','retract',...) e' tale da includere in Prolog un metalinguaggio del linguaggio delle clausole di Horn.

 

Un'altra caratteristica importante di Prolog e' l'equivalenza formale tra strutture di dati e programmi, in quanto entrambi sono rappresentati come termini. Infatti, una clausola può essere considerata come un termine, avente come funtore ' :-' e come argomenti il termine

testa della clausola e il termine corpo della clausola. Se la clausola è unitaria, il termine corpo della clausola e' la costante 'true', altrimenti e' il termine costituito dai congiunti racchiusi in parentesi. Un programma (un insieme di clausole) può essere considerato come un termine, avente come funtore '.' e come argomenti il termine prima clausola e il termine costituito ricorsivamente dal resto delle clausole.

 

Da notare che questa caratteristica si basa a sua volta sull'adozione della stessa rappresen­tazione per predicati e funzioni. Ciò rende possibile che lo stesso simbolo sia usato in entrambi i ruoli:

 

p(f(t)) :-f(t).

 

Prolog sfrutta questa caratteristica mediante le primitive che operano a livello metalinguis­tico (i predicati metalogici), cioè che operano sui programmi trattandoli come strutture di dati.

 

Fondamentale tra esse e' la primitiva che consente di convertire un termine in un predicato eseguibile come meta. La possibilita', vista prima, di usare un simbolo o come termine o come predicato e' estesa in Prolog a termini che sono variabili, o direttamente (‘meta-vari­able facility'):

 

p(x) :- x.

 

o mediante il predicato predefinito 'cali':

 

p(X) :- call(X).

 

Quando, durante l'esecuzione, viene chiamata come meta, la variabile deve essere istanr,iata a un termine non variabile.

 

Le primitive metalinguistiche di Prolog sono essenziali per la metaprogrammazione, cioe' per estendere il linguaggio, costruire metaprogrammi (programmi che operano su altri programmi) ed esprimere metaconoscenza (regole su come usare regole).

 

Riguardo al primo aspetto, la metavariabile e' necessaria ad esempio per definire la negazione:

 

not X:-X,!,fail.

not X.

 

e per definire altri costrutti, quali:

or(X,y}:-X.           oppure                 X;

Y:-X.or(X, y) :-y.    oppure                 X; Y :- Y.

if_then_else(P,Q,R) :- P,!,Q.               

if_then_else(P,Q,R) :- R.               

Si possono inoltre definire predicati che rappresentano relazioni di secondo ordine; ad esempio, i seguenti:

 

 

ha_proprietà(L,P): tutti gli elementi della lista L hanno la proprietà,  P (predicato

                                            unario)

mappa_lista (L1, P, L2):   la lista L1 e' la mappa della lista L2 secondo il predicato (binario) P.

 

Potendo usare variabili predicative (alcuni Prolog consentono tale sintassi), tali relazioni si definirebbero nel modo seguente:

 

ha_proprieta([T | C], P) :- P(T), ha_proprieta(C, P).

ha _proprieta ([], _ ).

mappa_lista( [T1IC1],P, [T2 | C2]) :-P(T1, T2),
mappa_lista(C1 ,P,C2).

mappa_lista ([] ,_, []).

 

dove P ha, come dominio di valori, dei predicati.

Non disponendo di predicati variabili, questi possono essere realizzati con gli usuali costrutti metalinguistici di Prolog, come illustrato nel seguito.

 

ha_proprieta( [TIC] ,P) :-applica(P,T) ,ha_proprieta(C,P).

ha _proprieta ( [] ,_).

applica (maschio, X) :- maschio (X).

mappa_lista([T1|C1], P, [T2|C2]):-applica(P,T1, T2), mappa_lista(C1,P,C2).

mappa_lista([],_,[]).

 

applica (dizionario, X, Y) : -dizionario (X, Y) .

 

Ad esempio, con:

 

maschio(a).

maschio(c).

maschio(e).

maschio (g).

 

il quesito:

 

?- ha_proprieta([a,c,e,g] ,maschio).

ottiene risposta positiva (entrambi gli argomenti devono essere istanziati).

 

Con:

 

dizionario(cane, dog).

dizionario(gatto, cat).

dizionario(cavallo, horse) .

 

il quesito:

?-mappa_lista([cane,gatto,cavallo], dizionario, L).
ottiene la risposta: L = [dog,cat,horse].

In generale, e' necessaria una regola del tipo:

 

applica(funtore, X 1,... ,Xn) :- funtore(X1,... ,Xn).

 

per ogni nome e arietà di funtore. Alternativamente, si puoI elefinire il predicato applica(P,L) in modo che operi con qualunque funtore F e lista di argomenti L:

 

applica(F,L) :- G=..[F|L], G.

 

In questo caso, le relazioni precedenti vanno scritte nella forma:

 

ha_proprieta([T|C] ,P) :- applica(P,[T]) ,ha_proprieta(C,P).

ha _proprieta([],_) .

mappaJista([T1|C1], P, [T2|C2]) :- applica(P,[T1,T2]),
                                                                                  mappa_lista(C1,P,C2).

mappa_lista( [] ,_, []).

 

Questa soluzione, oltre ad essere piu' generale, consente di 'incapsulare' l'uso della metavariabile in un' unica metaregola.

La metaprogrammazione (definizione di metaprocedure, cioe' procedure che usano predicati metalogici) consente di rendere più espressivo il linguaggio delle clausole di Horn (che costituisce il linguaggio oggetto dei metapredicati e quindi del1c metaprocedure).

Numerose limitazioni del linguaggio oggetto possono essere superate estendendolo mediante inclusione di altre parti del metalinguaggio, in primo luogo quel1a parte che riguarda la rela7:ione di dimostrabilità del linguaggio oggetto. Se la le relazione e' rappresentata mediante il linguaggio oggetto stesso, ed eseguita dal suo interprete, essa costituisce un metainterprete (interprete del linguaggio scritto nel linguaggio stesso).

 

 

La definizione di base di un metainterprete Prolog e' la seguente:

 

dimostra (true).

dimostra ((A, B)) :- dimostra(A), dimostra(B) .

dimostra (A):- clause(A,B), dimostra(B).

 

Ad esempio, con il programma:

 

figlio_a(salvatore, pietro).

figlio_a(matteo, pietro).

figlio_a(mara, matteo).

coniuge(salvatore, anna).

fratello_sorella(F1, F2) :- figlio_a(F1 ,G), figlio_a(F2, G).

zio_a (Z,X) :- figlio_a(X, G), fratello_sorella(G, Y) ,coniuge(Y,Z).

 

ottiene risposta positiva il quesito:

 

?- dimostra (zio_a(anna, mara)) .

 

Dichiarativamente, la relazione 'dimostra(M)' e' vera se M e' vera rispetto al (deducibile dal) programma da interpretare (definito da 'clause'). Proceduralmente, simula il modello computazionale dell'interprete Prolog. Infatti, la seconda clausola di 'dimostra' esprime la regola di selezione del Prolog (scelta del predicato più a sinistra) e la terza effettua (mediante 'clause') la ricerca scquenziale delle clausole, con unificazione e ritorno indietro.

 

Volendo rendere esplicita la rappresentazione del programma, anziché affidarsi im­plicitamente alla rappresentazione interna utilizzata da 'c1ausc', si può definire:

 

dimostra(vero).

dimostra((A, B)):- dimostra (A), dimostra(B).

dimostra(A):- clausola (A :- B), dimostra(B).

 

insieme con la seguente rappresentazione delle clausole (sfrutta anch'essa la definizione degli operatori ':-', ',' che costituiscono funtori dei termini, per rappresentare le clausole come termini):

 

clausola(figlio_a (salvatore, pietro) :- vero) .

clausola(figlio_a (matteo, pietro) :- vero).

clausola(figlio_a (mara, matteo) :- vero) .

 

 

clausola(coniuge(salvatore,anna):-vero).

clausola(fratello_sorella(F1,F2):-(figlio_a(F1,G), figlio _a(F2,G))). clausola(zio_a(Z, X) :-(figlio_a(X,G),fratello_sorella(G,Y),

                        coniuge(Y,Z))).

 

La definizione di base del metainterprete e' semplice perché rimanda all'interprete (mediante 'clause') l'effettuazione della maggior parte del processo computazionale. Possono essere date definizioni diverse (diversi metainterpreti) in funzione di quali parti del processo computazionale vengono rese visibili nel programma (differenza di 'granularita ").

 

Il piu' semplice metainterprete e' costituito in Prolog da\l' LISO della metavariabile (o deI predicato 'cali'), in:

 

dimostra(A):- A.   oppure    dimostra(A) :- call(A).

 

E  in generale in ogni definizione in cui sia usata al posto della chiamata esplicita del predicato 'dimostra', ad esempio:

 

usa(A):-A.              al posto di             usa(A):- dimostra(A).

 

Si tratta di un'approssimazione della definizione di 'dimostra' cui pero' non si puo' dare direttamente una lettura dichiarativa.

E' da notare che, proceduralmente, e' necessario usare '!' nel1a definizione di 'dimostra':

 

dimostra (true): -!.

dimostra ((A, B)) :- !, dimostra(A), dimostra(B).
dimostra (A):- clause(A, B), dimostra(B) .

 

sia per maggiore efficienza; sia per evitare una scorretta applicazione incaso di ritorno indietro (la meta' dimostra(true) , in ingresso al1a terza clausola darebbe errore con 'clause(true, B)', perche' il primo argomento di 'clause' non puo' essere un predicato o un termine di sistema).

Alternativamente, se la meta ha un'unica soluzione, si puo' usare:

 

dimostra_una _volta(M) :- dimostra(M), !.

 

La definizione a tre clausole di 'dimostra' tratta solo clausole di Horn pure. Per consentire al metainterprete di trattare anche i predicati predefiniti di Prolog occorre aggiungere, in

 

terza posizione, la seguente clausola, che usa il predicato predefinito 'system' per operare un controllo:

 

dimostra (A) :- functor(A, F,_) ,system(F), ! ,call(A).

 

(l'argomento di 'system' deve essere un atomo, quindi 'functor' serve ad estrarre il funtore della meta).

Nel caso che ‘system’ non sia disponibile nella vesione di Prolog utilizzata, può essere ovviamente definito dal programmatore mediante fatti.

 

E' da notare però che il '!' sfugge al trattamento desiderato, perche' viene eseguito al livello delle clausole di definizione di 'dimostra' e non delle clausole del programma in cui e' inserito. Per ottenere l'effetto desiderato è necessario un predicato 'ancestor cut', ad esempio della forma !(ancestor), che opera cioè a livello delle clausole genitrici (esso e' disponibile in Wisdom Prolog e Waterloo Prolog, ma non in Edinburgh Prolog).

 

Il controllo inserito nella clausola aggiunta in 'dimostra' rende i predicati che lo soddisfano invisibili al metainterprete (rinviandone l'esecuzione all'interprete); esso può essere usato per rendere invisibili al metainterprete solo alcuni dei predicati predefiniti, indicando quali, ad esempio:

 

dimostra (A): -sistema (A), ! ,call (A).

sistema (A is B).

sistema (A < B).

 

...............

 

e rendendo visibili quel1i non indicati mediante apposite clausole del metainterprete, ad esempio aggiungendo:

 

dimostra(not A):- !, not dimostra(A).

dimostra (set_of(I, M, L)) :- ! , set- of(I, dimostra (M), L).

 

(l'ordine sarà: prima le prime due clausole della definizione generale, poi tali clausole specifiche, poi la clausola con il controllo di sistema, infine la terza clausola con 'clause').

 

Altre clausole con controlli possono essere usate per rendere invisibili al metainterprete alcuni predicati definiti dall'utente. Ad esempio, per rendere invisibili i predicati 'appar­tenenza' e 'concatenazione’, si può adottare uno dei due modi seguenti:

 

dimostra (true): -! .

dimostra ((A, B)) : -! I dimostra (A), dimostra (B) .

 

dimostra(appartenenza (E,L)) :- !, appartenenza(E,L).

dimostra(concatenazione(L1,L2,L3)) :- !,concatenazione(L1, L2, L3).

dimostra(A):- clause(A,B), dimostra(B).

 

oppure:

 

dimostra(true) : -! o

dimostra((A, B)): -! ,dimostra(A), dimostra(B).
dimostra (A) :- invisibile(A),! ,A.

dimostra (A):- clause(A, B), dimostra(B)
invisibile (appartenenza(E,L)).

invisibile (concatenazione(L1,L2,L3))o

 

In questo caso, dato ad esempio il programma:

 

sottoinsieme([],_).

sottoinsieme([T|C],I) :- appartenenza(T,I), sottoinsieme(C,I). appartenenza(E, [E|_]).

appartenenza(E, [J C]) :- appartenenza(E,C)o

 

con il quesito:

 

?-dimostra(sottoinsieme([b], [a,b,c])).

la procedura 'sottoinsieme' viene eseguita dal metainterprete, mentre la procedura 'appar­tenenza' viene eseguita direttamente dall'interprete Prolog.

Le relazioni di secondo ordine considerate prima possono essere definite mediante la chiamata di un opportuno metainterprete:

 

ha_proprieta([T|C],P) :- dimostra([P,T]), ha_proprieta(C,P).

ha _proprieta([], _).

 

dove le tre clausole di base di ' dimostra' vengono estese con la seguente quarta clausola:

 

dimostra([P,T]):- G=..[P,T],dimostra(G).

oppure:

 

mappa_lista([T1|C1], P, [T2|C2]) :- dimostra([P, T1, T2]),

                                                                                     mappa_lista(C 1 ,P,C2).

Mappa_lista( [] ,_, []).

 

dove le tre clausole di ' dimostra' vengono invece estese con la seguente quarta clausola:

 

dimostra ([P, T1, T2]) :-G=.. [P, T1, T2] ,dimostra(G).

 

(questa soluzione evita l'uso della metavariabile).

 

Diversi costrutti possono essere definiti usando i metainterpreti, evitando così di con­siderare estensioni separate per ciascun costrutto. La definizione di base del metainterprete può infatti essere elaborata in modo da realizzare diverse funzionalita' (gli esempi che seguono ne illustrano alcune).

 

Il seguente metainterprete ' conta (Meta, Numero)' da' i n uscita al secondo argomento il numero di chiamate (nodi dell'albero di dimostrazione) cui da' luogo la meta a cui e' istanziato in ingresso il primo argomento.

 

conta (true, O) :-!.

conta((A,B),N):-!,conta(A,NA),conta(B,NB),N is NA+NA.

conta(A,N):-clause(A,B),conta(B,NB),N is NB+1.

 

Ad esempio:

 

?-conta(appartenenza(a, [b,c,d,e,a]) ,N).   /* N=5 * /

p:-q,r.

q:-s,t.

r;-u,v.

u.

v. s.

t.

?-conta(p,N).   /* N=7 */

 

Un metainterprete che visualizza le mete chiamate è il seguente:

 

dimostra (true) :- !.

dimostra ((A,B)) :- ! ,dimostra(A) ,dimostra(B).

dimostra(A) :- clause(A,B), write(A),nl , dimostra(B).

 

Con il programma seguente:

 

f(a,b).

f(c,b).

f(d,c).

c(a,e).

 

 

fs(F1,F2) :- f(F1, G), f(F2, G).
z(Z,X) :- f(X,G), fs(G,Y), c(Y,Z).

 

e i quesito:

 

?-dimostra(z(e,d)).

 

si ottiene:

 

z(e,d).

f(d,c).

fs(c,_69).

f(c,b).

f(a,b).

c(a,e) .

 

Per ottenere la costruzione (in un termine) dell'intero albero della dimostrazione, basta aggiungere un argomento alla procedura 'dimostra':

 

dimostra (true, true) :- !.

dimostra ((A, B),(DA, DB)) :- !, dimostra(A, DA), dimostra(B, DB). dimostra (A, (A:-D)) :- clause(A,B), dimostra(B,D).

 

Dati i programmi precedenti, con il quesito:

 

?-dimostra(p,A).

si ottiene: A = p:-(q:-(s:-true), (t:-true)), (r:-(u:-true), (v:-true)).

 

e con il quesito:

 

?-dimostra(z(e,d) ,A).

 

si ottiene:

 

A = (z(e,d):-(f(d,c):-true),fs(c, a):-f(c,b):-true),(f(a,b):-true)),(c (a,e) :-true).

Il termine che rappresenta l'albero di dimostrazione, costruito dalla precedente procedura 'dimostra(Meta,Albero)', puo' essere passato ad un'altra procedura, ad esempio per visualizzarlo in modo piu' leggibile:

spiega(A) :- dimostra(A,D), nl, interpreta(D).

 

­dimostra (true, true) :- !.

dimostra ((A, B), (DA, DB) ) :- !, dimostra(A, DA), dimostra (B, DB).

dimostra(A,implica(D,A,B)) :-clause(A,B) ,dimostra(B,D).

interpreta(implica (true,A,true)) :- !, write (A), write(' E’UN FATTO),

                                                                                              nl,nl.

interpreta(implica(D,A,B)):- !, write(A),nl,tab(3),

write('SI PROVA USANDO LA REGOLA'), nl, write(B) ,nl, tab(3), write ('IMPLICA') ,nl, write (A), write ('.'), nl, nl, interpreta (D).

 

interpreta ((D 1 ,02)) : -! ,interpreta (D 1 ) ,interpreta (02) .

 

Con il quesito:

 

?-spiega(zio_a(anna,mara)) .

 

si ottiene:

 

zio_a (anna,mara) SI PROVA USANDO LA REGOLA

figlio_a(mara,matteo),

 

fratello_sorella(matteo,salvatore), coniuge(salvatore,anna)

     IMPLICA

zio_a(anna,mara).

 

figlio_a(mara,matteo) E' UN FATTO.

 

fratello_sorella(matteo, salvatore) SI PROVA USANDO LA REGOLA

 

figlio - a (matteo, pietro) , figlio - a (salvatore, pietro) IMPLICA

fratello _sorella(matteo,salvatore).

 

figlio_a(matteo,pietro) E' UN FATTO.

 

figlio_a(salvatore,pietro) E' UN FATTO.

 

coniuge(salvatore,anna) E' UN FATTO.

 

Un altro aspetto interessante dei metainterpreti è che essi possono essere composti tra loro, secondo diverse modalità.

 

Il seguente metainterprete 'dim_cont(Meta,Numero,Albero)' costituisce la composizione dei due precedenti metainterpreti 'conta(Meta,Numero)' e'dimostra(Meta,Albero)', ottenuta aggiungendo, al primo argomento comune ad entrambi, il secondo argomento dell'uno e il secondo argomento dell' altro.

 

dim_cont(true,O,true) :-!.

dim_cont((A,B) ,N, (DA,DB)) :- !, dim_cont(A,NA,DA),

                                 dim_cont(B, NB, DB),N is NA+NA. dim_cont(A,N,(A:-D)):-clause(A,B),dim_cont(B,NB,D),N is NB+1.

 

Un programma Prolog consiste usualmente di un'unica base di dati, non suddivisi bile in moduli (ma esistono alcuni sistemi Prolog che lo consentono). La modularità può essere ottenuta definendo un metainterprete con un argomento che rappresenta il contesto, e rappresentando esplicitamente le clausole in modo congruente, come di seguito illustrato.

 

dimostra(C,vero) :- !.

dimostra(C,(A,B)) :- !,dimostra(C,A) ,dimostra(C,B).

dimostra(C,A) :- clausola(C,A:-B) ,dimostra(C,B).

clausola(rossi, f(a,b):-vero).
clausola(rossi, f(c,b):-vero).
clausola(rossi, f(d,c):-vero).
clausola(rossi, c(a,e):-vero).

clausola(rossi, (fs(F1,F2):-(f(F1,G), f(F2,G)).
clausola(rossi, (z(Z,X) :-(f(X,G), fs(G,Y), c(Y,Z)))).

 

clausola(verdi, f(s,p) :- vero).

clausola(verdi, f(m,p) :- vero),

clausola(verdi, f(e,m) :- vero),

clausola(verdi, c(s,t) :- vero),

clausola(verdi, (fs(F1,F2) :- (f(F1,G), f(F2,G)))).

clausola(verdi, (z(Z,X) :- (f(X,G), fs(G,Y), c(Y,Z)))).

 

?-dimostra(rossi, z(e,d)).    /* yes */
?-dimostra(verdi, z(e,d)).    /* no */

?-dimostra(rossi, z(t,e)).    /* no */

?-dimostra(verdi, z(t,e)).    /* yes */

 

 

 

E' da notare che il metainterprete ‘dimostra(Contesto,Meta)’ può anche essere usato con il primo argomento non istanziato, leggibile come: "trova il Contesto in cui Meta vale":

 

?-dimostra (C,z(e, d)).          / * C = rossi  */

?-dimostra (C,z(t, e)).          / * C = verdi */

Un metainterprete può essere usato per modificare la strategia standard del Prolog, per esempio per far sì che la regola di selezione sia da destra a sinistra. Questo può essere utile per ottimizzare certi quesiti. Per esempio, dati:

 

nonno(X, Y) :-padre(X,Z) ,padre(Z, Y).

padre(a,b) .

padre(a,c) .

padre(a,d) .

padre(c,e).

padre(c,f).

padre(c,g).

 

con il quesito:

 

?-nonno(a, Y).

 

è più efficiente scegliere il primo predicato, mentre con il quesito:

 

?-nonno (X I f) .

 

è più efficiente scegliere il secondo. Definendo:

 

dimostra (true):- !.

dimostra ((A, B)):- !, dimostra(B), dimostra(A).

dimostra (A):- clause(A, B), dimostra (B) .

 

insieme con:

 

nonno_1(X, Y):- var(X), nonvar(Y),! ,dimostra(nonno(X,Y)).

nonno_1(X, Y):- nonno(X,Y).

 

i quesiti:

 

?-nonno _1 (a,f).

?-nonno_1 (X,Y).

 

saranno eseguiti direttamente dall'interprete, con la regola di selezione standard, mentre i quesiti:

 

?-nonno_1(a, Y).

?-nonno_1(X, f) .

 

saranno eseguiti dal metainterprete, con la regola di selezione modificata.

L'esempio mostra anche come e' possibile non solo eseguire un quesito o a livello oggetto (mediante l'interprete) o al metalivello (mediante il metainterprete) , ma in modo misto. Si può quindi metainterpretare solo quelle mete per le quali si vuole modificare l'inter­pretazione standard, eseguendo più e efficientemente le altre nel modo usuale.

Un altro problema affronta bile con la tecnica dei metainterpreti si presenta, sempre a causa dello scarto esistente tra interpretazione dichiarativa e procedurale, nel trattare le proprieta' delle relazioni, come la simmetria. La seguente rappresentazione, ad esempio, e' ridon­dante:

 

coniuge(a,b) .

coniuge (b, a).

.............

 

Dichiarativamente e' possibile, in alternativa:

 

coniuge(a,b).

.............

coniuge (X, Y): -coniuge (Y, X).

 

ma proceduralmente puo' andare in ciclo infinito (se entrambi gli argomenti non sono istanziati, o sono entrambi istanziati alla stessa costante).

 

Si potrebbe usare una relazione ausiliaria:

 

coniugi(X, Y) :- coniuge(X, Y).

coniugi(X, Y) :- coniuge(Y, X).

 

ma si tratta di un artificio. E' piu' naturale asserire:

 

simmetrica(coniuge).

 

e lasciare inferire le coppie simmetriche al metainterprcte, che può essere così e definito:

 

dimostra(true) :- !.

dimostra((A, B)) :- ! I dimostra (A), dimostra (B) .

dimostra(A) :- clause (A, B) I dimostra (B) .

dimostra(A) :- A=.. [F,A1,A2], simmetrica(F), B=..[F,A2,A 1],

                                   dimostra(B).

 

Con questa definizione, la relazione 'dimostra(Meta)' puoi essere letta come: "la Meta dimostrabile usando una strategia che usa la conoscenza di quali relazioni sono sim­metriche". Analogamente possono essere trattate le proprietà di riflessività e transitività.

 

Un altro aspetto simile e' quello dell'utilizzo della conoscen7.a sulla fun7.ionalita' di una relazione rispetto a un sottoinsieme degli argomenti. Una relazione binaria di tipo uno-molti e' funzionale rispetto a un argomento (nel senso che, ad esempio, per la relazione 'madre(Madre,Figlio)' ci possono essere diverse istan7.e di Figlio per ogni data istanza di Madre, ma c'e' un'unica istanza di Madre per ogni data istanza di Figlio); una relazione binaria di tipo uno-uno e' funzionale rispetto a entrambi gli argomenti (ad esempio per la relazione 'codice_fiscale(Persona,Codice)' c'e' un'unica istanza di Codice per ogni data istanza di Persona, e viceversa). La funzionalità di una relazione può essere sfruttata per rendere più efficiente la ricerca delle sue istanze; in riferimento agli esempi menzionati, il quesito:

 

?-madre(a,F) .

 

con l'esecuzione usuale ottiene più risposte, con ritorno indietro utile, mentre il quesito:

 

?-madre(M,c).

 

ottiene una sola risposta, con ritorno indietro inutile per cercarne altre.

 

Con entrambi i quesiti:

 

?-codice_fiscale(Var,cost).

?-codice_fiscale(cost, Var).

il ritorno indietro e' inutile.

Per evitare il ritorno indietro quando e' inutile, si puoi sfruttare la conoscenza sulla funzionalità della relazione asserendo tale informazione e usandola con un metainterprete:

 

uno_molti(madre(M,F), M, F).

uno_uno(codice_fiscale(P,N), P, N).

dimostra(true):- ! .

dimostra((A,B)):-!, dimostra(A), dimostra(B).

dimostra(A) :- uno_molti(A,X, Y),! ,dim_um(A,X, Y).

dimostra(A) :- uno_uno(A,X,Y),! ,dim_uu(A,X,Y).

dimostra(A) :- clause(A,B), dimostra(B).

dim_um(A,X, Y) :- nonvar(Y) ,clause(A,B) ,dimostra(B),!.

dim_um (A, X, Y):- var(Y) ,clause(A,B), dimostra(B).

dim_uu(A,X, Y) :- clause(A,B), dimostra(B),!.

 

In tal modo, se la meta compare come argomento di un fatto di predicato 'uno_molti' e ha il secondo argomento istanziato, viene eseguita una sola volta (per il cut nella prima clausola di 'dim_um'), mentre se il secondo argomento e' libero, viene eseguita lasciando possibile il ritorno indietro (con la seconda clausola di 'dim_um', senza cut). Se la meta compare come argomento di un fatto di predicato 'uno_uno', viene eseguita sempre senza ritorno indietro (per il cut nella clausola di 'dim_uu').

 

Anche in questo caso, come per la simmetria, si potrebbero adottare soluzioni specifiche alla relazione interessata, ma la soluzione con il metainterprete e' generale (vale per tutte le relazioni binarie dichiarate uno_molti o uno_uno) e consente di tenere la parte del programma descrittiva dell' applicazione separata dagli aspetti di controllo confinati unicamente a livello del metalinguaggio.

 

La composizione del metainterprete per la simmetria con quello per la funzionaIita' delle relazioni costituisce un esempio di un'altra modalita' di composizione, per semplice giustapposizione delle clausole specifiche:

 

dimostra(true) :- ! .

dimostra((A,B)) :- !, dimostra(A) ,dimostra(B).

dimostra(A) :- uno_molti(A,X,Y),! ,dim_um(A,X,Y).

dimostra(A) :- uno_uno(A,X,Y),! ,dim_uu(A,X, Y).

dimostra(A) :- A=.. [F,A1,A2], simmetrica(F),!,B=..[F,A2,A 1],call(B). dimostra(A):- clause(A,B), dimostra(B).

dim_um(A,X,Y) :- nonvar(Y), clause(A,B), dimostra(B),!.

 

 

dim_um(A, X, Y)  :- var(Y) ,clause(A,B) ,dimostra (B).

dim_uu(A, X, Y) :- clause(A,B), dimostra(B),!.

 

Date, insieme alle precedenti, le clausole:

 

madre(a,b) .

madre(a,c).

codice_fiscale(a,n1) .

codice_fiscale (b, n2).

 

i seguenti sono esempi di esecuzione:

 

?-dimostra (coniuge (b, a)).               /* yes */

?-dimostra(madre(X,c)) .                   /* X=a */

?-dimostra ( codice_fiscale (X, n1).       /* X=a */

?-dimostra ( codice_fiscale (b, X) ) .     /* X=n2 */

 

Una funzione importante nei 'gusci' ('shell') per sistemi esperti e' quella di interfaccia con l'utente, che riguarda sia la funzionalità di spiegazione (‘explanation'), sia quella di acquisizione di dati dall'utente ('query the user'). Per usare il Prolog per l'implementazione di sistemi esperti, occorre dotarlo di tale interfaccia. Per la funzionalità di spiegazione si può usare uno dei metainterpreti visti prima (o analoghi più sofisticati) per la visualiz­zazione dell'albero di dimostrazione. Per l'acquisizione di dati dall'utente si può definire un metainterprete come quello che segue(componendoli si otterrebbe l'interfaccia comples­siva) .

 

dim(true) :-!.

Dim((A,B)) :-! ,dim(A) ,dim(B).

dim(A) :-clause(A,B) ,dim(B).

dim(A) :-chiedibile(A), not noto(A), write(A), write('? '), read(R),

                 nl, risposta(R,A).

noto(A) :-vero(A); falso(A).

risposta (si,A) :-assert(A) ,assert(vero(A)).

risposta (no,A): -assert (falso (A)), fail.

 

Si ha ad esempio:

 

chiedibile(s).

chiedibile(t).

chiedibile(u).

chiedibile(v).

 

 

­p:-q,r,w.

q:-s,t.

r:-u,v.

W:-s,t,u,V.

?-dim(p). /* yes (dopo aver risposto 'si.' una volta per s,t,u,v */

Si presuppone cioe' che alcuni predicati possono essere oggetto di domande (quelli che compaiono come argomento di 'chiedibile'); le risposte ottenute vengono asserite, evitando cosi' di effettuare piu' volte le stesse domande.

Un ultimo esempio di metainterprete e' il seguente, che visualizza l'esecuzione di un quesito secondo il modello di Byrd:

 

dim(true) :- !.

dim((A,B)) :- ! ,dim(A) ,dim(B).

dim(A) :- dim_call(A) ,dim_clause(A,B), dim(B), dim_exit(A).

dim_call(A) :- write(call(A)) ,nl.

dim_call(A) :- write(fail(A)) ,nl,fail.

dim_clause(A,B) :- clause(A,B), dim_redo(A).

dim_redo(A).

dim _redo (A):- write(redo(A)), nl, fail.

dim_exit(A) :- write(exit(A)), nl.

Dati ad esempio:

p:-q,r.

q:-s.

q:-t.

r:-a,b,s,a.

?-dim(p).

 

Si ottiene la seguente visualizzazione:

 

call(p)

call(q)

call(s)

exit(s)

exit(q)

call(r)

call(a)

exit(a)

call(b)

fail(b)

redo(a)

fail(a)

redo(r)

fail(r)

redo(s)

fail(s)

redo(q)

call(t)

fail(t)

redo(q)

fail(q)

redo(p)

fail(p)