Un linguaggio di programmazione logica: il Prolog


Cenni storici

Il Prolog (PROgramming in LOGic) fu progettato ed implementato a Marsiglia da Colmerauer e Roussel nel 1972. È basato sul principio di risoluzione di Robinson del 1965 e sull'interpretazione data da Kowalski della logica dichiarativa come insieme di istruzioni procedurali per un calcolatore [Kowalski 1979]. Da allora è stato utilizzato per un'infinità di applicazioni: integrazione simbolica, pianificazione, CAD, progetto di compilatori, interrogazione e descrizione di basi di dati, problemi meccanici, analisi del linguaggio naturale, robotica.

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).

Introduzione

Il Prolog è un linguaggio di programmazione logica basato sulle clausole di Horn. Un programma logico consiste in un insieme di procedure espresse in clausole di Horn ed attivate da una asserzione iniziale d'obiettivo. Considerando le clausole espresse secondo le due categorie di fatti e regole, diciamo che un programma Prolog è costituito da un insieme di fatti, che dichiarano un certo stato di cose, da un insieme di regole, che definiscono relazioni fra stati di cose, e da obiettivi (o domande) a cui rispondere. La procedura utilizzata dal Prolog è il Principio di risoluzione.

Esempio di programma Prolog

  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'è.

 

Logica del linguaggio


Sintassi

Un programma Prolog è costituito da un insieme di termini ognuno dei quali è costituito da un insieme di caratteri. I caratteri riconosciuti dal Prolog sono divisi in quattro categorie:
  1. insieme delle lettere maiuscole: A B C D ... X Y Z;
  2. insieme delle lettere minuscole: a b c ... x y z;
  3. insieme dei caratteri numerici: 0 1 2 3 4 5 6 7 8 9;
  4. insieme dei caratteri speciali: ! " # $ % & ' ( ) = - ~ ^ \ | { } [ ] _ ` @ + ; * < > , . ? /
I termini possono essere di vari tipi :
  1. Costanti
  2. Variabili
  3. Strutture
  4. Liste


Costanti

Le costanti indicano oggetti o relazioni fra oggetti. In Prolog esistono due tipi di costanti: gli atomi e gli interi.

Gli atomi possono essere di due tipi:

  1. sequenza di lettere e cifre che inizia con lettera minuscola oppure che inizia con lettera maiuscola, ma in questo caso la sequenza va messa fra apici. Si può usare il carattere speciale "_" inserito nel corpo di un atomo per migliorarne la leggibilità. Esempio:
    tavolo 'Tavolo' tavolo_di_legno a113
  2. simboli. Esempio:
    :- ?-

Gli interi sono utilizzati per rappresentare numeri; sono costituiti da sole cifre e non possono contenere il punto decimale.


Variabili

Le variabili, cioè i temini utilizzati per indicare una entità che al momento non si è in grado di nominare, sono rappresentati dalle lettere maiuscole o dal carattere "_" che è chiamato "indifferenza".


Strutture

Una struttura è un oggetto costituito da un insieme di termini, chiamati componenti, riuniti in una sola struttura per questioni di funzionalità. Una struttura è una entità del tipo:

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.

 


Liste

Una lista in Prolog è un termine particolare che si ottiene scrivendo fra parentesi quadre una sequenza ordinata costituita da un qualunque numero di elementi che possono essere atomi, strutture, liste o qualunque altro tipo di dato. Una lista può essere vuota o non vuota. Esempio:
[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:

 


Semantica


Semantica di fatti, regole, obiettivi

Vedi in Forma clausale e principio di risoluzione


Semantica di un programma


Introduzione
Dare una semantica dichiarativa per un linguaggio di programmazione significa fornire un metodo rigoroso per associare ad ogni programma del linguaggio l'oggetto da esso denotato. Nel caso della programmazione logica si tratta di fornire una interpretazione che soddisfi ogni programma del linguaggio.

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.


Semantica dichiarativa
La semantica dichiarativa di un programma logico dipende dalla semantica degli elementi che costituiscono il programma; questi elementi sono formule della logica del primo ordine alla cui semantica rimandiamo.

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 ==> G  S  {G} è inconsistente,

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.

Definizione.
Dato un programma P e un obiettivo G, una risposta corretta ottenuta per sostituzione per P  {G} è una sostituzione sulle variabili di G.
Questa definizione cattura il significato intuitivo di "risposta corretta", ma un sistema basato sulla programmazione logica può anche non dare risposta (il Prolog risponde "no"). In questo caso diciamo, in base al teorema sopra ricordato, che la risposta "no" è corretta se P  {G} è consistente.


Semantica procedurale
Il significato procedurale specifica come il Prolog risponde alle domande ed è basato sulla risoluzione diretta. La semantica procedurale del Prolog è una procedura per soddisfare un insieme di obiettivi nel contesto di un dato programma. La procedura dà in uscita la verità o falsità dell'insieme di obiettivi e le corrispondenti istanziazioni delle variabili.

Diamo la definizione di risposta calcolata ottenuta per sostituzione, definita mediante l'impiego della risoluzione diretta.

Definizione.
Dato il programma P e l'obiettivo G, una risposta calcolata ottenuta per sostituzione Th per P U {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 U {G}.


Apparato deduttivo del Prolog


Principio di risoluzione

Il PROLOG combina la risoluzione diretta all'indietro con il metodo di ricerca in profondità, seguendo un ordine fisso di analisi del programma: dall'alto in basso, da sinistra a destra. In questo modo il principio di risoluzione riesce ad essere implementato facilmente ed efficacemente; questi vantaggi comportano però una perdita di completezza della procedura dimostrativa di cui si parlerà nella sezione dedicata alla "metateoria".


Assunzione di mondo chiuso
Negazione come fallimento

Per dedurre logicamente un'informazione negativa da un programma sono necessarie regole speciali. La più importante è la regola di Negazione come fallimento, regola corretta e completa, controparte implementativa dell'Assunzione di mondo chiuso.
Assunzione di mondo chiuso
Consideriamo il programma:
studente(paolo).
studente(mario).
studente(giulio).
maestra(laura).
?- not studente(laura).
yes.
Si vuole dimostrare che "Laura non è uno studente".

"not studente(laura)" non è conseguenza logica del programma; ma non lo è neppure "studente(laura)". Possiamo qui invocare una speciale regola di inferenza:

Regola di Assunzione di Mondo Chiuso
"se un atomo chiuso A non è conseguenza logica di un programma, allora si inferisce not A".
Secondo 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.
Regola di negazione come fallimento
Uno dei modi per provare che un predicato non è conseguenza logica dei fatti e delle regole contenute nel programma e quindi poter inferire la sua negazione (Assunzione di mondo chiuso) è mostrare che esiste una deduzione che fallisce per la negazione di quel predicato.

La regola di negazione come fallimento è definita nel seguente modo:

"dato il predicato not(Obiettivo), se Obiettivo ha successo, allora not(Obiettivo) fallisce, altrimenti not(Obiettivo) ha successo".

In Prolog:

not(P) :- P, !, fail; true.
dove 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) = Vero  <==>  Obiettivo = 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).

Riferimenti

Vedi in Forma clausale e principio di risoluzione.


Metateoria


Correttezza e completezza della risoluzione diretta

La risoluzione diretta è una procedura dimostrativa corretta e completa solo per le clausole di Horn, cioè per quelle clausole che hanno al più un letterale negato.


Incompletezza del Prolog

Due sono le cause dell'incompletezza del Prolog: alcuni usi errati del costrutto di controllo "cut" e il metodo di ricerca in profondità che segue un ordine fisso di analisi delle clausole.


Incompletezza per il metodo di ricerca in profondità
La risoluzione diretta è corretta e completa per le clausole di Horn; il Prolog dunque utilizza una procedura corretta e completa. Ma il Prolog combina la risoluzione diretta con il metodo di ricerca in profondità e quest'ultimo, come dimostreremo, fa perdere la completezza.
Alberi di deduzione nel prolog
Definizione:
Dato un programma P, un obiettivo G e la regola di risoluzione diretta, un albero di deduzione per P{G} è definito nel seguente modo: Dato un programma P e un obiettivo G, diciamo che ogni ramo dell'albero è una derivazione di P  {G}:

Esempio:
Dato il programma:

p(X,Z) :- q(X,Y),p(Y,Z).
p(X,X).
q(a,b).
?- p(X,b).
Il corrispondente albero di deduzione è:
                     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:

Ipotesi
Se P {G} è inconsistente
Tesi
allora l'albero per P {G} ha almeno un ramo di successo.
Definizione di regola di ricerca
Una regola di ricerca è una strategia per trovare negli alberi di deduzione i rami di successo.

La regola di ricerca utilizzata dal Prolog combina la selezione della chiamata procedurale più a sinistra con la ricerca in profondità.

Implementazione della regola di ricerca
La regola di ricerca utilizzata dal Prolog combina la selezione della chiamata procedurale più a sinistra con la ricerca in profondità. Questa regola di ricerca è implementata per mezzo di una "pila" di obiettivi.

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.

Conclusioni
Secondo il teorema (2) degli alberi di deduzione, se P  {G} è inconsistente, allora l'albero per P  {G} ha almeno un ramo di successo. Ma un sistema di programmazione logica con una regola di ricerca in profondità e che segue un ordine fisso di analisi delle clausole del programma non può garantire di trovare sempre un ramo di successo. Dunque il Prolog non utilizza una procedura dimostrativa completa.

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.

Esempio
Dato il programma P composto dai fatti:
P(a,b).
P(c,b).
e dalle regole:
P(X,Z) :- P(X,Y), P(Y,Z).
P(X,Y) :- P(Y, X).
e dato l'obiettivo G:
?- P(a,c).
si danno due casi:

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.


Incompletezza dovuta al "cut"
Il cut non modifica la semantica dichiarativa dei programmi, ma può introdurre una indesiderabile forma di incompletezza nella procedura di refutazione. L'incompletezza dovuta al cut è differente da quella provocata dalla strategia di ricerca in profondità; infatti in questo caso il sistema si perdeva in una ricerca infinita e non dava mai una risposta. Qui invece è possibile che il sistema risponda "no" anche quando dovrebbe dare una risposta. È infatti questo il caso del "cut" usato congiuntamente al predicato predefinito "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).

Controllo del linguaggio


Costrutti di controllo


Backtracking

Se l'applicazione di una clausola del programma non ha portato ad una terminazione di successo, il Prolog, per procedere, deve analizzare una clausola alternativa. La procedura Prolog esegue automaticamente dei ritorni all'indietro per esaminare le diverse alternative (backtracking). Il Prolog abbandona tutta la parte dell'esecuzione che non ha avuto successo e ritorna indietro al punto da dove era iniziato il ramo fallimentare dell'esecuzione, rimuovendo tutte le istanziazioni di variabili attuate dopo quel punto. Questo assicura che il Prolog esamini sistematicamente tutti i possibili cammini alternativi finché non ne trovi uno che porti eventualmente al successo, o finché non dimostri che tutti portano al fallimento.

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

Il cut è un costrutto extralogico di controllo utilizzato per prevenire il backtracking; infatti il backtracking automatico è utile, ma se incontrollato può portare ad inefficienze nella esecuzione dei programmi.

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.


Esempio
  1. A :- B, C.
  2. B :- D, !, E.
  3. D.
  4. ?- 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.


Cut e completezza
Vedi Incompletezza dovuta al "cut".


Usi più comuni del Cut
Gli usi più comuni della funzione cut sono i seguenti:


Problemi con il Cut
Il cut, migliorando l'efficienza dei programmi e dando la possibilità di specificare le regole mutuamente esclusive, aumenta il potere espressivo del Prolog. Le riserve che si avanzano contro il cut derivano dal fatto che può causare la perdita di corrispondenza fra significato dichiarativo e procedurale dei programmi. Infatti con la presenza del cut bisogna prestare molta attenzione all'ordine delle clausole nel programma, cosa che diventa fondamentale per il significato dichiarativo.

Esempio:

P :-  A, !, B.
P :-  C.
ha significato dichiarativo P <== (A&B) v (¬A&C), mentre
P :-  C.
P :-  A, !, B.
ha significato dichiarativo P <== Cv(A&B).

Usando il cut si deve dunque prestare molta attenzione agli aspetti procedurali del programma.

 


Predicati extralogici predefiniti


Espressioni aritmetiche

Le espressioni aritmetiche in Prolog sono costituite da variabili e costanti numeriche e da speciali operatori che corrispondono a funzioni valutabili dall'interprete. Al momento della valutazione si assume che una variabile sia istanziata con un numero o con una espressione aritmetica. Il risultato della valutazione, quando possibile, è un numero intero. In alcuni interpreti Prolog sono disponibili anche i numeri a virgola mobile.

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 + Y per l'addizione;
X - Y per la sottrazione;
X * Y per la moltiplicazione;
X / Y per la divisione intera;
X mod Y per la valutazione del resto della divisione fra X e Y.

I confronti fra valori numerici vengono effettuati con i seguenti goal, che usano operatori infissi corrispondenti a predicati sui numeri interi:

X =:= Y
X =\= Y
X < Y
X > Y
X >= Y
X =< 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.

 


Predicati per la lettura di programmi

La procedura 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.


Operazioni su termini e strutture

Le operazioni avanzate su termini e strutture di un programma possono essere effettuate con i seguenti predicati:
arg(N,T,A)
Il predicato 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)
Il predicato atom(X) è usato per verificare se l'argomento X è un atomo del Prolog.
atomic(X)
Il predicato 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)
Il predicato 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)
Il predicato integer(N) è usato per verificare se l'argomento N è istanziato con un numero intero.
name(A,L)
Il predicato 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)
Il predicato 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)
Il predicato var(X) è usato per testare se l'argomento X è istanziato con una variabile.
=..
Il predicato =.. (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.


Predicati per operazioni su clausole

asserta(C)
La procedura 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)
La procedura 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)
La procedura 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)
La procedura 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)
La procedura 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)
La procedura 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.

 


Procedure di ingresso e di uscita

Le procedure d'ingresso e d'uscita sono quei costrutti predefiniti che vengono usati per leggere e per scrivere dati nei file. Essi sono:
display(X)
La procedura 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)
Il goal 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)
Il goal 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
Il goal nl è sempre soddisfatto e provoca il passaggio alla linea di scrittura successiva nell'uscita corrente.
print(X)
La procedura 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)
Il goal 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)
Il goal 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)
La procedura 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)
La procedura 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)
La procedura 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)
Il goal telling(F) ha successo solo se F si unifica con il nome del file di uscita corrente.
told(F)
La procedura told(F) chiude il file di uscita corrente inserendo un carattere di fine file e ripristina user (il terminale dell'utente) come uscita corrente.


Procedure per la correzione di programmi: debugging

spy P
La direttiva 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
La procedura debugging lista i punti di controllo (spy point) nell'uscita corrente.
nodebug
La direttiva nodebug elimina tutti i punti di controllo (spy point) correnti.
nospy P
La direttiva 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
La direttiva 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
La direttiva notrace interrompe il tracciamento esaustivo dell'esecuzione del programma. Continua soltanto il tracciamento dovuto alla presenza di punti di controllo.


Prolog puro e Prolog impuro

Esiste una distinzione fra Prolog puro e Prolog impuro: il Prolog puro è il linguaggio strettamente corrispondente alla logica formale; gli elementi che lo "inquinano" introducono discrepanze fra il Prolog e la pura logica formale, ma vengono altresì utilizzati in larga misura per gli indubbi vantaggi pratici di cui sono portatori.

Gli elementi che rendono impuro il Prolog puro sono: