La sua diffusione è stata talmente ampia da diventare il linguaggio scelto dai Giapponesi per il progetto dei calcolatori della V Generazione. Il motivo di questo successo è da ricercarsi da un lato nella sua semplicità e naturalezza, dall'altro nella potenza espressiva della logica formale.
Per queste ragioni il Prolog si sta diffondendo anche fuori dagli ambienti prettamente scientifici, tanto è vero che sono già disponibili sul mercato varie implementazioni per personal computer (anche piccoli).
Va inoltre evidenziato il fatto che, a differenza di altri linguaggi di programmazione, per imparare il Prolog non è richiesto alcun prerequisito specifico; tanto è vero che tale linguaggio viene attualmente insegnato in via sperimentale (e con grande successo) agli alunni di alcune scuole elementari inglesi allo scopo di sviluppare capacità di ragionamento logico.
Oltre al Prolog nato a Marsiglia, esistono attualmente parecchie altre implementazioni, tra cui ricordiamo quelle di Edimburgo, Imperial College di Londra, Budapest, Waterloo (Canada), Palo Alto (California), Svedish Institute of Computer Science (Svezia).
genitore(pam,tom). genitore(tom,bob). figlio(Y,X) :- genitore(X,Y). ?- figlio(X,Y). X = tom Y = pam
Vediamo che i fatti in Prolog vengono scritti senza la freccia di
implicazione e terminano sempre con il punto. Nelle regole la testa (o
parte sinistra della regola) ed il corpo (o parte destra della regola)
sono unite dal simbolo ":-", proprio del linguaggio. Un eventuale
punto e virgola (;) all'interno del corpo di una regola rappresenta la
disgiunzione; una eventuale virgola (,) all'interno del corpo di una
regola rappresenta la congiunzione. Le regole terminano sempre con il
punto. Gli obiettivi sono clausole senza testa in cui al posto della
freccia d'implicazione iniziale è presente il simbolo "?-".
Il Prolog risponde alle domande (soddisfa gli obiettivi) secondo il Principio di risoluzione, cioè tenta di dimostrare un teorema, o meglio, tenta di derivare la clausola vuota a partire dalle ipotesi (i fatti e le regole). Il Prolog fornisce una risposta se c'è refutazione; risponde "no" se non c'è.
A B C D ... X Y Z;
a b c ... x y z;
0 1 2 3 4 5 6 7 8 9;
! " # $ % & ' ( ) = - ~ ^ \ | { } [ ] _ ` @ + ; * < > , . ? /
Gli atomi possono essere di due tipi:
_" inserito
nel corpo di un atomo per migliorarne la leggibilità.
Esempio:
tavolo 'Tavolo' tavolo_di_legno a113
:- ?-
Gli interi sono utilizzati per rappresentare numeri; sono costituiti da sole cifre e non possono contenere il punto decimale.
_" che è chiamato "indifferenza".
legge(giovanni,libro).
dove il predicato "legge" viene detto funtore e gli argomenti "giovanni"
e "libro" sono chiamati componenti. Una struttura mostra chiaramente
che il Prolog è basato sulla logica dei predicati.
[a, b, [c, d]]è una lista non vuota; [ ]è una lista vuota.
In una lista è possibile individuare due parti strutturalmente
significative: la testa e la coda. La testa di una lista è il primo
elemento della lista stessa; la coda è sempre una lista ed è quello
che rimane alla lista originaria dopo aver tolto la testa. La coda può
anche essere la lista vuota. La notazione per rappresentare la
distinzione fra testa e coda è una barretta verticale: [a|b].
Le operazioni più comuni sulle liste sono le seguenti:
membro(X,L)");
conc(L1,L2,L3)"
dove L1 e L2 sono due liste e L3
è la loro concatenazione);
permutazione(A,P)").
Definire una semantica procedurale per un linguaggio di programmazione significa definire una macchina astratta (cioè un modello astratto del programma) e definire come l'esecuzione delle varie istruzioni del linguaggio viene condotta su tale macchina. Il significato di un programma è definito dal comportamento della macchina astratta per effetto della esecuzione del programma, cioè dalla sequenza di stati che la macchina assume.
Un esempio per capire la differenza dei due significati è il seguente:
Data la clausola:
P :- Q, R.
Pè vero seQeRsono veri;
per risolvere il problemaP, prima risolvi il sottoproblemaQe poi risolvi il sottoproblemaR.
Il significato dichiarativo di un programma Prolog definisce se un dato obiettivo è vero rispetto a un dato programma e, in caso affermativo, per quali istanziazioni di variabili esso risulti vero.
Questa definizione è incentrata sul concetto di verità, equivalente a quello di conseguenza logica. Infatti dal punto di vista della dimostrazione di teoremi, l'interesse è quello di dimostrare la conseguenza logica, ma dal punto di vista della programmazione si è molto più interessati ai valori a cui vengono istanziate le variabili presenti nell'obiettivo; questi valori costituiscono infatti il risultato ottenuto in seguito all'esecuzione del programma.
Questa visione è tuttavia molto riduttiva; molti programmi possono infatti essere capiti solo in modo procedurale a causa della presenza di componenti non logiche che inquinano la corrispondenza fra linguaggio e logica formale, ma che portano molti vantaggi dal punto di vista pratico. Per questo si parla di "Prolog puro" e "Prolog impuro".
Il procedimento utilizzato per ottenere i valori cui istanziare le variabili presenti nel programma è il Principio di risoluzione.
Per il teorema:
P
è inconsistente,
G S {G}
se G è l'obiettivo
"?- B1, ..., Bn." con le variabili y1,...,yn, mostrare che il
programma P {G} è inconsistente è lo stesso che mostrare che
y1,...,yn (B1, ..., Bn) è conseguenza logica di P
dunque che G è vero rispetto a P.
L'obiettivo principale di un sistema di programmazione logica è dunque quello di calcolare valori. Introduciamo un importante concetto, quello di risposta corretta ottenuta per sostituzione, che fornisce una comprensione dichiarativa del risultato che si desidera ottenere da un programma e da un obiettivo.
Diamo la definizione di risposta calcolata ottenuta per sostituzione, definita mediante l'impiego della risoluzione diretta.
per P
{G}
è la sostituzione ottenuta restringendo la composizione delle sostituzioni
q1,...,qn alle variabili di G, dove q1,...,qn sono la sequenza di
"unificatori più generali" usati nella refutazione di
P
{G}.
Si vuole dimostrare che "Laura non è uno studente".studente(paolo). studente(mario). studente(giulio). maestra(laura). ?- not studente(laura). yes.
"not studente(laura)" non è conseguenza logica del programma;
ma non lo è neppure "studente(laura)".
Possiamo qui invocare una speciale regola di inferenza:
"se un atomo chiusoSecondo questa assunzione il mondo è chiuso nel senso che ogni cosa che esiste si trova nel programma o può venire derivata da esso. Di conseguenza se qualcosa non si trova nel programma, allora essa non è vera ed è vera la sua negazione.Anon è conseguenza logica di un programma, allora si inferiscenot A".
La regola di negazione come fallimento è definita nel seguente modo:
"dato il predicatonot(Obiettivo), seObiettivoha successo, alloranot(Obiettivo)fallisce, altrimentinot(Obiettivo)ha successo".
In Prolog:
dovenot(P) :- P, !, fail; true.
true è un obiettivo che ha sempre successo.
Inoltre per esprimere programmi che vogliono affermare qualcosa ed
escludere qualcos'altro si può utilizzare la combinazione dei due
predicati "cut" e "fail";
ma in genere è preferibile in casi del genere
utilizzare il predicato unario not tale che:
not(Obiettivo) = VeroObiettivo = Falso
Il not è un costrutto che offre vantaggi rispetto al predicato "cut";
infatti il programma diventa più leggibile e la formulazione è più
naturale.
Da notare è il fatto che il predicato "nor" definito come fallimento non corrisponde alla negazione della logica matematica. Inoltre, la regola di negazione come fallimento è corretta e completa.
Esempio.
"Maria ama tutte le piante tranne quelle carnivore" diventa:ama(maria,X) :- pianta(X), not_pianta_carnivora(X).
A1,...,Am
un nodo dell'albero e sia Am un atomo selezionato
dalla regola di risoluzione diretta. Questo nodo ha un discendente
per ciascun clausola
A
B1,...,Bn
tale che Am e A siano unificabili;
Esempio:
Dato il programma:
Il corrispondente albero di deduzione è:p(X,Z) :- q(X,Y),p(Y,Z). p(X,X). q(a,b). ?- p(X,b).
p(X,b)
/\
(b/Z)/ \(X/b)
/ \
/ \
q(X,Y),p(Y,b) (ramo di successo)
|
|
(a/X)(b/Y)|
|
p(b,b)
/ \
/ \
(b/X)(b/Z)/ \(b/X)
/ \
/ \
q(b,U),p(U,b) (ramo di successo)
------
(fallimento: non esistono
nel programma dati del
tipo q(b,...))
Teorema:
La regola di ricerca utilizzata dal Prolog combina la selezione della chiamata procedurale più a sinistra con la ricerca in profondità.
Una pila è una struttura dati i cui elementi sono in ordine uno sopra l'altro e a cui è possibile accedere attraverso due operazioni che permettono di inserire un elemento in cima alla pila (push) oppure estrarre l'elemento che è in cima alla pila (pop).
Un'istanza della pila di obiettivi rappresenta il ramo che si sta considerando; il calcolo diventa perciò una sequenza intervallata di operazioni push e pop sulla pila.
Una operazione push si ha quando la chiamata procedurale selezionata nel primo obiettivo della pila viene unificata con successo con la testa di una clausola del programma. Il risultato viene inserito nella pila.
Una operazione pop si ha quando non ci sono più clausole nel programma la cui testa si possa unificare con la chiamata procedurale del primo obiettivo della pila. Questo obiettivo viene rimosso mediante una operazione pop e si prende in considerazione il nuovo primo obiettivo della pila.
La ricerca in profondità può essere vista come una regola che specifica in che ordine debbano essere esaminate le clausole del programma. Negli ambienti standard Prolog l'ordine delle clausole è fisso; questo facilita l'implementazione, ma ha lo svantaggio che ad ogni esecuzione del programma le clausole vengono analizzate sempre nello stesso ordine. Per gli alberi infiniti è meglio una regola di ricerca che utilizzi una ricerca in ampiezza, ma questa ha lo svantaggio di essere poco compatibile con una implementazione efficiente.
Il metodo di ricerca in profondità consente di ottenere vantaggi dal punto di vista pratico di efficienza del programma, ma costituisce una forzatura del principio di risoluzione che comporta la perdita di certezza di risposta anche quando la risposta ci sarebbe. Questa è la ragione per cui comunemente si preferisce considerare il Prolog un linguaggio di programmazione piuttosto che un dimostratore di teoremi.
e dalle regole:P(a,b). P(c,b).
e dato l'obiettivo G:P(X,Z) :- P(X,Y), P(Y,Z). P(X,Y) :- P(Y, X).
si danno due casi:?- P(a,c).
Refutazione senza strategia di ricerca in
profondità
P(a,c)
{a/X} |
{c/Z} |
|
P(a,Y), (Y,c)
|
{b/Y} |
|
P(b,c)
|
{b/X} |
{c/Y} |
|
P(c,b)
|
|
P {G} ha una refutazione che
utilizza tutti i fatti e tutte le regole.
Refutazione con strategia di ricerca in profondità
P(a,c)
{a/X} |
{c/Z} |
|
P(a,Y), P(Y,c)
|
{b/Y} |
|
P(b,c)
|
{b/X}|
{c/Z}|
|
P(b,Y1), P(Y1,c)
{c/X} |
{Y1/Z} |
|
P(b,Y2), P(Y2,Y1), P(Y1,c)
|
(il risolvente diventa sempre più grande all'infinito)
Procedendo con la regola o strategia di ricerca del Prolog non si troverà mai una refutazione; questo è dovuto al fatto che le regole del programma P hanno una testa del tutto generale e possono unificarsi con qualsiasi sottobiettivo. Il Prolog, procedendo sempre "dall'alto in basso" secondo la strategia di ricerca in profondità, non arriverà mai a considerare la seconda delle due regole, cosa indispensabile ai fini della refutazione.
fail".
Il fail è un
predicato che fallisce sempre, forzando il backtracking che sarà
alterato dalla presenza del cut.
L'uso congiunto del cut e del fail
serve per tradurre in Prolog definizioni che contengono delle
esclusioni.
Per esempio, la frase
"Maria ama tutte le piante tranne quelle carnivore"può essere tradotta in:
ama(maria,X) :-
pianta_carnivora(X), !, fail.
ama(maria,X) :-
pianta(X).
Esempio:
maschio(paolo). /* fatto 1 */
femmina(francesca). /* fatto 2 */
uomo(X) :- /* regola 1 */
persona(X).
persona(Y) :- /* regola 2 */
maschio(Y).
persona(Z) :- /* regola 3 */
femmina(Z).
?- uomo(francesca).
yes.
Cos'è avvenuto? Il Prolog, quando cerca di soddisfare la regola 1 per
X=francesca,
richiama la regola 2 che fallisce, poiché fallisce l'obiettivo
:- maschio(Y) con Y=francesca.
Esegue un backtracking e torna alla
regola 1, richiama la regola 3, istanzia X=Z=francesca e ha successo
con il fatto 2.
Anche dopo una terminazione di successo l'utente può forzare il sistema
ad eseguire un backtracking alla ricerca di altre soluzioni. Questo
avviene mediante il comando interattivo attivato dal carattere ";".
Esempio.
persona(paolo). persona(giulio). persona(luca). ?- persona(X). X = paolo; X = giulio; X = luca; no.
Il cut è uno speciale meccanismo che permette di dire, quando la procedura Prolog riprende la ricerca lungo la catena degli obiettivi soddisfatti (disistanziando le variabili che hanno portato al fallimento), quali delle scelte fatte in precedenza devono essere salvate e quali no.
Sintatticamente il cut compare nel corpo di una
regola come obiettivo indicato dal carattere "!" e ha sempre successo.
L'obiettivo che causa l'attivazione della clausola contenente il cut è
detto obiettivo padre; l'atomo selezionato nel padre può essere
unificato alla testa della clausola che contiene nel corpo il cut.
A :- B, C.
B :- D, !, E.
D.
?- A.
A, B, C, D, E sono atomi.
L'atomo selezionato B nell'obiettivo :- B,C
causa l'introduzione del cut. L'atomo D è selezionato e ha successo.
Poi il cut ha successo, ma l'atomo E fallisce e il sistema torna
indietro al cut. A questo punto il sistema non continua nessun'altra
ricerca nella regola 2) e, se ce ne fossero, prenderebbe in
considerazione un'altra possibilità per soddisfare l'obiettivo ?- A.
Esempio:
ha significato dichiarativoP :- A, !, B. P :- C.
P
(A
B)
(¬A
C),
mentre
ha significato dichiarativoP :- C. P :- A, !, B.
P
C
(A
B).
Usando il cut si deve dunque prestare molta attenzione agli aspetti procedurali del programma.
Il goal
X is Y
ha successo solo se X si unifica con il risultato della
valutazione del termine con cui è istanziata Y. L'invocazione della
procedura is è l'unico modo per valutare una espressione aritmetica in
Prolog.
Le espressioni aritmetiche sono costruite con i seguenti operatori infissi:
X + Yper l'addizione; X - Yper la sottrazione; X * Yper la moltiplicazione; X / Yper la divisione intera; X mod Yper la valutazione del resto della divisione fra XeY.
I confronti fra valori numerici vengono effettuati con i seguenti goal, che usano operatori infissi corrispondenti a predicati sui numeri interi:
X=:=YX=\=YX<YX>YX>=YX=<Y
X =:= Y ha successo se le due espressioni aritmetiche
X e Y hanno lo stesso valore, mentre
X =\= Y ha successo se le due espressioni aritmetiche
X e Y hanno valore diverso.
consult fa leggere all'interprete il programma - costituito
da un insieme di clausole - contenuto in un file F.
Le clausole lette vengono
inserite in coda alle clausole già presenti nel database.
Nel file F possono anche essere presenti delle richieste all'interprete.
Queste
vengono eseguite immediatamente senza però fornire alcuna risposta sul
terminale.
L'argomento F deve essere un atomo i cui caratteri devono soddisfare le specifiche sui nomi dei file sul particolare sistema. Se il nome del file contiene caratteri speciali, tutto il nome va scritto fra apici. Ad esempio sono corrette le seguenti richieste:
?- consult(file).
?- consult('File').
?- consult('file.p').
Questa procedura permette anche di aggiungere direttamente da terminale
nuove clausole al database.
Ciò è possibile specificando "user" come
argomento.
In tal caso l'interprete resta in attesa che le nuove
clausole vengano scritte dall'utente, che termina la scrittura con un
carattere di end_of_file.
La procedura reconsult è analoga alla procedura consult e per essa
valgono le stesse regole.
La differenza consiste nel fatto che le nuove
clausole lette dal file F vengono sostituite e non aggiunte alle
clausole già presenti nel database aventi lo stesso predicato.
Questa
procedura è utile per la correzione di programmi, in quanto permette di
rileggere solo quei file che sono stati corretti senza ripartire da
capo.
arg(N,T,A)
arg(N,T,A) è usato per accedere a un particolare
argomento di un termine. I primi due argomenti (che vanno sempre
istanziati) specificano rispettivamente il numero dell'argomento e il
termine a cui ci si riferisce; il terzo argomento viene unificato con
l'argomento del termine richiesto.
atom(X)
atom(X) è usato per verificare se l'argomento X è un
atomo del Prolog.
atomic(X)
atomic(X) è usato per verificare se l'argomento X è un
atomo del Prolog (cfr. predicato atom) o un numero intero (cfr.
predicato integer).
functor(T,F,N)
functor(T,F,N) è usato in due possibili modi. Nel
primo, l'argomento T deve essere istanziato con un termine; in tal caso
F è unificato con il funtore principale di T, e N con un numero intero
indicante il numero di argomenti di T. Nel secondo modo, gli argomenti F
e N devono essere istanziati rispettivamente con un atomo e un numero
intero; in tal caso T è unificato con un termine avente F come funtore
principale e N argomenti variabili. Se però T è un termine atomico, F
e N sono unificati rispettivamente con T e con 0.
integer(N)
integer(N) è usato per verificare se l'argomento N è
istanziato con un numero intero.
name(A,L)
name(A,L) è usato per accedere ai caratteri che formano
un atomo. L è infatti la lista dei codici ASCII dei caratteri
dell'atomo A.
nonvar(X)
nonvar(X) è usato per verificare se l'argomento X è
istanziato con un termine che non è una variabile. Può essere
considerato il complementare del predicato var.
var(X)
var(X) è usato per testare se l'argomento X è istanziato
con una variabile.
=..
=.. (chiamato univ) serve a costruire o a scomporre delle
strutture. L'argomento L è una lista il cui primo elemento è il
funtore principale dell'argomento X e i cui altri elementi sono
nell'ordine gli argomenti di X.
asserta(C)
asserta(C) aggiunge in cima al database la nuova clausola
con cui è istanziato l'argomento C. Le variabili non istanziate nella
nuova clausola sono sostituite con nuove variabili interne.
assertz(C)
assertz(C) è analoga ad asserta, ma con la differenza che
la nuova clausola con cui è istanziato l'argomento C è aggiunta in
fondo al database.
call(X)
call(X) ha l'effetto di invocare come goal corrente il
termine con cui è istanziato l'argomento X e ha successo o fallisce se
e solo se questo goal ha successo o fallisce. Per tale ragione il
termine con cui è istanziato X deve essere interpretabile come goal.
Questa procedura è utile per introdurre dei goal variabili, soprattutto
in connessione col predicato =.. .
clause(P,Q)
clause(P,Q) è usata per accedere a delle clausole nel
database. Gli argomenti P e Q sono unificati rispettivamente con la
parte sinistra e la parte destra di una clausola presente nel database.
L'argomento P deve essere istanziato con un termine non variabile. Nel
caso di clausole unitarie (clausole senza parte destra), l'argomento Q
è unificato con il termine true.
listing(P)
listing(P) ha l'effetto di listare sul file di uscita
tutte le clausole presenti nel database il cui predicato della parte
sinistra è uguale all'atomo con cui deve essere istanziato l'argomento
P.
retract(X)
retract(X) ha l'effetto di cancellare dal database la
prima clausola che si unifica con l'argomento X, che deve essere
istanziato con un termine non variabile.
display(X)
display(X) scrive nell'uscita corrente il termine con cui
è istanziata X. Una struttura è scritta ignorando qualsiasi
dichiarazione di operatore, cioè con prima il funtore principale e poi
gli argomenti tra parentesi. Il goal display(X) ha successo solo una
volta.
get(X)
get(X) ha successo se il prossimo carattere scrivibile (con
codice ASCII maggiore di 32) nell'ingresso corrente si unifica con X. I
caratteri non scrivibili sono ignorati. Ha successo solo una volta. Il
risultato è l'unificazione di X con il carattere in ingresso.
get0(N)
get0(N) ha successo se N si unifica con il codice ASCII del
prossimo carattere nell'ingresso corrente. get0(N) ha successo solo una
volta, con il risultato di unificare N col carattere in ingresso.
nl
nl è sempre soddisfatto e provoca il passaggio alla linea di
scrittura successiva nell'uscita corrente.
print(X)
print(X) è usata per far scrivere sull'uscita corrente il
termine X con un formato specificato dall'utente, con una definizione
opportuna del predicato portray(X). print(X) si comporta come se fosse
definita nel modo seguente:
print(X) :- portray(X), !. print(X) :- write(X).
put(N)
put(N) ha successo solo una volta con il risultato di scrivere
sull'uscita corrente il carattere corrispondente al codice ASCII con cui
è istanziata N. Se N non è istanziata si ha un errore.
read(X)
read(X) ha successo solo una volta con il risultato di unificare
con il prossimo termine nell'ingresso corrente. Il termine deve essere
seguito da un punto "." e da almeno un carattere non scrivibile (con
codice ASCII minore o uguale a 32). Il punto finale viene eliminato. La
sintassi del termine deve soddisfare le dichiarazioni correnti di
operatori.
see(F)
see(F) definisce come ingresso corrente il file F. Si ha
errore se F non è istanziata o se il file specificato non esiste.
Normalmente l'ingresso corrente è user, a cui è associato il terminale
dell'utente.
write(X)
write(X) scrive nell'uscita corrente il termine con cui è
istanziata X. Se X non è istanziata, scrive il codice della variabile
interna corrispondente . Una struttura viene scritta tenendo conto delle
dichiarazioni correnti di operatori, cioè se l'operatore è infisso
questo viene inserito fra gli argomenti. Il goal write(X) ha successo
solo una volta.
tell(X)
tell(X) definisce il file F come uscita corrente. La prima
volta che tell è invocata per un file non ancora aperto, questo viene
aperto e, se esso non esiste, viene creato; se invece esiste già, il
suo contenuto precedente viene distrutto. Si ha errore se F non è
istanziata.
telling(F)
telling(F) ha successo solo se F si unifica con il nome del file
di uscita corrente.
told(F)
told(F) chiude il file di uscita corrente inserendo un
carattere di fine file e ripristina user (il terminale dell'utente) come
uscita corrente.
spy P
spy P inserisce dei punti di controllo (spy point) in
corrispondenza delle procedure indicate nel parametro P. Se il parametro
P è un atomo, i punti di controllo sono inseriti in tutte le procedure
aventi tale atomo come nome, indipendentemente dal numero di parametri.
Se P è una struttura con un solo argomento numerico, i punti di
controllo sono inseriti in tutte le procedure aventi come nome il
funtore della struttura e numero di argomenti pari al valore
dell'argomento numerico della struttura. Se P è una lista, ogni
elemento della lista deve essere un argomento lecito per spy e i punti
di controllo sono inseriti in tutte le procedure specificate nella
lista. Ad esempio, direttive lecite sono le seguenti:
spy append spy max(2) spy [append,max(2)].
debugging
debugging lista i punti di controllo (spy point)
nell'uscita corrente.
nodebug
nodebug elimina tutti i punti di controllo (spy point)
correnti.
nospy P
nospy P elimina tutti i punti di controllo (spy point)
specificati nell'argomento P. L'argomento P ha lo stesso formato
specificato nella procedura spy.
trace
trace ha l'effetto di attivare il tracciamento esaustivo
della esecuzione del programma, che corrisponde alla scrittura di ogni
invocazione di procedura con i relativi valori correnti delle variabili.
notrace
notrace interrompe il tracciamento esaustivo
dell'esecuzione del programma. Continua soltanto il tracciamento dovuto
alla presenza di punti di controllo.
Gli elementi che rendono impuro il Prolog puro sono: