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 rappresentazione
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 metalinguistico (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-variable 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 implicitamente 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 'appartenenza' 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 'appartenenza'
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 considerare
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'interpretazione 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' ridondante:
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
simmetriche". 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 visualizzazione 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
complessiva) .
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)