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.

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.

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.


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 (not A & C), mentre
P :-  C.
P :-  A, !, B.
ha significato dichiarativo P -> C v (A & B).

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


Predicati extralogici predefiniti


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. Le clausole lette vengono inserite in coda alle clausole già presenti nel database. Nel file possono anche essere presenti delle richieste all'interprete. Queste vengono eseguite immediatamente senza però fornire alcuna risposta sul terminale.

L'argomento 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 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.


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


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: