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


Fatti, regole, obiettivi


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 q 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}.


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

Esempio.

Maria ama tutte le piante tranne quelle carnivore, diventa: ama (maria, X) :- pianta (X), not_pianta_carnivora (X).


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}:

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  {G}    ha    una    refutazione     che               
utilizza  tutti  i  fatti  e tutte  le  regole          P (c,b)
                                                                                        
                                {b/X}
                                      {c/Y} 
                                          P (c,b)
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 piu' 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).