invece un modello che parte dalla logica per arrivare agli oggetti. Ma prima di fare ciò dobbiamo porre le
basi per capire quello che vogliamo fare. In questa prima parte parleremo del perché gli oggetti sono così
importanti tanto che oggi si dà per scontato che i programmi siano ad oggetti, e delle peculiarità della LP.
Alla fine della esposizione dovremmo essere convinti dei seguenti fatti
• gli oggetti hanno proprietà tali da renderne giustificato l’ampio uso che se ne fa oggi,
• la LP ha delle interessanti caratteristiche assenti nella OOP e che la rendono per certi versi
complementare a questa;
• vale la pena tentare di integrare in maniera organica
OOP ed LP.
Parte Prima: Differenze tra OOP e LP
LP e OOP
5
Capitolo 1
Differenze tra OOP ed LP
Verranno illustrate le caratteristiche della LP e messe in risalto le differenze con la OOP. Inoltre
mostreremo gli usi naturali dei due strumenti.
1.1. Caratteristiche della LP
Vediamo le peculiarità della LP che la differenziano dagli altri modelli, per capire perché questo modello merita
attenzione, non solo sul piano concettuale, ma anche su quello pratico. Vogliamo cioè capirne l’utilità ed il tipo di
applicazioni per cui l’uso risulta più naturale. Una volta appreso che la LP ha molto da offrire nei confronti della OOP,
nel senso che per certe applicazioni ci dà dei vantaggi (in termini di semplicità di utilizzo, chiarezza, prestazioni, o
quant’altro), se vi sono delle limitazioni, avrà senso compiere sforzi al fine di porvi rimedio. Elenchiamo le
caratteristiche della LP dividendolo in peculiarità, che la differenziano da ogni altro tipo di modello, e caratteristiche
secondarie, che sono anche del modello funzionale ma non di quello imperativo.
Peculiarità:
• dichiaratività dei programmi
• invertibilità: uno stesso programma può risolvere più problemi
• possibilità di fare inferenze
• motore di ricerca su alberi già implementato con possibilità di backtracking
• unificazione
Caratteristiche secondarie:
• mancanza di tipi
• variabili senza assegnamento
• mancanza di stato e quindi di effetti collaterali
• mancanza di interazione
1.1.1. Peculiarità della LP
Per peculiarità intendiamo quelle caratteristiche della LP che la differenziano da ogni altro tipo di modello (imperativo
o funzionale). Vediamole una ad una.
1.1.1.1. Dichiaratività dei programmi
Per dichiaratività si intende che si specifica il cosa fare e non il come farlo. Mostriamo questo con il classico esempio
dell’append di due liste
append([],L1,L1).
append([H|T],L1, [H|L]) :- append(T,L1,L).
Il programma coincide con la specifica, non abbiamo menzionato strutture di nessun tipo né algoritmi. Il come è
nascosto dentro la macchina LP. Si provi a pensare ad un programma analogo in linguaggio imperativo, ad esempio il
C, si noterà immediatamente la differenza in termini di comprensibilità e di facilità di scrittura.
Dalla dichiaratività deriva la seguente interessante caratteristica:
• invertibilità: uno stesso programma può risolvere più problemi
1.1.1.1.1. Invertibilità: uno stesso programma può risolvere più problemi
Abbiamo costruito l’esempio dell’append pensandolo come programma che riceve in ingresso due liste (i primi due
parametri) e fornisce in uscita la lista concatenazione delle due nel terzo parametro. Un tipico goal può essere come il
seguente
:- append([3,5],[6,7,8,9],L).
yes, L/[3,5,6,7,8,9]
Ma possiamo usare lo stesso programma per fare la “differenza” di due stringhe:
:- append(L,[6,7,8,9], [3,5,6,7,8,9]).
yes, L/[3,5]
Lo abbiamo cioè usato in maniera inversa rispetto a quella in cui lo avevano progettato. Questa è una diretta
conseguenza della dichiaratività, non specificando il come non abbiamo neanche fatto commitment che pregiudicassero
l’invertibilità.
1.1.1.2. Possibilità di fare inferenze
La possibilità di fare inferenze significa che a partire da un certo insieme di informazioni e di regole possiamo derivarne
delle altre.
Parte Prima: Differenze tra OOP e LP
LP e OOP
6
fatti + regole di inferenza → informazioni derivate
E’ senz’altro la caratteristica più interessante ed usata della LP. Risulta del tutto naturale scrivere delle relazioni tra
termini, i quali rappresentano le entità del discorso, e cioè dei fatti che risultano sempre veri. Possiamo poi scrivere
delle “relazioni tra relazioni”, nel senso che una è vera se lo sono delle altre, usando la logica del primo ordine. Con un
numero di istruzioni (fatti e regole) finito possiamo rappresentare una quantità di conoscenza illimitata. Inoltre tali
informazioni possono non essere a priori a conoscenza di chi scrive la il programma, il quale si limita solamente a
descrivere il dominio del problema con fatti e regole, ed ad usando solo in un secondo momento la capacità inferenziale
della LP, per derivare informazioni di cui prima non era a conoscenza. Questa caratteristica è il motivo del successo
della LP in intelligenza artificiale.
1.1.1.3. Motore di ricerca su alberi già implementato con possibilità di backtracking
La risoluzione di un goal è per sua natura una ricerca su un albero. In tale ricerca, ad ogni bivio, si tengono memorizzati
i punti di scelta in modo da potere tornare indietro se il ramo preso si dovesse rivelare fallimentare. Quindi non vengono
mai fatti commitment, ma tutte le alternative vengono sempre lasciate aperte, questo permette non solo di tornare
indietro se si arriva ad un fallimento ma anche di trovare tutte le soluzioni se ve ne sono più di una. La ricerca su alberi
è un problema ricorrente, soprattutto in intelligenza artificiale, avendo questa caratteristica gratis incorporata nel motore
inferenziale LP, è ovvio che problemi di questo tipo sono facilmente specificabili usando tale modello.
1.1.1.4. Unificazione
L’unificazione è il meccanismo che sta alla base della LP e che permette la dichiaratività e l’invertibilità dei programmi
ed il backtracking. Permette di usare una semantica diversa per le variabili di quella dell’assegnamento a cui siamo
abituati. Permette di scrivere funzioni e predicati parzialmente specificati, nel senso che una stessa funzione (predicato)
con variabili può rappresentare una serie di funzioni ground. Quale di questo insieme verrà deciso solo durante la
risoluzione. Ad esempio la funzione
f(X,a)
è un modo per indicare tutta un insieme di funzioni ground come le seguenti
f(a,a)
f(b,a)
f(g(a),a)
…
Il secondo uso delle variabili è quello di passaggio dei parametri (si pensi all’interpretazione procedurale).
1.1.2. Caratteristiche secondarie della LP
Per caratteristiche secondarie intendiamo quelle che sono anche del modello funzionale ma non di quello imperativo.
Tutte le caratteristiche esposte sono date in senso negativo, come mancanze, ma lo sono rispetto ai linguaggi imperativi,
quindi non vanno visti come difetti, almeno per il momento, ma come differenze. Infatti una mancanza può risultare
utile perché semplifica certi usi. Discuteremo in seguito quali di esse sono realmente delle limitazioni. Ricordiamo che
comunque la LP è computazionalmente completa, è cioè equivalente alla TM. Vedremo però che in molti casi le TM
sono insufficienti. Discutiamo queste caratteristiche una ad una.
1.1.2.1. Mancanza di tipi
La mancanza di tipi non è una limitazione concettuale ma pratica. In realtà, in nessun linguaggio realmente utilizzabile
possiamo fare a meno dei tipi, ma quello che si intende per tipi in LP è ben diverso da quello che si intende in OOP. In
LP possiamo indicare gli interi con:
0, s(0), s(s(0)), …
una lista con:
[primo, secondo, terzo, quarto]
un albero con
[radice, SINISTRA, DESTRA]
il tipo “persona” con
persona(X).
e così via. Però il significato di tali termini viene dato da chi legge, per il motore inferenziale sono simboli (termini)
qualunque. La differenza è chiara se si pensa all’uguaglianza:
f(a)=f(a).
yes
f(a)=f(b).
no
X=f(a).
yes, X/f(a)
Parte Prima: Differenze tra OOP e LP
LP e OOP
7
Viene controllata solo l’uguaglianza sintattica e se uno dei due termini è una variabile non legata viene data la
condizione di unificazione affinché la relazione sia valida. Con il seguente predicato:
5=5.
yes
abbiamo semplicemente verificato che i due simboli sono uguali senza far riferimento ad alcun valore; difatti se
scriviamo:
2+3=5.
no
la risposta è no perché la stringa “2+3” è diversa dalla stringa “5”. In questo non c’è nulla di strano se non per il fatto
che a certi simboli (come quelli numerici) siamo abituati a dare sempre e solo uno stesso significato. Per usare i numeri
come tali e non come simboli è stato introdotto il predicato “is”:
2+3 is 5.
yes
Questo è un predicato non logico introdotto solo per motivi pragmatici e che lavora come nei sistemi imperativi. In
pratica “l’is” prima valuta il termine di sinistra e quello di destra e poi confronta i due valori associati ai simboli. Se
scriviamo:
2 # 3 is 5.
Il predicato fallisce perché il concetto di tipo è legato a quello di operazioni associate al tipo stesso. Quindi le
operazioni aritmetiche (come quella denotata da “+”) sono consentite tra numeri, mentre “#” non denota nessuna
operazione predefinita tra numeri. In LP per le “operazioni” si usano i predicati ma questi non sono associati a nulla.
Concludendo, in OOP i tipi definiscono dei sottoinsiemi nel dominio D dei significati (un dominio per ogni tipo) e a
ciascuno assegna delle operazioni proprie di quel dominio. In LP non esiste il concetto di tipo come in OOP: per il
motore inferenziale non c’è alcun dominio D, è il programmatore che considera il suo dominio D e che assegna alla lista
[radice, SINISTRA, DESTRA]
il suo “tipo” di albero. Non essendoci tipi non ci possono neanche essere operazioni associate e così, ad esempio, un
predicato (“operazione”) che ha come primo termine una lista a tre argomenti non può fare differenze se tale lista
rappresenta un albero o meno. Non vengono quindi fornite costrutti per lavorare in maniera semplice con dei tipi
predefiniti, ma è il programmatore a farsene carico.
1.1.2.2. Variabili senza assegnamento
La mancanza di assegnamento è una diretta conseguenza della mancanza di stato e di effetti collaterali. In LP le
variabili hanno un significato molto differente da quello a cui siamo abituati in linguaggi imperativi. In
programmazione imperativa quando scriviamo:
tipoX nomeVar;
nomeVar=valy;
nomeVar=<espressione>;
nella prima riga definiamo un simbolo (nomeVar) dicendo che è di tipo tipoX, cioè può assumere, in ogni momento,
uno (e un solo) valore nel domino di tipoX o nessun valore.
La seconda riga valuta il simbolo a destra dell’operatore di assegnamento e lo attribuisce a nomeVar. La terza elimina
il collegamento precedentemente creato e ne stabilisce uno nuovo tra nomeVar e il valore di <espressione> (valz).
sintassi semantica
nomeVar
tipoX
S
D
Parte Prima: Differenze tra OOP e LP
LP e OOP
8
Il concetto di variabile in LP è ben diverso. Innanzitutto non può essere di un tipo, come abbiamo già visto, e quindi il
suo dominio di valori è in pratica tutto D che coincide con S ovvero con l’universo di Herbrand H(P) più il simbolo di
underscore (_) per indicare l’assenza di legame. Ma la differenza principale è nella mancanza dell’operatore di
assegnamento, difatti scrivendo
X=a
la variabile X viene legata alla costante a. Viene legato proprio al simbolo e non al suo valore perché non ne ha, o
meglio, esso coincide con il simbolo stesso. Inoltre questo legame non si può sostituire, ad esempio scrivendo poi
X=b
vi sarebbe un fallimento. “L’assegnamento” è unico e può essere sciolto solo in backtracking. In pratica la variabile in
LP è solo un modo per dare un nome diverso ad un termine. E’ come le variabile nelle funzioni, ad esempio
f(x)=x
2
+3x+2
è un modo compatto per denotare una collezione di espressioni che appartengono ad una stessa classe (quelle la cui
struttura è come quella indicata):
f(0)=0
2
+3*0+2
f(1)=1
2
+3*1+2
f(2)=2
2
+3*2+2
…
Così anche la clausola
f(a, X) :- g(X).
denota una collezione
2
di clausole:
f(a, y) :- g(y).
f(a, f(y)) :- g(f(y)).
f(a, g(a,b)) :- g(g(a,b)).
…
Come per la funzione, in cui la visibilità della variabile x è l’espressione che segue, anche per le clausole la visibilità
della variabile è la clausola stessa. Questo mette in luce il significato denotazionale della variabile logica, in contrasto
con le variabili imperative che sono visibile all’interno di un blocco entro cui possono assumere valori diversi. In
linguaggi imperativi per visibilità si intende la porzione di codice entro cui si può usare la variabile (il suo valore). Si
ricorda che, a differenza delle funzioni, non vi è il passo successivo di valutazione (f(0)=0
2
+3*0+2=2) perché il valore è
la clausola stessa. In LP si possono legare due variabili
X=Y
se poi Y viene legata al termine “a” anche X lo sarà. Se in un linguaggio imperativo scriviamo
x=y
dove x e y sono due variabili dello steso tipo a cui non è stato assegnato ancora alcun valore, si verifica un errore perché
il simbolo y viene valutato per conoscere il suo valore e questo non esiste. Per una semantica simile a quella LP si
devono usare espressamente i puntatori.
1.1.2.3. Mancanza di stato e quindi di effetti collaterali
Questo concetto si ricollega al precedente quando abbiamo detto che i legami delle variabili possono essere eliminati
soltanto in backtracking e non si possono sostituire. Abbiamo dovuto farne a meno per potere fare computazioni con
semplici trasformazioni simboliche. Ad esempio con la clausola
f(a) :- g(a).
vogliamo che durante una derivazione f(a) sia sempre sostituibile con g(a) senza tenere conto di fattori contingenti,
rappresentabili tramite lo stato, che possono fare in modo che tale sostituzione sia lecita in certi casi e non in altri.
Consideriamo l’esempio
pioggia → bagnato(prato) (bagnato(prato) :- pioggia.)
A bagnato(prato) posso sostituire pioggia. Per decidere la verità di questo predicato potrei guardare fuori dalla
finestra ed osservare lo stato del tempo. Questo non è ovviamente esprimibile sintatticamente perché dipende da fattori
contingenti che possono variare nel tempo facendo variare la verità del predicato, mentre in LP si esprimono verità
2
La collezione può anche essere infinita ma è comunque numerabile.
sintassi semantica
nomeVar
valy
valz
S
D
tipoX
Parte Prima: Differenze tra OOP e LP
LP e OOP
9
assolute senza alcuna nozione di tempo: la stessa teoria interrogata con lo stesso goal darà sempre lo stesso risultato.
Questo non è compatibile con lo stato che è invece modificabile nel tempo direttamente dal programma stesso ma anche
e soprattutto da agenti esterni. Difatti quello di creare e modificare uno stato solo internamente è un modo di risolvere
un problema in stile imperativo, ma lo stesso problema è risolubile (almeno in principio, senza tenere conto
dell’efficienza) con metodi puramente trasformazionali (linguaggi logici e funzionali). L’uso più importante dello stato,
che non può essere sostituito con metodi trasformazionali, è quello di tenere traccia degli input esterni in modo di avere
una computazione history dependent. Di questo parleremo ancora molto in seguito.
1.1.2.4. Mancanza di interazione
Un programma LP, come una TM, prende in ingresso un input e fornisce un output, e durante tutto il processo di
computazione taglia fuori il mondo esterno che quindi non può modificare la computazione. Per ogni input si avrà
sempre lo stesso output. Questo aspetto è strettamente legato al precedente poiché attraverso l’interazione il sistema
viene a conoscere una parte dello stato esterno che può modificare la computazione, in genere modificando lo stato
interno. Nessun linguaggio può fare a meno di istruzioni di I/O infatti queste sono presenti in ogni implementazione
Prolog, il problema è che non hanno una semantica logica. Di interazione parleremo ancora molto in seguito.
1.2. Caratteristiche della OOP
Non è certo nostra intenzione dare una trattazione esauriente della OOP perché cioè va oltre i nostri scopi e perché vi
sono molti libri che già lo fanno. Inoltre non vi è ancora un totale accordo su cosa siano gli oggetti, tanto che si parla di
paradigma ad oggetti e nono di modello. Questa tesi vuole anche contribuire a fornire un modello di basso livello degli
oggetti che verrà presentato nella prossima sezione. Si danno comunque per scontati i concetti base che acquisisce
chiunque abbia mai usato un linguaggio OOP. I concetti più importanti verranno comunque brevemente introdotti nel
capitolo “Oggetti in LP”.
1.3. Usi “naturali” della OOP e della LP
Per evidenziare le differenze nella rappresentazione della conoscenza tra linguaggi logici ed ad oggetti consideriamo la
seguente informazione: “Mario e Paolo sono due impiegati. Mario è nato a Roma ed ha 30 anni, Paolo è nato a Bologna
ed ha 35 anni, Mario e Paolo sono colleghi. Antonio è il capo dell’ufficio dove lavorano Mario e Paolo. Gli impiegati
lavorano 40 ore alla settimana e sono tutti iscritti al sindacato. Inoltre Mario possiede una automobile Alfa Romeo”.
Possiamo rappresentare tale conoscenza con un diagramma UML.
Una possibile rappresentazione in Prolog può essere la seguente:
persona(X) :- impiegato(X). (∀X impiegato(X) persona(X) )
iscrittosindacato (X) :- impiegato(X). (∀X impiegato(X) iscrittosindacato(X) )
orelavorative (X,40) :- impiegato(X). (∀X impiegato(X) orelavorative(X,40) )
collega(X, Y) :- collega(Y, X).
impiegato(X) :- capoufficio (X). (∀X capoufficio (X) impiegato(X) )
automobile(X) :- alfaromeo (X). (∀X alfaromeo(X) automobile(X) )
impiegato (mario).
luogo_nascita(mario, roma).
eta(mario, 30).
alfaromeo(bo55364).
possiede(mario, bo55364).
impiegato (paolo).
luogo_nascita(paolo, bologna).
eta(paolo, 35).
collega(mario, paolo).
capoufficio(antonio).
In una rappresentazione ad oggetti avremmo invece definito le classi automobile, alfaromeo, persona,
impiegato e capo come segue:
class automobile {
…
}
Parte Prima: Differenze tra OOP e LP
LP e OOP
10
class alfaromeo extends automobile {
…
}
class persona {
String nome;
String luogo_nascita;
Int eta;
public automobile auto;
public persona(String nome, String luogo_nascita, Int eta) { //
costruttore
this.nome=nome;
this. luogo_nascita= luogo_nascita;
this. eta= eta;
}
} // persona
class impiegato extend persona {
persona[] colleghi;
Boolean iscritto=true;
int orelavorative=40;
int stipendio=500;
public impiegato (String nome, String luogo_nascita, Int eta, persona[]
colleghi) { // costruttore
persona(String nome, String luogo_nascita, Int eta);
this.colleghi=colleghi;
}
} // impiegato
class capo extend impiegato {
int stipendio=1000;
} // capo
e quindi creato 2 istanze di tale classe:
impiegato mario, paolo;
capo antonio;
mario = new impiegato (mario, roma, 30, paolo);
paolo = new impiegato (paolo, bologna, 35 ,mario);
antonio = new capo();
mario.auto=new alfaromeo();
Parte Prima: Differenze tra OOP e LP
LP e OOP
11
L’informazione totale contenuta nei due programmi è la stessa però alcune cose risultano più naturalmente esprimibili
in un modo piuttosto che nell’altro. Nei linguaggi ad oggetti il fuoco è sulle singole entità che vengono descritte in
maniera compatta e unitaria attraverso le classi mentre in Prolog l’accento è sulle relazioni tra le entità. Quindi mentre
risulta naturale usare il predicato “collega”, risulta meno naturale usare un predicato “eta” come relazione tra gli enti
“mario” e “30”. Viceversa mentre è naturale inserire tutte le proprietà delle persone (nome, luogo di nascita, età) entro
una stessa classe, è meno chiaro l’uso di un array di persone per i colleghi perché questa è una proprietà esterna ad una
singola persona che riguarda più persone. Inoltre con tale meccanismo si ha ridondanza di informazione (dentro
l’oggetto “mario” vi è l’informazione che “paolo” è suo collega e viceversa) con problemi di aggiornamento e di
consistenza delle informazioni stesse. Avremmo potuto usare anche un’altra implementazione che racchiude la
relazione “collega” in un oggetto separato ma la questione di fondo rimane: in OOP le relazioni sono sì rappresentabili
ma in maniera meno naturale di quanto avviene in LP.
OOP → entità
LP → relazioni
Si noti che la rappresentazione migliore sarebbe una mista in cui tutte le proprietà di una singola entità vengono
racchiuse entro un oggetto OOP ed esprimere poi le relazioni tra tali oggetti in LP.
1.4. Riepilogo
Sono state presentate le peculiarità e le caratteristiche secondarie delle LP evidenziando le differenze con la OOP:
Peculiarità:
• dichiaratività dei programmi
• invertibilità: uno stesso programma può risolvere più problemi
• possibilità di fare inferenze
• motore di ricerca su alberi già implementato con possibilità di backtracking
• unificazione
Caratteristiche secondarie:
• mancanza di tipi
• variabili senza assegnamento
• mancanza di stato e quindi di effetti collaterali
• mancanza di interazione
In seguito discuteremo quali delle caratteristiche “negative” (mancanze) della LP sono dei limiti effettivi.
Abbiamo poi mostrato come una stessa informazione sia rappresentabile in entrambi i modelli. Nei linguaggi ad oggetti
il fuoco è sulle singole entità che vengono descritte in maniera compatta e unitaria attraverso le classi mentre in Prolog
l’accento è sulle relazioni tra le entità.
OOP → entità
LP → relazioni
persona
nome
luogo
eta
impiegato
colleghi
iscritto=true
stipendio=500
orelavorative=40
capo
stipendio=1000
antonio
mar io
nome=Mario
luogo=Roma
eta=30
colleghi={paolo}
paolo
nome=Paolo
luogo=Bologna
eta=35
colleghi={mario}
automobile
alfaromeo
bo55364
collega collega
Parte Prima: Differenze tra OOP e LP
LP e OOP
12
E’ chiaro che la rappresentazione migliore sarebbe una mista, e questo è il risultato a cui vogliamo arrivare. E’
importante che siano chiare le differenze tra i due strumenti e cosa uno offre in più dell’altro, in vista di una possibile
integrazione. Dobbiamo anche capire quali siano le caratteristiche più importanti della LP e quali quelle di contorno
perché è possibile che in una integrazione LP-OOP si debba arrivare ad un trade-off rinunciando ad alcune in favore di
altre. La caratteristica più importante che distingue la LP, ed a cui non vogliamo assolutamente rinunciare, è la
possibilità di fare inferenze. Mentre è sicuramente secondaria l’invertibilità dei programmi, difatti questa non viene mai
usata nei programmi “reali”.
Parte Prima: Limiti della LP
LP e OOP
13
Capitolo 2
Limiti della LP
Nel capitolo precedente abbiamo visto le differenze tra LP ed OOP. Sappiamo già che tutto quello che si
può fare in LP lo si può (anche se con maggior sforzo) anche in OOP, difatti gli interpreti Prolog sono di
solito implementati in linguaggi OOP come il C++. In questa sezione vedremo che non vale il viceversa,
la LP è un modello di programmazione inferiore alla OOP, non permette cioè di esprimere tutto quello
che è possibile in OOP. Questo aspetto è già stato messo in luce da Wegner [We93], riprenderemo i
concetti esposti in tale articolo e li rielaboreremo in funzione degli sviluppi futuri. E’ importante mettere
chiaramente in luce i limiti e soprattutto le cause di tali limiti per cercare di porvi rimedio in seguito.
Previeni i problemi prima che
sorgano.
Coltiva l’ordine prima che nasca
il disordine.
Lao Tzu, Tao te Ching
2.1. Sintassi e semantica in LP
L’aspetto fondamentale della LP da cui derivano gli altri è il seguente:
la semantica è ridotta alla sintassi.
Tale restrizione è d’obbligo se si vogliono risolutori corretti e soprattutto completi. Questo perché una macchina non
può conoscere il significato dei simboli dato dal programmatore, ma se la semantica è data dalla sintassi può limitarsi a
manipolare simboli dimostrando ciò che è vero indipendentemente dal significato dei simboli stessi. Per il risolutore un
simbolo non denota altro che se stesso, è un puro segno che non rappresenta nulla. Questo è ben diverso, ad esempio,
dall’italiano scritto in cui dietro la stringa “persona” c’è un significato (dato non da regole ma da convenzioni e usi). Di
solito nei programmi LP non si usano stringhe qualsiasi ma nomi significativi, ma significativi solo per chi legge, per la
macchina è la stessa cosa sostituirvi simboli qualunque purché dove compaia un certo termine ne compaia un altro e
sempre lo stesso.
Si può meglio comprendere tale differenza confrontando la definizione di interpretazione con quella di interpretazione
di Herbrand di un programma logico, essendo le interpretazioni ad attribuire la semantica ai programmi.
Definizione 2.1.1 (interpretazione). Dato un linguaggio del primo ordine L=(C, F, P, V)
3
, un’interpretazione per L
definisce un dominio non vuoto D e assegna:
• a ciascun simbolo di costante in C una costante in D;
• a ciascun simbolo di funzione n-ario una funzione F:D
n
→D;
• a ciascun simbolo di predicato n-ario in P una relazione in D
n
, cioè un sottoinsieme in D
n
.
Ci sono quindi due insiemi distinti: quello dei simboli S e quello dei significati (valori) D. In particolare più simboli
potrebbero indicare lo stesso valore.
3
C è l’insieme delle costanti, F quello delle funzioni, P dei predicati e V delle variabili.
S D
sintassi semantica
Parte Prima: Limiti della LP
LP e OOP
14
Si noti che gli elementi del dominio D possono essere qualunque cosa, anche enti reali. In questo caso abbiamo usato
stringhe ma il “9” in S è diverso dal “9” in D: il primo è un disegno il secondo il concetto del numero nove. Lo stesso
vale per “caino”. La confusione nasce dal fatto che per rappresentare i concetti su carta possiamo usare solo dei segni e
non possiamo denotare altrimenti oggetti reali o astratti. In realtà nel dominio D è definita implicitamente un’altra
funzione di interpretazione che collega queste altre stringhe al mondo reale ma tale funzione è nella mente di chi legge.
La definizione di interpretazione data è del tutto generale e nella LP ne viene data una più specifica:
Definizione 2.1.2. (interpretazione di Herbrand). Dato un insieme di formule ben formate P, un’interpretazione di
Herbrand I di P è un’interpretazione tale che:
• il suo dominio di interpretazione è H(P);
• una funzione di interpretazione che associa ciascun simbolo di costante a se stesso;
• se f è un simbolo di funzione n-ario in P, la funzione di interpretazione F: H(P)
n
→H(P) mappa la sequenza di
termini ground (t
1
, … t
n
)∈H(P)
n
nel termine f(t
1
, … t
n
)∈H(P);
• se p è un simbolo di predicato n-ario in P, la funzione di interpretazione gli associa un sottoinsieme di atomi
p(t
1
, … t
n
)∈B(P).
Esiste in pratica un solo dominio essendo quello della semantica coincidente con quello della sintassi, ogni simbolo
indica sempre e solo se stesso, non c’è quindi modo per far corrispondere due stringhe diverse come “caino” e
“fratello(abele)” allo stesso concetto (significato). Non vi è quindi neanche la possibilità di cambiare interpretazione.
In pratica il dominio della semantica non esiste, è solo chi legge che dà dei significati ai simboli. In particolare non vi è
possibilità di cambiare interpretazione; poiché la semantica è data dalla sintassi per cambiare semantica dobbiamo
cambiare programma. Questo permette di computare tramite delle meccaniche trasformazioni di simboli. Ad esempio
consideriamo la seguente teoria:
f(a) :- g(b), f(c), h(a),
f(c).
g(b) :- h(b).
h(a).
h(b).
9
6+3
10-1
caino
f ratello(abele)
squadreDiBasketBolognesi/1
9
caino
virtus
fortitudo
S D
H(P) H(P)
sintassi semantica
H(P) D
sintassi semantica
programmatore
9
6+3
10-1
caino
fratello(abele)
squadreDiBasketBolognesi/1
S
9
6+3
10-1
caino
fratello(abele)
squadreDiBasketBolognesi/1
D=S
Parte Prima: Limiti della LP
LP e OOP
15
Possiamo interpretare ogni riga come una regola di riscrittura: quello che è a sinistra del simbolo “:-“ può essere
sostituito con quello che si trova a sinistra o con la stringa vuota se non vi è nulla. L’input (goal) sia:
f(a).
Secondo la prima regola della teoria questo viene sostituito con
g(b), f(c), h(a).
usando poi la terza regola possiamo sostituire il primo termine:
h(b), f(c), h(a).
le altre sostituzioni sono le seguenti:
f(c), h(a).
h(a).
.
La terminazione con stringa vuota viene considerata come successo altrimenti come fallimento. Si noti che poiché un
programma ha sempre una sola interpretazione invariabile risponderà sempre allo stesso modo agli stessi goal. Questo è
diverso da quanto avviene nella OOP in cui la chiamata ad un metodo può dare risposte diverse in momenti diversi.
Abbiamo detto che la riduzione della semantica alla sintassi è il punto cruciale della LP. Da questo non derivano
solamente i limiti di cui parleremo in seguito, ma anche le peculiarità della LP. Difatti senza tale caratteristica non
sarebbero nemmeno possibili l’unificazione (detta appunto sintattica), l’invertibilità, la dichiaratività che sono i punti di
forza della LP che la distinguono dagli altri modelli. All’assenza del dominio semantico è da attribuire la mancanza dei
tipi e delle variabili con assegnamento, ma questi non sono dei limiti. Vedremo invece che la mancanza di stato e di
interazione sono dei veri limiti. E’ chiaro anche che
nel momento in cui tentiamo di superare queste mancanze della LP introducendo lo stato e
l’interazione perdiamo anche alcune peculiarità.
Dovremo fare quindi un bilancio delle nuove funzionalità che si acquisiscono e di quelle che si perdono e valutare se
l’estensione può risultare vantaggiosa.
2.2. I veri limiti della LP
In questa sezione cercheremo di far capire perché l’assenza di stato e di interazione sono effettivamente dei limiti per la
LP. La trattazione sarà piuttosto informale ed intuitiva facendo appello ad idee che chiunque abbia mai avuto a che fare
con la programmazione logica ed ad oggetti ha avuto modo di conoscere. Nella seconda parte verranno formalizzati tali
concetti e si capirà meglio perché l’interazione e lo stato aggiungono una dimensione alla elaborazione.
2.2.1 Mancanza di stato
Sul fatto che la mancanza di stato sia veramente una mancanza certamente non saranno tutti d’accordo (“per computare
non abbiamo bisogno di stato”). Cercheremo ora di vedere in maniera intuitiva perché questa sia realmente una pecca
della LP mentre nella seconda parte approfondiremo tale questione con più precisione e daremo un significato più
preciso alla frase sopra citata.
Consideriamo il seguente esempio:
contatore(0).
incrementa :- contatore(X), retract(contatore(X)), assert(contatore(s(X))).
valore(X) :- contatore(X).
A tale programma poniamo i seguenti goal:
:- value(Y).
yes, Y/0
:- incrementa.
yes
:- value(Y).
yes, Y/s(0)
Lo stesso predicato (value(Y)) in interrogazioni diverse ha dato risposte diverse, cosa che è “illogica”. Questo perché
allo stesso programma corrispondono più teorie logiche diverse non equivalenti. Nell’esempio il goal value(Y) è stato
sottoposto la prima volta alla teoria:
contatore(0).
incrementa :- contatore(X), retract(contatore(X)), assert(contatore(s(X))).
valore(X) :- contatore(X).
e la seconda volta alla teoria:
contatore(s(0)).
incrementa :- contatore(X), retract(contatore(X)), assert(contatore(s(X))).
valore(X) :- contatore(X).
Consideriamo un altro esempio:
lunghezza_lista(LISTA, N) :- assert(contatore(0)), lunghezza_lista(LISTA), contatore(N),
retract(contatore(_)).
Parte Prima: Limiti della LP
LP e OOP
16
lunghezza_lista([]).
lunghezza_lista([HEAD|TAIL]) :- retract(contatore(X)), assert(contatore(s(X))),
lunghezza_lista(TAIL).
In questo caso l’uso dello stato è “interno” e lo stesso goal (ad esempio lunghezza_lista([a, b, c], N)) sottoposto
alla teoria in momenti diversi darà sempre lo stesso risultato. A differenza del primo esempio, irrisolubile in LP
classica, in questo caso la soluzione logica è molto più semplice e elegante:
lunghezza_lista([], 0)
lunghezza_lista([HEAD|TAIL], N) :- lunghezza_lista(TAIL, M), N=s(M).
Oppure nella versione tail recursive:
lunghezza_lista(LISTA, N) :- lunghezza_lista(LISTA, 0, N).
lunghezza_lista([], N, N).
lunghezza_lista([HEAD|TAIL]), ACC, N) :- ACC1=s(ACC), lunghezza_lista(TAIL, ACC1, N).
Nella versione tail recursive, nell’ultima clausola, abbiamo usato delle variabili accumulatore per tenere lo stato della
computazione, ma poiché non abbiamo voluto usare effetti collaterali abbiamo avuto bisogno di accumulatori (ACC e
ACC1): uno per il vecchio stato e uno per il nuovo. In entrambi gli esempi abbiamo parlato di stato ma con diverso
significato. Nel primo caso lo stato era esterno nel senso che serviva per collegare diverse computazioni
(interrogazioni), ogni richiesta di incrementa è asincrona ed ha l’effetto di influenzare le computazione future
(history dependent). Non esistono soluzioni a tale problema nella logica. Nel secondo esempio (in tutte e due le
versioni) lo stato era interno nel senso che esso non è permanente tra computazioni successive, quindi goal passati non
influenzano i futuri (history independent) e ogni goal darà sempre lo stesso risultato. Per ogni goal si sa già di quanto
deve essere incrementato il contatore. Su questi concetti torneremo in seguito. La prima soluzione del secondo esempio
è assolutamente da evitare poiché se si vuole risolvere un problema in stile imperativo tanto vale usare un linguaggio di
tale genere.
Come si sarà intuito si sono due modi diversi di usare lo stato: il primo serve per collegare le singole computazioni
mentre il secondo per effettuare una singola computazione. In entrambi i casi l’introduzione di effetti collaterali ci fa
uscire dal modello logico. Mentre il secondo uso si può evitare come abbiamo fatto nell’esempio ed anzi un suo uso in
LP è inutile (se non dannoso), nel primo caso non possiamo farne a meno. Notare che i due usi sono su due piani
diversi, tanto che sarebbe meglio usare termini diversi per evitare la confusione che nasce appunto dal parlare di cose
diverse con lo stesso termine.
usi dello stato:
esterno → collega le singole computazioni → non presente in LP
interno → collega i passi della computazione → non serve in LP
Accenniamo anche al fatto che lo stato può essere usato anche come meccanismo di comunicazione asincrona tra più
sistemi diversi. Difatti la risposta che riceve un utente è funzione dello stato che a sua volta è funzione degli ingressi
passati che possono essere stati prodotti da utenti diversi. Con lo stato non c’è bisogno che due entità siano in collegate
direttamente nello stesso momento per comunicare. Il mittente può lasciare il messaggio ed il ricevente prenderlo in un
secondo momento. Il meccanismo è simile a quello ben conosciuto di comunicazione tramite tuple.
2.2.2. Mancanza di interazione
Sul fatto che l’interazione sia un limite nessuno ha dubbi, difatti nessun programma che non sia solo un esercizio può
fare a meno di istruzioni di I/O. L’introduzione di questi predicati fa però perdere la logica del sistema, ad esempio i
programmi non sono più invertibili. Non a tutti è però chiaro se l’introduzione dei predicati di I/O è solo una necessità
pratica oppure una caratteristica indispensabile non rappresentabile in logica. A questa domanda ha risposto Wegner
[We97a], il quale ha mostrato che l’interazione è una diversa dimensione che si aggiunge alla elaborazione e che quindi
non è riconducibile a questa. Un programma LP, come una TM, è una macchina che tra un input ed un output taglia
fuori il mondo esterno ed è quindi completamente indipendente da esso. Un uso interattivo è invece più espressivo
perché riceve più informazioni dall’ambiente esterno e soprattutto informazioni che in genere non sono disponibili tutte
assieme nel momento in cui si riceve il primo input perché alcune saranno funzioni di output che fornirà
successivamente la macchina.
Un esempio classico che descrive bene questa idea è quello della automobile automatica che deve andare da un punto A
ad un punto B. Se la pianta della città è conosciuta e non ci sono altre macchine il problema è risolvibile con una TM
perché si conoscono tutte le informazioni a riguardo e queste sono immutabili. Se però non abbiamo la carta stradale
oppure ci sono anche altre macchine che circolano l’approccio precedente non è più praticabile perché ci mancano
informazioni e nel caso di presenza di altre macchine non solo mancano informazioni ma queste sono anche variabili. Il
problema è però risolvibile con un approccio interattivo: non conosciamo lo stato globale ma solo una parte, quindi
prendiamo decisioni compatibili con l’informazione correntemente disponibile, l’output corrispondente in genere
influenzerà anche l’input successivo che ci fornirà altre informazioni non disponibili in precedenza, quindi si itera il
procedimento. Il processo interattivo può anche essere illimitato e non portarci mai alla soluzione voluta ma, a
differenza del comportamento non interattivo, è comunque percorribile. Si noti che in alcuni problemi non solo
l’informazione è incompleta come nel caso della mancanza di una carta stradale, ma è anche non formalizzabile e quindi
Parte Prima: Limiti della LP
LP e OOP
17
non può essere racchiusa in una macchina come nel caso della presenza di altre automobili. Si faccia però attenzione
che la non formalizzabilità è esterna alla macchina.
In molti casi l’interazione è legata allo stato
4
, ed in particolare alla sua modifica. Di solito infatti, tramite l’interazione,
lo stato interno viene modificato in modo da rappresentare quello esterno, o meglio, quella parte di che viene trasmessa
tramite l’input. Si hanno così computazioni history dependent in cui il risultato di un input dipende anche dagli input
precedenti la cui informazione è racchiusa nello stato. Se alcuni degli input precedenti derivano da altre entità ecco che
l’insieme stato-interazione permette la comunicazione asincrona di cui abbiamo accennato nel punto precedente e di cui
parleremo ancora in seguito.
2.3. Il Prolog è un linguaggio logico?
A conferma che l’interazione e lo stato sono effettivamente dei limiti vi il fatto che tutte le implementazioni di linguaggi
LP introducono costrutti di I/O (ad esempio read e write) e di modifica permanente (ad esempio assert e retract). Come
sappiamo questi sono dei meccanismi non logici e quindi i programmi che ne fanno uso (la totalità se si escludono e
semplici programmi di esempio di presentazione della LP) perdono alcune delle peculiarità proprie della LP che ne
fanno un linguaggio così particolare. Difatti tutti sappiamo bene che un programma Prolog che “serve a qualcosa” non è
invertibile. E’ allora spontaneo chiedersi se sia ancora corretto parlare di programmazione logica. Il dubbio nasce dal
fatto che spesso si crede che i programmi perdono l’invertibilità non a causa di limiti intrinseci della LP, ma a causa del
suo uso pratico. Per motivi di efficienza si preferisce, ad esempio, usare i numeri ed il predicato “is” anche se
teoricamente potremmo farne a meno. L’uso di un semplice “is” è da sola causa di perdita di invertibilità. Il problema è
che spesso non si ha chiaro il confine tra la perdita di “logicità” dovuta a limiti intrinseci della LP e quella dovuta a
limiti pragmatici.
Per mettere chiarezza in tale campo proponiamo la seguente classificazione dei linguaggi “logici”.
• linguaggi logici, quei linguaggi con non fanno uso di nessun predicato extra logico, non usano termini “speciali”
come i numeri e sono corretti e completi. Non esistono interpreti commerciali per tali linguaggi che sono (erano) di
interesse solo a livello accademico. Con LP ci riferiremo a tali linguaggi.
• linguaggi basati sulla logica, per avere uno strumento che sia usabile, sia dal punto di vista pragmatico sia da
quello dell’efficienza, si è dovuto rinunciare alle caratteristiche della precedente categoria. Sono non completi a
causa della ricerca in profondità secondo l’ordine testuale; esiste il termine numerico e i relativi predicati (“l’is” in
particolare); ci sono predicati metalogici e alcuni extra-logici ma non esiste il concetto di stato e di interazione con
l’ambiente esterno.
• linguaggi orientati alla logica, ai precedenti aggiungono il concetto di stato e/o di interazione introducendo
predicati come read/1 e write/1, andando quindi oltre la TM. Il Prolog e la maggior parte di tutti i suoi dialetti
appartengono a questa classe.
• linguaggi ispirati alla logica, quelli che non appartengono alle classi precedenti poiché aggiungono ulteriori nuovi
costrutti. Comprende una vasta classe di linguaggi, molti dei quali sono stati solo proposti ma non realizzati, ma in
pratica anche tutti i linguaggi commerciali che nelle prime versioni si potevano considerare della terza classe ma
poi, nelle versioni successive, hanno aggiunto nuovi costrutti non logici per aumentare l’espressività del
linguaggio. In particolare linguaggi che usano altri tipi di logica o che permettono l’uso di oggetti C++ o Java.
La totalità dei “linguaggi logici” appartiene alle ultime due categorie, ma lo stato e l’interazione non hanno una chiara
semantica logica, si possono allora chiamare logici? Uno degli obbiettivi di questo lavoro è quello di introdurre le
caratteristiche citate nella LP rimanendo però in ambito logico. Discuteremo di questo nella seconda parte.
2.4. Riepilogo
Si è mostrato che la causa di tutti i limiti (e delle peculiarità) della LP è il fatto che la semantica è ridotta alla sintassi.
Questo non è imposto dalla logica, in cui l’interpretazione prevede il dominio semantico D, ma è dovuto alla
implementazione della LP che usa un sottocaso di interpretazione (quella di Herbrand). Abbiamo poi mostrato che i veri
limiti della LP sono due: stato e interazione. Non tutti sono disposto a considerare la mancanza di stato come un limite
della LP, ma questo è dovuto al fatto che il termine “stato” ha (almeno) due significati distinti.
usi dello stato:
esterno → collega le singole computazioni → non presente in LP
interno → collega i passi della computazione → non serve in LP
L’interazione invece permette di usare, durante l’elaborazione, informazioni esterne non disponibili all’inizio. So pensi
al classico esempio dell’automobile che deve andare da A a B.
Infine abbiamo proposto la seguente classificazione dei linguaggi “logici”
• linguaggi logici,
• linguaggi basati sulla logica,
• linguaggi orientati alla logica,
4
Vedremo meglio questo aspetto in seguito; per il momento facciamo notare che, anche se in certi casi vi è uno stretto legame tra stato e interazione,
non è corretto identificare le macchine con stato con le macchine interattive.