Dispense del corso - Dipartimento di Ingegneria dell`Informazione

Transcript

Dispense del corso - Dipartimento di Ingegneria dell`Informazione
PARTE I
Gianni Aguzzi
Il Paradigma della Programmazione Dichiarativa
Programmazione Logica e
Linguaggio Prolog
Dispense del corso di
“Paradigmi di Programmazione”
Facoltà di Scienze Matematiche Fisiche e Naturali
Università degli Studi di Firenze
INDICE
La Programmazione Logica: un rapido sguardo .........................1
Paradigma di Programmazione Dichiarativa ..............................1
Sintassi della Logica dei Predicati ...............................................................................2
Sostituzioni ...................................................................................................................3
Unificazione..................................................................................................................5
Calcolo di sostituzioni mgu: Il Metodo delle Trasformazioni di Insiemi di Equazioni. 5
Semantica della Logica dei Predicati ..........................................................................8
Algebre e Stati ...........................................................................................................8
Modelli e Conseguenza Logica ................................................................................10
Ulteriori Importanti Concetti Semantici....................................................................12
Trasformazione di Formule in Forma Normale.........................................................13
Forme Normali Prenesse ..........................................................................................13
Forme Normali Congiuntive e Disgiuntive...............................................................14
Forme Normali di Skolem........................................................................................15
Teoria della Dimostrazione........................................................................................17
Il Concetto di Calcolo ..............................................................................................17
Un Calcolo di Hilbert...............................................................................................18
Il Calcolo della Risoluzione per Clausole Generali...................................................20
Dimostrazione per Refutazione con la Risoluzione...................................................21
Correttezza della Risoluzione come Calcolo per la Deduzione .................................22
Completezza della Risoluzione come Calcolo per la Refutazione .............................23
Strategie di Esecuzione della Dimostrazione per Refutazione via RES ....................26
La Programmazione Logica ........................................................28
Le Clausole di Horn ...................................................................................................28
La Risoluzione SLD ...................................................................................................28
Regola di Calcolo.....................................................................................................31
Alberi SLD .................................................................................................................32
Strategie di Ricerca nell’Albero SLD ........................................................................33
Semantica della Programmazione Logica ..................................36
La Semantica Operazionale.......................................................................................36
La Semantica a Modelli .............................................................................................36
Ancora Interpretazioni e Modelli alla Herbrand........................................................36
Alcune Proprietà dei Modelli di Herbrand ................................................................39
Correttezza e Completezza della Risoluzione SLD...................................................40
Il Trattamento della Negazione...................................................42
Regola dell’Ipotesi del Mondo Chiuso ......................................................................43
Regola della Negazione per Fallimento .....................................................................44
Caratterizzazione Operazionale della NF..................................................................44
La Risoluzione SLDNF ..............................................................................................47
Breve Excursus sul Linguaggio Prolog.......................................49
Strategia di Ricerca del Prolog: in Profondità con Backtracking Cronologico...........49
Unificazione in Prolog .............................................................................................50
Controllo dell’Esecuzione – Modello di Esecuzione per il Prolog ............................50
Il modello di Esecuzione del Prolog...........................................................................51
Il predicato Cut ........................................................................................................54
II
Realizzazione del Determinismo ..............................................................................56
Strutture Condizionali ..............................................................................................57
Predicato fail e Iterazione (di relazioni con effetti collaterali) ...................................57
Di nuovo la Negazione in Prolog (il predicato not)...................................................57
Predicati setof, bagof e findall..................................................................................58
Aritmetica ..................................................................................................................60
Predicato “is” per la Valutazione di Espressioni .......................................................61
Definizione di Relazioni con Uso della Ricorsione ....................................................63
Ricorsione Tail ........................................................................................................63
Da Ricorsione Non Tail a Ricorsione Tail ................................................................66
Le Liste.......................................................................................................................69
Le Liste Differenza.....................................................................................................72
I Termini: loro Ispezione e Manipolazione ...............................................................74
Operatori e Definizione di Alcune loro Proprietà .....................................................74
Ispezione/Manipolazione di Termini ........................................................................76
Alcune Relazioni per la Manipolazione di Termini...................................................78
Ispezione/Manipolazione di un Programma .............................................................80
Il Predicato Clause di Ispezione del Programma .......................................................80
Modifica del Programma(Database) .........................................................................81
Un metainterprete del Prolog in Prolog ....................................................................81
Le Grammatiche a Clausole Definite (Definite Clause Grammars) ........................81
Un traduttore da Produzioni a DCG..........................................................................84
I Predicati di I/O ........................................................................................................85
Testi di Riferimento .....................................................................86
III
La Programmazione Logica: un rapido sguardo
L’idea principale della Programmazione Logica, è quella di poter riguardare una teoria
assiomatica del I ordine, espressa tramite formule nella forma di clausole di Horn, come un
programma in cui ciascuna clausola rappresenta la definizione di una procedura il cui
nome è il simbolo di predicato presente nella testa della clausola, i cui parametri sono
quelli presenti nella testa ed il cui corpo è il corpo (ossia la parte restante) della clausola
stessa.
Un assioma nella forma di una clausola del tipo h ←b1, b2,…,bn può essere intuitivamente
letto come “h, la testa, vale se valgono b1 e b2 e.. e bn, ossia tutti i letterali che
compongono il corpo”.
L’esecuzione di un tale programma, P, per certi inputs nella forma di termini, è
rappresentata ora dal concetto di provare che una certa formula del tipo ∃x1…∃xm ϕ, detta
goal dove ϕ è della forma c1, c2,…,ck ossia come il corpo di una clausola, segue
logicamente da P; da un punto di vista procedurale ciò può essere visto come l’esecuzione
della chiamata delle procedure ci con i dati inputs ed eventuali parametri d’uscita,
quest’ultimi sempre nella forma di termini eventualmente con variabili in {x1,…,xm}.
Esempio
Sia dato il seguente programma P:
append([],Ys,Ys) ←
append([X|Xs], Ys, [X|Zs]) ← append(Xs, Ys, Zs)
dove la prima clausola è nella forma h ← , detta fatto, in cui si afferma che la relazione
append vale fra i suoi tre parametri per ogni Ys nel senso che: la concatenazione della lista
vuota ([]) con una lista qualsiasi Ys vale Ys, oppure che la differenza fra una lista e sé
stessa vale la lista vuota o che la differenza fra una lista Ys e la lista vuota vale Ys o che lo
“split” (la suddivisione) di una lista Ys puó esssere la coppia ([], Ys) e la seconda clausola
afferma che la concatenazione fra una lista non vuota (il cui primo elemento è l’oggettto
qualsiasi X e la cui parte restante, coda, è la lista, eventualmente vuota Xs) ed una lista
qualsiasi Ys è la lista, il terzo parametro della testa, il cui primo elemento è X e la cui coda
è la lista Zs a patto che questa sia, ricorsivamente, la concatenazione fra Xs e Ys.
Possibili goals sono i seguenti:
1) ∃X append([1],[2],X) dove [1] e [2] sono parametri di input e X è di output
2) ∃X ∃Y ∃Z append(X,[1],Y), append([1], Z, X) dove [1] è un parametro di input e
X, Y e Z sono di output
3) ∃Y ∃Z append([1|Y], [2], Z), dove [2] é di input, [1|Y] di input/output e Z di
output
La prova che il goal 1) è conseguenza logica di P, ossia l’esecuzione della chiamata di
procedura indicata, calcola l’istanza di X = [1,2] e la prova di 3) calcola le seguenti
(infinite) coppie di istanze per Y e Z : Y= [] e Z = [1, 2], Y= [X1] e Z= [1, X1, 2], Y= [X1,
X2] e Z=[1, X1, X2, 2],…
Paradigma di Programmazione Dichiarativa
Poiché gli elaboratori (elettronici) non comprendono altro che proposizioni formalizzate, è
ovvia l’importanza della logica nell’ambito dell’informatica. Inoltre la possibilitá di
riguardare la logica come un effettivo paradigma di programmazione, ha sempre piú stretto
tali legami fra logica e informatica: fra tali “linguaggi logici” il piú importatane è la logica
dei predicati, altri sono costituiti da sottolinguaggi della logica dei predicati: la logica a
clausole di Horn, la logica equazionale, i sistemi di riscrittura di termini. Riporteremo
quindi i punti essenziali di un linguaggio della logica dei predicati. I soliti due aspetti sono
quello sintattico e quello semantico.
1
Sintassi della Logica dei Predicati
Per formalizzare adeguatamente frasi come “ogni numero diverso da zero è il successore di
un altro numero”, occorre fedelmente rispecchiare la loro intima struttura servendosi,
innanzitutto, di un opportuno vocabolario, detto segnatura, nel quale possiamo trovare dei
simboli tramite i quali “nominare” relazioni fra oggetti del voluto dominio del discorso,
operatori su tali oggetti ed infine gli oggetti, o parte di essi, del dominio stesso. Se il
dominio è finito, potremo avere un simbolo diverso per ogni oggetto nominato
(rappresentato), altrimenti, se il dominio è infinito numerabile, potremo rappresentare con
un numero finito di costanti altrettanti oggetti e, tramite essi ed opportuni operatori,
rappresentare tutti gli altri oggetti. Ci convinciamo immediatamente che ogni numero
naturale può essere adeguatamente nominato dall’applicazione di un numero opportuno
dell’operatore unario s alla costante 0 che rappresenta l’oggetto zero: così s(s(s(0))) può
essere un adeguato nome per l’oggetto costituito da tre unità e s(X), con X variabile, può
analogamente rappresentare la classe degli infiniti numeri maggiori di zero. Tali “nomi”
sono detti termini.
Le nostre frasi stabiliscono relazioni ovvero proprietà degli oggetti e quindi, nella
rappresentazione, proprietà dei termini. Allora se d è un simbolo di relazione binaria,
d(X,0), detta formula atomica, stabilisce che la relazione d vale fra i due oggetti (termini)
riferiti dalla variabile X e dalla costante 0.
Per formalizzare l’intera frase sopra riportata, ci accorgiamo di dover disporre di adeguate
funzioni logiche (dette connettivi logici), che applicate a valori logici, vero e falso, e
valutate restituiscano ancora un valore logico. E ancora, per esprimere formalmente le
espressioni “per ogni” ed “esiste” potremo disporre di simboli come ∀ e ∃. A questo punto,
la nostra frase iniziale può essere opportunamente (ed equivalentemente) riformulata come
“per ogni numero X, con X diverso da zero esiste un numero, Y, di cui X è il successore
(ovvero t.c. X è uguale al successore di Y)”. Ci manca ora di capire come effettivamente
sono legati i tre pezzi “per ogni numero X”, “X diverso da zero” e “esiste un numero, Y,
di cui X è il successore (ovvero t.c. X è uguale al successore di Y)”: la prima
quantificazione si estende a tutto il resto, gli altri due pezzi possono essere legati, nel
discorso in italiano, nel modo seguente “se X è diverso da zero allora esiste un numero, Y,
di cui X è il successore (ovvero t.c. X è uguale al successore di Y)” nel senso che tutte le
volte che X è diverso da zero, perché l’intero asserto sia vero, occorre che esista quel tale
Y e tutte le volte che X è zero allora l’asserto è comunque vero. Tutto questo ci porta ad
usare un opportuno connettivo logico che lega gli ultimi due pezzi: il connettivo →, detto
implicazione logica. Possiamo allora descrivere formalmente la frase vista con la formula
∀X( d(X,0) → ∃Y u(X, s(Y)) ), dove d ed u, simboli di predicato o relazione binari
dovranno avere il significato di “diverso” e “uguale”, rispettivamente.
Possiamo riportare formalmente le definizioni degli oggetti usati per arrivare a scrivere la
formula sopra riportata.
Definizione I seguenti simboli vengono usati per costruire formule di un linguaggio nelle
logica dei predicati.
• Connettivi Logici (la cui definizione è data per nota)
~ negazione
∧ congiunzione
∨ disgiunzione
→ implicazione
•
•
•
equivalenza
Quantificatori : ∀ e ∃
Parentesi: ( e ), Virgola: ,
2
•
Segnatura Σ = F∪P, con F = F0∪…∪Fn, P = P0∪…∪Pm , n,m ≥ 0 dove gli
insiemi Fi e Pj, così come tutti gli Fi fra di loro ed i Pj fra loro, sono disgiunti e Fi è
l’insieme degli operatori o simboli di funzione di arietá i e P j è quello dei simboli di
predicato di arietá j
• V insieme numerabile di simboli di variabile.
Definizione Data una segnatura Σ e un insieme di variabili V, definiamo induttivamente i
Σ-termini come segue:
a) ogni variabile in V è un Σ-termine,
b) se f è un operatore in Fn e t 1,…,tn sonoΣ-termini, allora pure f(t 1,…,tn) è un Σtermine, in particolare se f ∈ F0, f è una costante che è pure un Σ-termine.
Se la segnatura è sottintesa invece di dire Σ-termine diremo semplicemente termine.
Indichiamo con T(Σ) l’insieme dei termini non contenenti variabili, detti termini ground.
Con T(Σ∪V) viene indicato l’insieme di tutti i termini e con var(t) l’insieme delle variabili
che figurano in t.
Analogamente, possiamo dare le seguenti ulteriori definizioni relative alle formule.
Definizione Data una segnatura Σ e un insieme di variabili V, definiamoΣ-formula atomica
la stringa p(t1,…,tn) con p ∈ Pn e t1,…,tn Σ-termini, in particolare se p ∈ P0 è detto atomo
ed è pure una formula atomica.
Definizione Data una segnatura Σ e un insieme di variabili V,definiamo induttivamente le
Σ-formule come segue:
a) ogni Σ-formula atomica è una Σ-formula
b) se ϕ e ψ sono Σ-formule e X è una variabile allora le seguenti stringhe sono ancora
Σ-formule: ~ϕ, ϕ c ψ, con c connettivo logico binario, ∀Xϕ e ∃Xϕ.
Definizione Per una formula … QXϕ … con sottoformula QXϕ , con ϕ minima formula,
e Q in {∀, ∃} definiamo ϕ essere il campo d’azione della quantificazione QX.
Considerando le occorrenze della variabile X in una formula ϕ che non fanno parte della
sottostringa QX, queste sono dette vincolate se si trovano nel campo d’azione di una
quantificazione QX; altrimenti sono dette occorrenze libere di X in ϕ.
Esempio Nella formula ∀Xq(X) ∧ ∃Zp(X, Z), l’unica occorrenza libera di una variabile è
la seconda occorrenza di X.
Definizione Una formula è detta chiusa sse non contiene alcuna occorrenza libera di
variabili. Per una formula ϕ, le cui variabili libere siano X1,…,Xn, la sua chiusura
universale, ∀(ϕ), ed esistenziale, ∃(ϕ), sono definite come le formule ∀X1,…, ∀Xnϕ e
∃X1,…, ∃Xnϕ rispettivamente. Una formula universale è una formula ∀(ϕ) con ϕ non
contenente alcun quantificatore. Un letterale è o una formula atomica, letterale positivo, o
una formula atomica negata, letterale negativo; ϕ e ~ϕ sono complementari con ϕ letterale.
Sostituzioni
Definizione Una sostituzione σ (su una segnatura Σ) è un insieme finito, {X1/t1,…,Xn/tn},
di coppie X/t tali che:
• X1,…,Xn sono tutte variabili distinte,
• t1,…,tn sono termini,
• Xi è diverso da t i per i = 1,…,n.
Per una tale σ, diciamo che essa agisce sul dominio dom(σ)= {X1,…,Xn}, se poi tutti i ti
sono ground σ è detta ground. Se σ = {} allora viene detta sostituzione vuota, spesso
indicata con ε. Con la notazione σ | D, con D insieme di variabili, si intende denotare la
sostituzione σ ristretta alle variabili che stanno in dom(σ)∩D.
3
Definizione Applicazione di una sostituzione
Data un sostituzione σ e un termine s, definiamo sσ o σ(s), detto il risultato della
applicazione di σ a s, il termine ottenuto da s, rimpiazzando simultaneamente tutte le
occorrenze di Xi in s con ti per i = 1,…,n (simultaneamente si riferisce a tutti gli i e anche a
tutte le occorrenze di X i in s). Così
• Xiσ = t i per i = 1,…,n
• Yσ = Y per ogni Y∈ V – dom(σ)
• f(s1,….,sn) σ = f(s1σ,…,snσ)
Allora, data σ = {X/Z, Y/0} e s = X + f(Y,Z) si ha sσ = Z + f(0, Z). Si noti che tε = t per
ogni termine t.
Per quanto riguarda l’applicazione di una sostituzione ad una formula, si intende il
rimpiazzamento sopra detto per le sole variabili libere della formula.
Definizione Ammissibilità
Per una formula φ ed una sostituzione σ, diciamo che φσ è ammissibile sse nessuna delle
variabili dei ti diventa vincolata dopo la sostituzione dei ti per le occorrenze libere delle Xi,
per i = 1,…,n.
Una sostituzione è un insieme finito di coppie, ma può essere riguardata anche come una
funzione da T(Σ∪V) in T(Σ∪V) e ancora come una funzione da V in T(Σ∪V). Il seguente
lemma, stabilisce l’equivalenza dei tre punti di vista.
Lemma
Per due sostituzioni σ e ρ sulla medesima segnatura Σ, i seguenti fatti sono equivalenti:
1. σ = ρ come insiemi finiti di coppie
2. tσ = tρ per ogni Σ-termine t
3. Yσ = Yρ per ogni variabile Y.
Dim. Banali implicazioni sono 1 → 2 e 2 → 3, basta provare 3 → 1. Mostriamo che σ ⊆
ρ. Sia data una coppia X/t di σ, allora per definizione X e t sono diversi e Xσ = t. Per la 3
si ha pure che Xρ = t e questo implica che X/t è anche una coppia di ρ, altrimenti Xρ
sarebbe stato X diversa da t. Analogamente si ha ρ ⊆ σ e quindi σ = ρ.
Definizione Composizione di due sostituzioni
Siano σ = {X1/t1,…,Xn/tn} e ρ = {Y1/s1,…,Ym/sm} due sostituzioni sulla medesima
segnatura.
Definiamo la composizione, σρ, di σ e ρ come la sostituzione
{Xi/tiρ | 1 ≤ i ≤ n e Xi ≠ t iρ}∪{Yj/sj | 1 ≤ j ≤ m e Yj∉dom(σ)}.
Esempio
Con σ = { X/Y, Z/s(0) }, ρ = {Y/f(W), Z/0}, σρ = { X/f(W), Z/s(0), Y/f(W)}.
Lemma Composizione di sostituzioni
Siano σ, ρ, τ sostituzioni sulla medesima segnaturaΣ, t un Σ-termine, allora
1. t(σρ) = tσρ
2. (σρ)τ = σ(ρτ)
3. εσ = σε = σ
4
Dim. (di 1) Notiamo che t(σρ) è l’applicazione della singola sostituzione σρ a t, mentre
tσρ è l’applicazione di ρ a tσ. Basta provare l’asserto per t che è una qualunque variabile
Z, si distinguono allora tre casi:
CASO
Z=Xi, 1 ≤ i ≤ n
Z= Yj , Yj ∉ dom(σ)
Z∉ dom(σ)∪dom(ρ)
Z( σρ)
tiρ
sj
Z
Zσ
ti
Yj
Z
Z σρ
tiρ
sj
Z
Come si vede la prima e la terza colonna sono identiche.
Unificazione
Sia T un insieme non vuoto di Σ-termini. Un unificatore di T è una sostituzione σ tale che
Tσ è un singoletto. Se esiste almeno un unificatore per T, allora T è detto unificabile.
Analogamente, un unificatore di un insieme non vuoto di formule M è una sostituzione σ
tale che ϕσ è ammissibile per ogni ϕ in M e Mσ è un singoletto. In particolare, un
unificatore di due termini s e t è un unificatore dell’insieme {s, t}e analogamente per due
formule.
Definizione Unificatore più generale (most general unifier, mgu)
Un unificatore µ di un insieme di termini o formule M è detto unificatore più generale,
mgu, sse ogni altro unificatore θ di M può essere ottenuto da µ componendolo con una
sostituzione τ, cioè θ = µτ. Ovvero, µ è il minimo fra gli unificatori di M rispetto all’ordine
≤ sull’insieme delle sostituzioni, dove σ ≤ θ sse esiste λ tale che θ = σλ.
Calcolo di sostituzioni mgu: Il Metodo delle Trasformazioni di Insiemi di Equazioni
Un modo di calcolare sostituzioni unificatrici di due o più termini è quello di riguardare
questo problema come quello del calcolo di sostituzioni che verifichino un dato sistema di
equazioni. Tale sistema di equazioni sarà costituito, inizialmente, dalle(a) equazioni t1 =? t2,
t2 =? t3,…,tn-1 =? tn, con n ≥ 2, se l’insieme dei termini da unificare è {t1, t2, t3,…,tn-1, tn};
l’equazione u =? v rappresenta il problema di unificazione fra u e v nel senso di dover
calcolare quelle sostituzioni σ tali che uσ = vσ. Nel caso dell’unificazione di due termini u
e v, il sistema iniziale sarà S = { u =? v}. L’idea che si propone è quella di trasformare tale
sistema, con manipolazioni che conservano le soluzioni (sostituzioni), fino ad arrivare ad
una forma, risolta, la cui sostituzione unificatrice risulti evidente.
Definizione Soluzione di un Sistema di Equazioni
Dato un insieme S di equazioni, S = {u1 =? v1, u2 =? v2,…,un =? vn}, una soluzione per S è
una sostituzione σ la cui applicazione ad S renda simultaneamente uguali i due membri di
ciascuna equazione, ossia tale che ∀i, 1 ≤ i ≤ n, ui σ = vi σ.
Definizione Equazione in Forma Risolta
Un’equazione X =? t (o t =? X) è in forma risolta in un insieme S sse X è una variabile che
non figura in nessun altro posto in S, in particolare X∉ var(t). La variabile X è detta allora
variabile risolta. Un sistema è allora in forma risolta se tutte le sue equazioni sono in
forma risolta; una variabile in S è irrisolta se non è una variabile risolta.
Si noti che un’equazione in forma risolta è possibile che sia X =? t con t pure variabile in
forma risolta oppure no. Allora, non tenendo conto dell’ordine dei due membri delle
equazioni, possiamo caratterizzare, conseguentemente, un sistema in forma risolta nella
seguente maniera:
5
Definizione Sistema Risolto
Un sistema S è risolto sse è della forma S = {X1 =? v1,…,Xn =? vn} dove X1,…,Xn sono
variabili distinte e per tutti gli i e j tali che 1 ≤ i, j ≤ n, Xi ∉ var(vj).
Un sistema in forma risolta definisce essenzialmente un’unica sostituzione, come si vede
nella seguente definizione.
Definizione Sostituzione Associata ad un Sistema Risolto
Dato un sistema S in forma risolta, definiamo la sostituzione associata ad S, σS, come
segue
se S = {X 1 =? v1,…, Xn =? vn} allora σS = {X1 / v1,…,Xn / vn}.
Nota Bene
Se in un’equazione X =? v entrambe X e v sono variabili risolte, possiamo avere in σS sia
X/v che v/X. Così σS non è univocamente definita. Tuttavia, si consideri che per ogni
coppia σS’ e σS” ottenuta da S, c’è una sostituzione ridenominante ( renaming) ρ, dove ρ è
determinata dalla equazione X =? v con entrambe X e v variabili risolte, tale che σS’ =
σS”ρ. In generale, una sostituzione ϕ è un renaming sse Xϕ è una variabile per ogni X in
dom(ϕ) e ϕ è anche iniettiva sul suo dominio, ossia se Xϕ = X’ϕ con X e X’ in dom(ϕ),
allora X = X’. Dato un termine t, variante(t) è tρ con ρ sostituzione renaming.
Così σS è univocamente definita a meno di ridenominazioni, inessenziali, di variabili.
Il fatto importante è che σS è un mgu idempotente di S, come verrà provato appena sotto.
Si ricorda che una sostituzione ϕ è idempotente sse ϕϕ = ϕ. Si vede facilmente che ϕ è
idempotente sse dom(ϕ) ∩ I (ϕ) = ∅, dove I(ϕ), insieme delle variabili introdotte da ϕ, è
definito da X ∈dom(ϕ ) var( Xϕ ) .
Lemma Un sistema in forma risolta rappresenta un mgu idempotente
Sia S = {X1 =? v1,…, Xn =? vn } in forma risolta. Allora σS è un mgu idempotente di S,
ossia tale che per ogni i, 1 ≤ i ≤ n, XiσS = Xi σSσS (=vi) e per ogni unificatore θ di S si ha θ
= σSθ.
Dim.
Osserviamo che per ogni tale θ, unificatore di S, si ha X iθ = viθ = XiσSθ per ogni i, 1 ≤ i ≤
n, e Xθ = XσSθ per ogni variabile X ∉dom(σS), quindi σS è un mgu. D’altra parte poiché
dom(σS) ∩ I(σS) = ∅ per definizione di sistema in forma risolta, è anche idempotente.
Possiamo riportare qui sotto un insieme di trasformazioni, chiamato , di un insieme di
equazioni in un insieme di equazioni, che definisce la relazione ⇒ST sull’insieme delle
coppie di insiemi di equazioni, atto a risolvere problemi di unificazione. Per un tale
insieme di trasformazioni le seguenti due proprietà fondamentali saranno dimostrate:
1. Correttezza o Soundness : Se S ⇒+ST S’allora tutti gli unificatori di S’ lo sono
anche di S
2. Completezza: Per ogni unificatore θ di S, c’è un insieme risolto S’ t.c. S ⇒+ST S’ e
σS’ ≤ θ | var(S).
Definizione Insieme di Trasformazioni Sia S un insieme (o sistema) di equazioni, eventualmente vuoto, u e v termini. è
costuito dalle seguenti trasformazioni:
a) {u =? u}∪ S ⇒ST S
( delete)
?
?
?
b) {f(u 1,…,un ) = f(v1,…,vn )} ∪S ⇒ST {u1= v1,…,un = vn }∪S (decomposition)
c) { X =? v }∪S ⇒ST {X =? v}∪S{X/v} e
{ v =? X }∪S ⇒ST {v =? X}∪S{X/v}
( variable elimination)
6
dove X è una variabile non risolta tale che X =? v (o v =? X) non è risolta in
{ X =? v }∪S, e X ∉ var(v) (occur check), ossia X ∈ var(S) – var(v).
Tale insieme è una variante dell’insieme di trasformazioni di Herbrand-MartelliMontanari per l’unificazione sintattica.
Mostriamo che è un insieme corretto e completo per l’unificazione sintattica. L’idea
base delle trasformazioni è quella di trasformare il problema originale in una forma risolta
che “rappresenta” la sua propria soluzione minima.
Esempio Dati i due termini f(X,g(a,Y)) e f(X,g(Y,X)), che unificano, il loro mgu è
determinato da
{f(X,g(a,Y)) =? f(X,g(Y,X))} ⇒dec
{X =? X, g(a,Y) =? g(Y, X)} ⇒del
{ g(a,Y) =? g(Y, X)} ⇒dec
{ a =? Y, Y =? X} ⇒vel
{ a =? Y, a =? X} in forma risolta! La sostituzione {Y/a, X/a} è l’unico unificatore dei due
termini e quindi è anche mgu.
Dati, invece, i due termini f(b,g(a,Y)) e f(X,g(X, Y)), che non unificano, si ha
{f(b,g(a,Y)) =? f(X,g(X, Y))} ⇒dec
{ b =? X, g(a,Y) =?g(X, Y)} ⇒vel
{ b =? X, g(a,Y) =?g(b, Y)} ⇒dec
{ b =? X, a =? b, Y =? Y}⇒del
{ b =? X, a =? b} non in forma risolta !
La prova di correttezza è data dal seguente lemma.
Lemma Correttezza di L’insieme di tutti gli unificatori di un sistema S sia denotato da U(S). Se S⇒ST S’ allora
U(S) = U(S’).
Dim. Casi banali sono quelli relativi a del e dec, la sola difficoltà riguarda vel.
Supponiamo che
{X =? v}∪ S ⇒vel {X =? v}∪ Sσ, con σ = {X/v}. Per ogni sostituzione θ, se Xθ = vθ
allora θ = σθ. Infatti, giacché σ consiste solo della coppia X/v, Xθ = vθ = Xσθ e per ogni
altra variabile Y, diversa da X, Yθ = Yσθ, quindi θ = σθ. Così,
θ ∈ U({X =? v}∪ S) sse
Xθ = vθ e θ ∈ U(S) sse , essendo θ = σθ,
Xθ = vθ e σθ ∈ U(S) sse , per proprietà della composizione (vedi 1. nel lemma p. 4)
Xθ = vθ e θ ∈ U(Sσ) sse
θ ∈ U({X =? v}∪ Sσ).
Il fatto notevole qui è che la più importante caratteristica di un problema di unificazione
– il suo insieme di soluzioni – è preservato dalle trasformazioni di e quindi siamo
giustificati nel perseguire lo scopo di trasformare il problema iniziale in una forma banale
ovvero risolta per cui l’esistenza di un mgu è evidente.
7
Teorema Completezza di Supponiamo che θ∈ U(S). Allora ogni sequenza di trasformazioni
S = S0 ⇒ST S1 ⇒ST …
deve prima o poi terminare in una forma risolta S’ tale che σS’ ≤ θ.
Dim. Mostriamo dapprima che ogni sequenza di trasformazioni termina. Per ogni sistema
S, definiamone una misura (di complessità) µ(S) = <n, m>, dove n è il numero delle
variabili irrisolte nel sistema e m è la somma delle dimensioni (numero di operatori e
variabili) di tutti i termini nel sistema. Si noti ora che l’ordine lessicografico sulle coppie di
naturali è ben fondato e ciascuna trasformazione produce un nuovo sistema con misura
strettamente minore rispetto a quest’ordine: delete e decompose fanno decrescere m e non
aumentare n e variable elimination fa decrescere n.
Quindi la relazione ⇒ST è ben fondata e ogni sequenza di trasformazioni deve terminare in
un sistema, in forma normale, al quale nessuna trasformazione è più applicabile.
Supponiamo che una sequenza termini in un sistema S’. Ora θ ∈ U(S) implica, per il
lemma di correttezza, che θ ∈ U(S’); così S’ non può contenere equazioni della forma
f(…) =? g(…) o della forma X =? v con X ∈ var(v), che chiaramente non hanno soluzioni.
Allora, giacchè nessuna trasformazione è applicabile, tutte le equazioni in S’ devono essere
in forma risolta. Infine, poiché θ ∈ U(S’), grazie al lemma sui sistemi in forma risolta si
dovrà avere σS’ ≤ θ.
Semantica della Logica dei Predicati
Abbiamo considerato le formule e i termini da un punto di vista sintattico. Rivolgiamo ora
la nostra attenzione al loro significato, introducendo vari concetti semantici.
Consideriamo la formula ∀Xp(X,X). Dal punto di vista sintattico questa è una opportuna
sequenza di caratteri tale da farla essere proprio una formula. Possiamo però attribuirle un
significato, intuitivamente tale formula dovrebbe stabilire “per tutti gli oggetti X, p(X, X) è
vero”. Per stabilire formalmente un significato sono necessarie le seguenti due definizioni:
1) stabilire un dominio nel quale la variabile X possa assumere valori
2) stabilire il significato del simbolo p come una relazione su coppie di elementi del
dominio.
dei naturali come dominio e interpretare
Possiamo, ad esempio, considerare l’insieme
p(X, Y) come la relazione “X è maggiore o uguale a Y”. Rispetto a questa interpretazione
o algebra la nostra formula è ovviamente vera. Ci sono tuttavia altre interpretazioni.
Consideriamo, dato il medesimo dominio, l’interpretazione che stabilisce p come la
relazione”X è minore di Y”. La nostra formula non vale più in questa interpretazione.
Daremo quindi una definizione formale di interpretazione e di cosa significhi essere una
formula valida in qualche interpretazione. Potremo quindi rappresentare certa conoscenza
tramite l’uso di formule e sarà allora importante stabilire che cosa significhi derivare o
estrarre ulteriori conoscenze da un insieme di formule. Questo ci porterà al fondamentale
concetto di conseguenza logica.
Algebre e Stati
Definizione Algebra o Interpretazione
Data una segnatura Σ, una Σ-algebra A è costituita da:
• un insieme non vuoto , dom(A), detto dominio di A;
• una funzione fA: dom(A)n → dom(A) per ogni operatore f in Fn, con n > 0 ;
• un elemento cA ∈ dom(A) per ogni costante c ∈ F0;
• una relazione r A ⊆ dom(A)n per ciascun simbolo di predicato r in Pn, con n>0;
• un valore di verità r A in {true, false} per ciascun atomo r in P0.
8
Definizione Stato
Sia A una Σ-algebra e V l’insieme delle variabili. Uno stato su A è una funzione, sta,
dall’insieme delle variabili V in dom(A). Se sta è uno stato su A, X1,…,Xn sono variabili
distinte e a1,…,an ∈ dom(A) allora sta(X1/a1,…, Xn /an) è lo stato modificato sta’ su A
definito da sta’(Xk) = ak per k =1,…,n e sta’(Y) = sta(Y) per Y ∉ {X1,…,Xn}, ossia sta’ è
lo stato sta modificato assegnando alle variabili X1,…,Xn (eventualmente) nuovi valori
a1,…,an e lasciando il resto inalterato.
Gli stati forniscono le informazioni necessarie alla valutazione di termini in una data
algebra.
Definizione Valutazione di Termini in un’Algebra e uno Stato
Data una Σ-algebra A, l’insieme delle variabili V, uno stato sta su A e un Σ-termine t,
definiamo il valore di t nell’algebra A nello stato sta, valA,sta(t), per induzione strutturale su
t, come segue:
valA,sta(X) = sta(X) per X ∈ V;
valA,sta(f(t1,…,tn)) = fA(valA,sta(t1),…, valA,sta(tn))
per ogni variabile X, funzione n-aria f e termini t1,…,tn. In particolare, per c costante si ha
valA,sta(c) = cA.
Esempio
Sia Σ = F0∪ F1∪F2 con F0 = {o}, F1 = {s}, F2 = {+}, e A l’algebra con dom(A) = ,il
numero 0 come interpretazione della costante o, funzioni successore, sA, e somma, +A,
come interpretazione di s e +. Sia sta tale che sta(X) = 3 e t = s(X) + s(o), allora
valA,sta(t) =
valA,sta(X)+A valA,sta(s(o)) =
3+A sA(valA,sta(o)) =
3+A sA(0) =
3+A1=
4.
Analogamente a quanto visto per i termini, possiamo definire formalmente la validità di
una formula in un’algebra in un dato stato.
Definizione Validità di una Formula in un’Algebra e uno Stato
Sia A una Σ-algebra, ϕ una formula e sta uno stato su A. Definiamo quando ϕ è valida
nell’algebra A nello stato sta, denotato da A |=staϕ:
• per ogni p in Pn con n>0, A |=sta p (t1,…,tn) sse (valA,sta(t1),…, valA,sta(tn))∈ pA;
• per ogni atomo in P0, A |=sta p sse p A = true;
• per generiche formule ϕ e ψ : A |=sta ~ϕ sse A |≠sta ϕ; {negazione}
• A |=sta (ϕ∧ψ) sse A |=sta ϕ e A |= staψ; {congiunzione}
• A |=sta (ϕ∨ψ) sse A |=sta ϕ o A |= sta ψ; {disgiunzione}
• A |=sta (ϕ → ψ) sse A |≠sta ϕ o A |= sta ψ; {implicazione}
• A |=sta ∀Xϕ sse A |=sta(X/a) ϕ per ogni a ∈ dom(A);
• A |=sta ∃Xϕ sse esiste un a ∈ dom(A) tale che A |=sta(X/a) ϕ;
Dato un insieme di formule M, A |=sta M sse A |=sta ϕ per ogni formula ϕ in M.
9
Esempio
Sia Nat = ( , 0, +) e A =(Nat, >), ϕ = ∀X∃Y Z + Y > X. Volendo verificare A |=sta ϕ , ci
aspettiamo che la validità dipenda da sta(Z), Z infatti è la sola variabile libera in ϕ.
A |=sta ϕ sse
per ogni a ∈ dom(A) A |= sta(X/a) ∃Y Z + Y > X sse
per ogni a ∈ dom(A) esiste un b ∈ dom(A) tale che A |=sta(X/a, Y/b) Z + Y > X sse
per ogni a ∈ dom(A) esiste un b ∈ dom(A) tale che sta(Z) + b > a.
L’ultima proposizione è vera qualunque valore sia sta(Z).
Per contro se ϕ = ∀X ∃Y Z + X > Y si ha che
A |=sta ϕ sse
….
….
sse
per ogni a ∈ dom(A) esiste un b ∈ dom(A) tale che sta(Z) + a > b
che è vera se e solo se sta(Z) > 0, infatti se sta(Z) = 0 esiste un valore a = 0 per X per cui
non esiste un valore b di Y che sia minore di 0.
Una proprietà (che esprime la validità) di una formula, che sarà usata in seguito, è espressa
dal seguente corollario.
Corollario Validità di ∀Xϕ → ϕ{X/r}
Sia ϕ una formula universale e r un termine, allora per ogni interpretazione A e stato sta su
A, A |=sta (∀Xϕ → ϕ {X/r}) e A |=sta (ϕ {X/r}→ ∃Xϕ) sono vere.
Modelli e Conseguenza Logica
Definizione Modello
Sia A una Σ-algebra, ϕ una Σ-formula e M un insieme di Σ-formule. Diciamo che A è un
modello di ϕ, denotato da A |= ϕ, sse A |=sta ϕ per tutti gli stati sta su A. Se M è un
insieme di Σ-formule, A è detta modello di M sse A è modello di ogni formula in M.
Per la relazione modello vale la seguente fondamentale proprietà.
Lemma Generalizzazione
A è un modello di una formula ϕ sse A è modello di∀(ϕ). Analogamente A è un modello
di un insieme di formule M sse A è un modello di ∀(M).
Dim. Siano X1,…,Xn le variabili libere di ϕ. Allora ∀(ϕ) è la formula ∀X1,…, ∀Xnϕ e
abbiamo
A |= ∀(ϕ) sse
per ogni stato sta su A A |=sta ∀X1,…, ∀Xnϕ sse
per ogni stato sta su A e ogni a1,…,an∈dom(A) A |=sta(X1/a1,...,Xn/an) ϕ sse
per ogni stato sta su A A |=sta ϕ sse
A |= ϕ.
È evidente infatti che la definizione della relazione modello implica una quantificazione
universale implicita delle variabili libere della formula. Come conseguenza, molte
proprietà vere per la relazione di validità |=sta non valgono per la relazione modello |=. Per
esempio, A |=sta(ϕ∨ψ) è equivalente, per definizione, a A |=sta ϕ ∨ A |=sta ψ. Invece, A |=
(ϕ∨ψ) non implica sempre A |= ϕ ∨ A |= ψ. Come esempio consideriamo A |= (p(X) ∨ ~
p(X))che è vera per qualunque algebra A, mentre né A |= p(X) né A |= ~ p(X) sono vere
per l’interpretazione A con dom(A) = e pA= l’insieme dei numeri pari. Analogamente, A
|=sta ~ ϕ è equivalente a A|≠sta ϕ, mentre A |≠ ϕ non implica A |= ~ ϕ, si riconsideri ϕ =
p(X). In generale, infatti,
A |≠ ϕ sse ~∀sta A |=sta ϕ sse ∃ sta A |≠sta ϕ e
10
A |= ~ ϕ sse ∀sta A |=sta ~ ϕ sse ∀sta A |≠sta ϕ.
La seguente ulteriore fondamentale proprietà vale per la relazione modello.
Lemma Modus Ponens
Se A |= ϕ e A |= (ϕ → ψ) allora A |= ψ.
Dim. Si assuma che A |= ϕ e A |= (ϕ → ψ). Ciò significa che A |=sta ϕ e A |=sta (ϕ →
ψ)per ogni stato sta su A. Dalla definizione di |=sta relativamente all’implicazione, si ha
che A |=sta ψ per ogni stato sta su A, quindi A |= ψ vale.
Siamo ora in grado di rivolgere la nostra attenzione all’importante concetto dell’essere una
formula “conseguenza logica” di un dato insieme di formule.
Definizione Conseguenza Logica
Una formula ϕ è conseguenza logica di un insieme di formule M (M implica logicamente
ϕ), M |= ϕ, sse ogni modello di M è anche modello di ϕ, ossia sse ∀A(A |= M → A |= ϕ).
Vari commenti sono da fare a proposito di questa definizione.
a. Il simbolo |= è usato per denotare due diversi concetti, la relazion e modello A |=
ϕ per una interpretazione A e la relazione di conseguenza logica M |= ϕ per un
insieme di formule M.
b. Trarre conclusioni da un insieme di formule (assiomi) M significa “estrarre”
ulteriore conoscenza implicitamente già presente in quella rappresentata dagli
assiomi di M. Tale processo è generalmente assai sofisticato e delicato,
specialmente pensando alla possibilità di rendere automatico tale processo.
c. Dalla definizione della relazione modello, si ha il seguente semplice ed importante
fatto: M |= ϕ sse ∀ (M) |= ∀(ϕ), ossia nell’estrazione di conoscenza sia M che ϕ
devono essere pensati universalmente implicitamente quantificati.
Esempio
Formalizziamo l’addizione sui naturali; per evitare l’uso dell’uguaglianza ci serviamo del
predicato ternario add, usiamo la costante 0 e la funzione unaria succ. Il significato inteso
della relazione add dovrebbe essere {(succn(0), succm(0), succn+m(0))| n,m ≥ 0}, dove fk(.)
sta per f(..f(.)..) con l’annidamento delle f di profondità k. Consideriamo il seguente
sistema di assiomi Add:
a1) ∀X add(X,0,X)
a2) ∀X ∀Y∀Z (add(X,Y,Z) → add(X,succ(Y),succ(Z)))
Questo sistema formalizza l’usuale definizione ricorsiva della somma. Le seguenti formule
seguono da Add per tutti i naturali n ed m:
add(succn(0), succm(0), succn+m(0))
∀X add(X, succm(0), succm(X)).
Si può procedere in entrambi casi per induzione su m.
Si vuol provare allora che Add |= add(succn(0), succm(0), succn+m(0)):
Base m = 0, Add |= add(succn(0), 0, succn(0))vale, infatti grazie alla validità di ∀Xϕ →
ϕ{X/r}, si ha pure la validità di ∀X add(X,0,X) → add(X,0,X){X/succn(0)} ed essendo
l’antecedente la formula a1) di M essa è valida in ogni modello di M, quindi anche il
conseguente è valido in ogni modello di M.
11
Induzione Sia m = k+1 e Add |= add(succn(0), succk(0), succn+k(0))l’ipotesi induttiva;
grazie alla validità di ∀Xϕ → ϕ{X/r}, con ϕ = a2) ed r opportuno si ha pure
Add |= add(succn(0), succk(0), succn+k(0))→ add(succn(0), succk+1(0), succn+k+1(0)).
Grazie all’ipotesi induttiva ed al lemma sul Modus Ponens si ha allora
Add |= add(succn(0), succk+1(0), succn+k+1(0)), come richiesto.
D’altra parte occorre procedere con molta cautela nel concludere che certe formule sono
conseguenza logica di altre, magari affidandoci alla sola nostra intuizione.
Si consideri, ad esempio, la formula
∀Yadd(succn(0), Y, succn(Y))
che sembrerebbe affermare una verità simile a quelle sopra viste: essa non segue
logicamente da Add, infatti è solo una conseguenza induttiva di Add. In altri termini i due
assiomi a1) e a2) non bastano ad implicarla logicamente, ma occorrono anche degli
assiomi che stabiliscano un qualche schema di prova per induzione. Per convincerci che
questa formula non è conseguenza logica di Add, cons ideriamo la interpretazione A con
dom(A) =
∪ {-1}, 0A= il naturale 0, succA = la funzione successore su ∪ {-1} e
addA = {(a, succm(0), succm(a)) | a∈ dom(A) e m ∈ }.
A è un modello di Add (la cui prova è lasciata al lettore), ma non lo è della formula
proposta. Infatti non è vero che per ogni Y in dom(A), la terna delle interpretazioni di
succn(0), Y e succn(Y) appartenga alla relazione addA; esistono infatti le infinite terne della
forma (succn(0), -1, succn(-1)) che non stanno in addA.
Ulteriori Importanti Concetti Semantici
Ci riferiamo ai concetti di validità e soddisfacibilità di formule ed alle loro relazioni.
Definizione Validità e Soddisfacibilità
Sia A unaΣ-algebra, ϕ una Σ-formula e M un insieme di formule.
• ϕ è valida nell’algebra A sse A è modello di ϕ;
• ϕ è valida sse ϕ è valida in ogni Σ-algebra A;
• ϕ è soddisfacibile nell’algebra A sse A |=sta ϕ per qualche stato sta su A;
• ϕ è soddisfacibile sse per qualche algebra A ϕ è soddisfacibile in A; se ϕ è una
formula chiusa ϕ è soddisfacibile sse per qualche algebra A, A è modello di ϕ.
Analoghe definizioni per un insieme di formule M.
Esempio
Mostriamo che la formula ∃X(p(X) → ∀Yp(Y)) è valida. Consideriamo una qualsiasi
algebra A ed un generico stato sta su A. Dobbiamo mostrare che A |=sta ∃X(p(X) →
∀Yp(Y)), ossia che esiste in dom(A) un a tale che A |=sta(X/a) (p(X)→ ∀Yp(Y)).
Due casi sono possibili: se pA = dom(A) scegliamo un a arbitrario, giacchè in questo caso
A |=sta(Y/b) p(Y) vale per ogni b in dom(A) si ha la desiderata conclusione per la
definizione di A |=sta nel caso dell’implicazione; se pA ≠ dom(A) scegliamo un elemento
a∈dom(A) – pA; poiché in questo caso A|≠sta(X/a)p(X), la conclusione desiderata segue
ancora dalla definizione di validità in un’algebra in uno stato nel caso dell’implicazione.
Le seguenti equivalenze seguono facilmente dalle definizioni date.
• ϕ è soddisfacibile sse ~ ϕ non è valida;
• ϕ è valida sse ~ϕ non è soddisfacibile.
La seguente affermazione è vera.
• A |= ~ϕ implica A|≠ ϕ, infatti A |= ~ϕ sse ∀sta A |=sta ~ϕ sse ∀sta A|≠sta ϕ e
A|≠ ϕ sse ~ ∀sta A |=sta ϕ sse ∃sta A|≠sta ϕ .
12
Trasformazione di Formule in Forma Normale
Vari tipi di proprietà delle formule ci permettono di trasformare una formula in una forma
normale per la quale certe proprietà semantiche restano valide: per alcune trasformazioni si
ha l’equivalenza ossia la validità in un’algebra qualsiasi ed in uno stato qualsiasi, per altre
si conserva semplicemente la soddisfacibilità. La ricerca di forme normali è motivata dal
semplice fatto che determinare qualche loro proprietà semantica è più semplice rispetto alla
loro forma originale.
Le proprietà che useremo sono ben note quali quelle di associatività e distributività dei
connettivi di congiunzione e disgiunzione o le seguenti:
ϕ→ψ sse ~ϕ∨ψ
ϕ∧ψ sse ~(~ϕ∨~ψ)
ϕ∨ψ sse ~(~ϕ∧~ψ)
che permettono, insieme ad altre, di trasformare una formula senza quantificatori in una
equivalente dove figurano solo i connettivi di negazione, disgiunzione e congiunzione.
Forme Normali Prenesse
La prima trasformazione sarà quella di spostare tutti i quantificatori in testa alla formula,
ottenendo una formula equivalente in forma normale prenessa. Tale trasformazione in una
formula equivalente è garantita dal seguente lemma.
Lemma Equivalenze
Le seguenti coppie di formule sono equivalenti, per Q ∈ {∀, ∃}.
1. ~∀Xϕ e ∃X~ϕ
2. ~∃Xϕ e ∀X~ϕ
3. (ϕ ∧QXψ) e QX(ϕ ∧ψ), X non libera in ϕ
4. (QXϕ ∧ψ) e QX(ϕ ∧ψ), X non libera in ψ
5. (ϕ∨QXψ) e QX(ϕ∨ψ), X non libera in ϕ
6. (QXϕ ∨ψ) e QX(ϕ∨ψ), X non libera in ψ
7. (ϕ→∀Xψ) e ∀X (ϕ→ψ), X non libera in ϕ
8. (∀Xϕ→ψ) e ∃X (ϕ→ψ), X non libera in ψ
9. (ϕ→∃Xψ) e ∃X (ϕ→ψ), X non libera in ϕ
10. (∃Xϕ→ψ) e ∀X (ϕ→ψ), X non libera in ψ
11. QXϕ e QYϕ {X/Y} (ridenominazione di variabile)
Definizione Forma Normale Prenessa
Una formula della forma QX1…QXnϕ con ϕ senza quantificatori è detta in forma normale
prenessa.
Teorema
Per ogni formula ϕ possiamo effettivamente costruire una formula equivalente in forma
normale prenessa.
Dim. Applicando opportunamente le equivalenze del lemma precedente.
Esempio
Data la formula ∀X∃Y((∀Xp(X)→q(X,f(Y),Z))∧~∀Z∃X~r(g(X,Z),Z)), applicando le
equivalenze del lemma si ottiene la seguente formula equivalente in forma normale
prenessa:
13
∀X∃Y∃U∃V∀W((p(U) → q(X, f(Y), Z)) ∧~ ~ r(g(W,V), V)).
Forme Normali Congiuntive e Disgiuntive
Una volta posta una formula in forma normale prenessa, trattiamo il nucleo senza
∧1≤ i ≤n γi la formula
(…∧γn)…), detta congiunzione finita e analogamente con ∨1≤ i ≤n γi la formula
quantificatori della formula ottenuta. Indichiamo con
(γ1∧(γ2∧
(γ1∨(γ2∨
(…∨ γn)…), detta disgiunzione finita. Vale il seguente teorema.
Teorema
Per ogni formula senza quantificatori possiamo effettivamente costruire le formule
equivalenti γ e δ tali che γ è una congiunzione finita di disgiunzioni finite di letterali:
γ = ∧1≤ i ≤nγi con γi = ∨1≤ j ≤m(i) Lij con Lij letterale e
δ è una disgiunzione finita di congiunzioni finite di letterali:
δ = ∨1≤ i ≤nδi con δi = ∧1≤ j ≤m(i) Lij con Lij letterale.
γ è detta in forma normale congiuntiva e
δ è detta in forma normale disgiuntiva.
Dim.
Usando le proprietà ricordate all’inizio del paragrafo e l’induzione sulla struttura della
formula.
Ma quali vantaggi sono offerti da queste forme? Il seguente teorema riporta alcuni di essi:
stabilisce che per formule ground proprietà semantiche sono ricondotte a semplici (e
decidibili) casi sintattici.
Teorema Validità e Soddisfacibilità di Congiunzioni e Disgiunzioni di Letterali Ground
Sia Σ una segnatura contenente almeno un operatore costante, e L1,…Ln siano letterali
ground in Σ.
Forma
Proprietà
Caratterizzazione Sintattica
a) L1∧…∧Lk ha un modello sse{ L1,…,Lk }non contiene coppie complementari
non è mai il caso
b) L1∧…∧Lk è valida
ha
un
modello
è
sempre il caso
c) L1∨…∨Lk
sse{ L1,…,Lk }contiene coppie complementari
d) L1∨…∨Lk è valida
Per la dimostrazione di questo teorema occorre utilizzare il seguente concetto di algebra
alla (o di) Herbrand.
Definizione Algebra alla Herbrand
Sia Σ una segnatura con almeno una costante. Una Σ-algebra A è detta algebra alla
Herbrand sse
• dom(A) = T(Σ), ossia l’insieme di tutti i Σ-termini ground, detto l’universo di
Herbrand;
• fA(t1,…,tn) = f(t 1A,…,tnA) dove f ∈ Fn e per ogni i t i, tiA ∈ T(Σ) e t iA è la
valutazione in A di t i; si noti che fA è davvero una funzione da dom(A) n in dom(A)
mentre f è solo un simbolo;
• pA ⊆ T(Σ)n se p∈ Pn con n>0 e p A∈{true, false}se p è un atomo in P 0.
Si noti che la sola cosa non prefissata in un’algebra alla Herbrand è l’interpretazione dei
simboli di predicato.
Per quanto riguarda la valutazione di un termine in un’algebra di Herbrand in uno stato,
vale il seguente lemma.
14
Lemma Valutazione di un termine in un’algebra di Herbrand
Sia Σ una segnatura con almeno un operatore costante e A una Σ-algebra di Herbrand. Sia t
un termine con variabili X1,…,Xn e sta uno stato su A con sta(X1) = t1,…,sta(Xn) = tn dove
i ti sono termini ground. Allora tA = valA,sta(t) = t{X1/ t1,…,Xn/tn}. In particolare, se t è
ground allora t A = t.
Allora la valutazione di un termine in un’algebra di Herbrand ed in uno stato coincide con
l’applicazione di una sostituzione ed i termini ground valgono loro stessi.
Siamo ora in grado di dare la dimostrazione del teorema sulla Validità e Soddisfacibilità di
Congiunzioni e Disgiunzioni di Letterali Ground.
Dim. Teor. su Validità e Soddisfacibilità di Congiunzioni e Disgiunzioni di Letterali Ground
Caso a). Consideriamo la formula L1∧…∧Lk e assumiamo che {L1,…,Lk} non contenga
coppie complementari come L e ∼L. Definiamo un’algebra alla Herbrand A come segue:
per ogni simbolo di predicato p, pA = {(t1,…,tn) ∈T(Σ)n | {L1,…,Lk}contiene il letterale
p(t1,…,tn)}.
Mostriamo che A è un modello di ogni Li. Se Li è positivo, diciamo Li = p(t1,…,tn) allora
p(t1,…,tn) è evidentemente presente in {L1,…,Lk}. Per definizione di pA si ha allora che
(t1,…,tn) ∈ pA. Quindi A è modello di Li. Se Li è un letterale negativo, diciamo
~p(t1,…,tn), allora p(t1,…,tn) non appare fra i letterali L1,…,Lk, altrimenti {L1,…,Lk}
conterrebbe una coppia complementare. Allora, per definizione di pA, (t1,…,tn)∉ pA. A è
quindi un modello di ~ p(t1,…,tn) ossia di Li .
Caso b). Ricordando che una formula ϕ è valida sse ~ϕ non è soddisfacibile, formule della
forma L1∧…∧Lk non sono valide giacchè ~ (L1∧…∧Lk) = ~ L1∨…∨~Lk è sempre
soddisfacibile in quanto ~ L1 ha sempre un modello, per quanto sopra mostrato (caso a)).
Caso c). Una formula della forma L1∨…∨Lk ha sempre un modello infatti L1 ha sempre un
modello, come mostrato nel caso a)).
Caso d). Per quanto ricordato al punto b), una formula della forma L1∨…∨Lk è valida sse
~ L1∧…∧~Lk non possiede un modello sse, per quanto visto al punto a), {~ L1,…,~Lk}
contiene una coppia complementare, ossia sse
{L1,…,Lk} contiene una coppia
complementare.
Forme Normali di Skolem
Volgiamo ora la nostra attenzione ai quantificatori esistenziali e cerchiamo un metodo per
eliminarli, avendo così solo quantificatori universali, in modo tale da mantenere la
soddisfacibilità. A questo riguardo vale il seguente fondamentale teorema.
Teorema Skolemizzazione
Sia ∀X1…∀Xn∃Yϕ una Σ-formula chiusa con variabili distinte X1,…,Xn,Y. Assumiamo
che ϕ non contenga quantificazioni QX1…,QXn così che ϕ{Y/g(X1,…,Xn)} è ammissibile.
Estendiamo Σ aggiungendole un nuovo simbolo di funzione n-aria g, detta funzione di
Skolem, ottenendo la segnatura Σg = Σ ∪ Fn’ dove Fn’= Fn∪{g}. Allora
• Ogni Σg-modello di ∀X1…∀Xnϕ{Y/g(X1,…,Xn)} è un modello
di∀X1…∀Xn∃Yϕ.
• Viceversa, ogni Σ-modello di ∀X1…∀Xn∃Yϕ può essere esteso ad un Σgmodello di ∀X1…∀Xnϕ{Y/g(X1,…,Xn)}.
Quindi le seguenti affermazioni sono equivalenti:
• esiste un modello di ∀X1…∀Xn∃Yϕ
• esiste un modello di ∀X1…∀Xnϕ{Y/g(X1,…,Xn)}.
15
Esempio
∃Yr(Y) ha un modello sse r(c) ha un modello, in questo caso la funzione di Skolem è la
costante c.
∀X∃Yp(X,Y) ha un modello sse ∀Xp(X,g(X)) ha un modello.
Definizione Forma Normale di Skolem e Clausola
Una formula chiusa della forma∀X1…∀Xnϕ, dove ϕ, che non contiene quantificatori, è in
forma normale congiuntiva è detta forma normale di Skolem. In questo caso tutte le
variabili di ϕ sono quantificate universalmente e ciascuna formula dei suoi congiunti, che è
una disgiunzione finita di letterali, è chiamata clausola.
Da quanto fin qui esposto possiamo dare il seguente utile teorema.
Teorema Trasformazione in Forma Normale di Skolem
Per ciascuna formula ϕ possiamo effettivamente costruire una formula ψ in forma normale
di Skolem tale che ϕ ha un modello sse ψ ha un modello.
Le formule in forma normale di Skolem sono quindi formule chiuse universalmente
quantificate. Il seguente fondamentale teorema, dovuto a Herbrand, mostra alcuni vantaggi
di tali formule chiuse universali.
Teorema di Herbrand
Sia Σ una segnatura con almeno una costante e M sia un insieme di formule chiuse
universali. Chiamiamo ground-instances(M) l’insieme di tutte le formule ψ{X1/t1,…,Xn/tn}
dove ψ che non contiene quantificatori è tale che ∀X1…∀Xn ψ è un elemento di M e
t1,…,tn sono termini ground.
I seguenti fatti sono equivalenti:
a. M ha un modello
b. M ha un modello di Herbrand
c. ground-instances(M) ha un modello
d. ground-instances(M) ha un modello di Herbrand.
Dim.Giacchè i modelli alla Herbrand sono per l’appunto modelli e giacchè le formule della
forma
∀X1…∀Xn ψ → ψ{X1/t1,…,Xn/tn}
sono valide, le implicazioni della figura seguente sono banali
b→a
↓ ↓
d→c
Per provare l’intero teorema è sufficiente mostrare l’implicazione c → b.
Sia A un modello di ground-instances(M) dato. Definiamo un’algebra di Herbrand B come
segue:
pB = {( t 1,…,tn) ∈T(Σ) n | A |= p(t1,…,tn)}, per ogni p in Pn, n>0
pB = pA per ogni p in P 0
ossia B |= p(t 1,…,tn) sse A |= p(t 1,…,tn).
Ciò significa che le medesime formule atomiche ground sono valide in A ed in B. Questo
risultato è direttamente esteso, con una semplice induzione strutturale, a formule qualsiasi
ground.
Infine, si mostra che B è un modello di M. Sia ∀X1…∀Xnψ una formula in M con ψ senza
quantificatori. Concludiamo che B |= ∀X1…∀Xn ψ è vero sse per tutti i t1,…,tn ∈T(Σ)
B |= ψ{X1/t1,…,Xn/tn}. Ora quest’ultima affermazione è equivalente alla seguente: per
tutti i t1,…,tn ∈T(Σ) A |= ψ{X1/t1,…,Xn/tn}.
16
Ma quest’ultima affermazione è vera poiché A è un modello di grond-instances(M) e
ψ{X1/t1,…,Xn/tn}, per quanto detto, è un elemento di tale insieme.
Come base della tecnica, che useremo in seguito, di dimostrazione per refutazione
dell’essere una formula ϕ conseguenza logica di un insieme di formule M, ci serviamo del
seguente teorema.
Teorema Conseguenza Logica e Insoddisfacibilità
Se una formula chiusa ϕ segue logicamente da un insieme di formule chiuse M, allora
l’insieme M∪{~ϕ} è insoddisfacibile.
Viceversa, se M∪{~ϕ} è insoddisfacibile e M è soddisfacibile allora ϕ è conseguenza
logica di M.
Dim.
Si supponga che ϕ sia conseguenza logica di M e che un’algebra A sia modello di M.
Allora A è pure modello di ϕ e non può essere modello di ~ϕ, quindi neache di M∪{~ϕ}.
M∪{~ϕ} è allora insoddisfacibile.
Sia ora M∪{~ϕ} insoddisfacibile e sia A un modello di M. A allora non può essere un
modello di ~ϕ, per l’ipotesi di insoddisfacibilità; allora A è un modello di ϕ da cui segue
che ϕ è conseguenza logica di M.
A questo punto è interessante provare l’insoddisfacibilità di un dato insieme, M∪{~ϕ},
usando solo semplici manipolazioni sintattiche ossia senza introdurre esplicitamente
nozioni e considerazioni semantiche. A questo scopo faremo uso della nozione di Calcolo
(vedi il prossimo capitolo Teoria della Dimostrazione), che in generale può essere
utilizzato per generare teoremi di una assegnata teoria assiomatica (insieme di formule)
ovvero conseguenze logiche della teoria, se il calcolo gode della proprietà di correttezza.
Teoria della Dimostrazione
Abbiamo fin qui trattato della questione centrale dell’essere una formula conseguenza
logica di un dato insieme di formule.
Un problema, da un punto di vista algoritmico, per decidere questa proprietà è che si
riferisce ad una infinità di oggetti quali le interpretazioni. Lo scopo della teoria della
dimostrazione è quello si sostituire nozioni semantiche con nozioni, equivalenti, puramente
sintattiche.
Introduciamo quindi il concetto di Calcolo il cui scopo è quello di rendere possibile la
derivazione di formule, tramite opportune regole, da un dato insieme di formule iniziali,
dette assiomi.
Un calcolo deve essere corretto, ossia tale che ogni formula che può essere derivata da un
insieme M, detta teorema, segua anche logicamente da M. Un calcolo dovrebbe essere
anche completo, ossia tale che ogni formula che segue logicamente da M sia pure un
teorema di M rispetto a quel calcolo.
Il Concetto di Calcolo
Un calcolo è cosituito da un insieme di regole che consentono di generare o derivare da un
dato insieme M di formule, altre formule.
Formule che possono essere inizialmente derivate sono solo quelle di M, dette assiomi, o
comunque assiomi che fanno parte del calcolo stesso.
Una regola di inferenza, o semplicemente regola, è rappresentata graficamente nel modo
seguente:
π 1 πn
con n ≥ 0
γ
17
dove π1…πn , dette premesse e γ, detta conclusione, sono formule in cui possono figurare
anche variabili sull’insieme delle formule. Una tale regola consente di derivare una istanza
di γ in accordo ad una sostituzione generalizzata σ (sulle variabili per le formule e per i
termini), a patto che le istanze corrispondenti, sempre secondo σ, delle premesse siano già
state derivate. Occorre allora poter derivare inizialmente formule senza il requisito che
altre formule siano già state derivate, a questo scopo possiamo usare regole senza
premesse. In una regola con n = 0, la sua conclusione prende il nome di assioma; una tale
regola ci permette di derivare una qualsiasi istanza della sua conclusione in qualsiasi
momento. Possiamo ora definire che cosa sia una derivazione (o deduzione, dimostrazione)
in accordo ad un dato calcolo C.
Definizione Derivazione
Sia C un calcolo. Una derivazione in C da un insieme di assiomi M è una sequenza finita di
formule (ψ1,…, ψm) tale che per ogni i = 1,…,m, ψi o è l’istanza di un assioma di M o C o
esiste una istanza π1σ…πnσ e γσ delle premesse e conclusione di una regola tale che ψi =
γσ e {π1σ,…,πnσ }⊆ {ψ1,…, ψi -1}.
Definizione Teorema
Sia C un calcolo ed M un insieme di assiomi. Se per una formula ψ esiste una derivazione
in C da M (ψ1,…, ψm) con ψm = ψ, allora ψ è detta un teorema di M in C, in simboli
M |−C ψ.
A titolo di esempio utile per ulteriori confronti con altri calcoli e investigazioni, riportiamo
il seguente calcolo alla Hilbert.
Un Calcolo di Hilbert
Le formule che considereremo, senza nessuna perdita in generalità, sono formule
“ristrette”, che contengono cioè solo i connettivi di implicazione e negazione ed il
quantificatore universale.
Assiomi e Regole del Calcolo di Hilbert
Questo calcolo consiste di cinque (schemi di) assiomi A1-A5 e due regole MP e GEN,
Modus Ponens e Generalizzazione rispettivamente. In particolare, se una formula può
essere derivata da A1- A5 e MP è detta tautologia.
A1:
ϕ → (ψ → ϕ )
A2:
(ϕ → (ψ → χ )) → ((ϕ → ψ ) → (ϕ → χ ))
A3:
(¬ϕ → ¬ψ ) → (ψ → ϕ )
A4:
∀Xϕ → ϕ { X / t}
A5:
∀X (ϕ → ψ ) → (ϕ → ∀Xψ )
purchè ϕ{X/t} sia ammissibile
purchè X non sia libe ra in ϕ
(ϕ → ψ ), ϕ
ψ
ϕ
GEN:
∀Xϕ
MP:
18
Esempio
Dato il seguente insieme di assiomi
M = {M1: ∀X add(X,0,X), M2:∀X∀Y∀Z(add(X,Y,Z) → add(X,s(Y),s(Z))}
si vuol provare che, chiamando H il calcolo di Hilbert,
M |−H add(s(0), s(0),s(s(0))).
In effetti, possiamo esibire la seguente dimostrazione:
D1: ∀X∀Y∀Z(add(X,Y,Z) → add(X,s(Y),s(Z)))) →∀Z(add(s(0),0,Z)→add(s(0),s(0),s(Z))
Istanzadi A4
D2: ∀X∀Y∀Z(add(X,Y,Z) → add(X,s(Y),s(Z))))
Istanza di M2
D3 : ∀Z(add(s(0),0,Z)→add(s(0),s(0),s(Z))
MP applicata a D1 e D2
D4: ∀Z(add(s(0),0,Z)→add(s(0),s(0),s(Z))→ (add(s(0),0,s(0))→add(s(0),s(0),s(s(0)))
Istanza di A4
D5: add(s(0),0,s(0))→ add(s(0),s(0),s(s(0))
MP applicata a D3 e D4
D6: ∀X (add(X,0,X))→ add(s(0),0,s(0))
Istanza di A4
D7: ∀X add(X,0,X)
Istanza di M1
D8: add(s(0),0,s(0))
MP applicata a D6 e D7
D9: add(s(0),s(0),s(s(0))
MP applicata a D8 e D5
Teorema Correttezza del Calcolo di Hilbert
Per ogni insieme di formule M e formula ϕ se
M |−H ϕ allora M |= ϕ.
Dim.
La prova viene data per induzione sulla lunghezza della derivazione; è diretta conseguenza
del seguente lemma sulla validità degli assiomi A1 – A5 e dei lemmi sul Modus Ponens e
di Generalizzazione già visti (pag. 10 e 11), che stabiliscono la conservazione della validità
delle due regole MP e GEN del calcolo di Hilbert.
Lemma Validità degli Assiomi A1 – A4
Ogni istanza degli assiomi A1 – A5 è valida.
Dim.
Data una istanza qualsiasi, ϕ, di un assioma del calcolo di Hilbert, un’algebra qualsiasi A e
uno stato sta su A, si deve mostrare che A |=staϕ.
Cominciamo con un’istanza generica ϕ→(ψ→ϕ) di A1.
Per assurdo sia A |≠sta ϕ→(ψ→ϕ). Allora si ha
A |=staϕ e
A |≠sta (ψ→ϕ) per la definizione di |= sta per l’implicazione, os sia
A |=staψ e
A |≠sta ϕ sempre per la definizione di |= sta per l’implicazione
Così è stata ottenuta una contraddizione.
19
Si procede analogamente per A2 e A3. L’assioma A4 è stato mostrato nel corollario
Validità di ∀Xϕ → ϕ{X/r} a pag. 10 e A5 nel lemma sulle Equivalenze a pag. 13.
Per arrivare a mostrare la completezza del calcolo di Hilbert, occorre introdurre alcuni
ulteriori concetti, che riportiamo brevemente di seguito.
Definizione Consistenza
Sia M un insieme di Σ-formule. M è detto Σ-consistente sse non esiste alcuna Σ-formula ϕ
tale che M |−H ϕ e M |−H ~ϕ.
Lemma Consistenza
Sia M un insieme di Σ-formule e ϕ una Σ-formula. Le seguenti proposizioni sono vere.
1. Se M ha un modello allora M è Σ-consistente;
2. M |−H ϕ sse M |−H ∀(ϕ) sse M∪{~ ∀(ϕ)} non è consistente.
Dim.
Mostriamo il punto 1. Sia A un modello di M. Se M non fosse consistente, esisterebbe una
formula ϕ tale che M |−H ϕ e M |−H ~ϕ. Giacchè il calcolo di Hilbert è corretto, avremmo
per A modello di M sia A |= ϕ che A |= ~ϕ, che è una contraddizione.
Il seguente lemma mostra come i due concetti di consistenza e soddisfacibilità siano
equivalenti, valendo il punto 1. del lemma di consistenza.
Lemma del Modello
Sia M un insieme di Σ-formule Σ-consistente. Allora esiste un Σ-modello A (la prova sarà
data tramite un modello di Herbrand) di M.
Possiamo dare il seguente teorema di completezza di Gödel.
Teorema Completezza
Sia M un insieme di formule e ϕ una formula. Allora M |= ϕ implica M |−H ϕ.
Dim.
Supponiamo che M |−H ϕ non valga. Allora per il punto 2. del lemma di consistenza si ha
che M∪{~ ∀(ϕ)}è consistente. Per il lemma del modello esiste allora un modello A di
M∪{~ ∀(ϕ)}. Quindi A è un modello di M ma non di ϕ. Questo mostra la contraddizione
M |≠ ϕ.
Possiamo ora volgere la nostra attenzione ad un altro calcolo che riveste una fondamentale
importanza dal punto di vista dell’informatica, in quanto è la base del paradigma di
programmazione logica : il Calcolo della Risoluzione.
Il Calcolo della Risoluzione per Clausole Generali
Le formule trattate con questo calcolo sono espresse tramite clausole, in generale da
congiunzioni di clausole, in cui le variabili sono tutte quantificate universalmente, vedi
sopra forme normali di Skolem. Una congiunzione (finita) di clausole è espressa come un
insieme di clausole e, a sua volta, una clausola come l’insieme dei suoi letterali ( i suoi
disgiunti).
Questo calcolo non possiede alcun assioma e la sua unica regola (principio di risoluzione)
è la seguente:
{L1,..., Lm} ∪ C1 {Lm + 1,..., Lm + n} ∪ C 2
(RES)
(C1 ∪ C2) µ
20
per tutti gli m, n ≥ 1, letterali L1,…,Lm+n, clausole C1 e C2, mgu µ di{L1,…, Lm, ~Lm +1,…,
~Lm+n}, con le due clausole{L1,…, Lm}∪ C1 e {Lm +1,…, Lm+n}∪ C2, senza variabili in
comune, che sono dette parent. La clausola (C1 ∪ C2) µ è detta risolvente delle due
clausole parent via la sostituzione µ.
Sia {L1,…, Lm}µ = {L} e quindi {Lm +1,…, Lm+n}µ = {~L}, allora tale regola ci permette di
eliminare una coppia complementare di letterali, L e ~L, e di riunire l’istanza
corrispondente dei rimanenti letterali in un’unica clausola. Ovviamente proveremo che tale
calcolo è sia corretto che completo.
A questo riguardo, facciamo notare che considereremo due varianti della regola RES:
RESext che al posto della mgu µ usa un qualsiasi unificatore σ di {L1,…, Lm, ~Lm +1,…,
~Lm+n} e RESprop che al posto di µ usa ε, ossia la sostituzione vuota. In quest’ultimo caso
RESprop è applicabile se {L1,…, Lm, ~Lm +1,…, ~Lm+n} è già un singoletto.
Dimostrazione per Refutazione con la Risoluzione
Dato un insieme di assiomi M e una formula ~ϕ, il tutto in forma clausale, derivando nel
calcolo RES da M∪{~ϕ}la contraddizione logica ossia la clausola vuota denotata da , si
dimostra che M∪{~ϕ}, data la correttezza di RES, è insoddisfacibile ossia che ϕ è
conseguenza logica di M. Per la completezza di RES, se partiamo da C0 = M∪{~ϕ} e
aggiungiamo a tale insieme tutti i suoi risolventi, iterando il procedimento si può arrivare a
derivare un insieme di clausole contenente la clausola vuota tutte le volte che ϕ è
conseguenza logica di M ovvero tutte le volte che C0 è insoddisfacibile. Quindi possiamo
delineare la seguente
Procedura (Ingenua) di Dimostrazione per Refutazione via RES : Dato l’insieme C0
1. i := 0
2. Ci+1:= Ci ∪{risolventi di Ci}
3. se Ci+1 contiene , allora stop con successo, altrimenti i := i+1 e vai a 2.
Riportando in seguito alcuni esempi di applicazione del calcolo della Risoluzione, usando
la procedura sopra vista, rappresenteremo una dimostrazione tramite il suo albero di
derivazione.
Definizione Albero di Derivazione
Un albero di derivazione in un calcolo C, in cui sono date le regole di inferenza del tipo
π 1 πn
con n ≥ 0,
γ
è un albero finito T (con la radice in basso e le foglie in alto) i cui nodi sono formule nel
modo seguente: se un nodo non foglia è etichettato con γσ e i suoi predecessori sono
π 1 πn
è una regola di C.
etichettati con π1σ…πnσ allora
γ
Esempio
Dato il seguente insieme di clausole M = {{a(X,0,X)}, {~a(X,Y,Z), a(X,s(Y),s(Z))} } e la
seguente clausola χ = {~ a(b,s(0),s(b))} = ~ϕ con ϕ = a(b,s(0),s(b)), si applica la procedura
di risoluzione partendo dall’insieme di assiomi M ∪ {χ}. Si può costruire così il seguente
albero di derivazione.
21
{~a(X1,Y1,Z1), a(X1,s(Y1),s(Z1))}
{~ a(b,s(0),s(b))}
µ1 ={X1/b, Y1/0,Z1/b}
{~a(b,0,b)}
{a(X2,0,X2)}
µ2 ={X2/b}
Correttezza della Risoluzione come Calcolo per la Deduzione
A questo riguardo vale il seguente teorema.
Teorema Correttezza di RESext
Sia M un insieme di clausole. Se una clausola C può essere derivata da M in RESext, allora
C è conseguenza logica di M. In particolare, quindi, se C può essere derivata da M in RES
o in RESprop, allora C segue logicamente da M.
Dim.
Dobbiamo mostrare che l’applicazione della regola RESext conserva la validità in
un’algebra A. Assumiamo così che A sia un modello delle clausole{L1,…, Lm}∪ C1 e
{Lm+1,…, Lm+n}∪ C2. Allora A è pure modello di ({L1,…, Lm}∪ C1) σ e ({Lm +1,…,
Lm+n}∪ C2) σ per una σ arbitraria (vedi Corollario a pag. 10). Sia allora σ un unificatore
in RESext tale che {L1,…, Lm}σ = {L} e {Lm +1,…, Lm+n}σ = {~L}. Allora A è un modello
di {L}∪ C1σ e {~L}∪ C2σ. Per mostrare che A è modello di C1σ ∪ C2σ, ritorniamo alle
formule corrispondenti alle clausole {L}∪ C1σ e {~L}∪ C2σ. Siano C1σ = {A1,…,Ak}e
C2σ = {B1,…,Bj}. La tavola seguente mostra le formule corrispondenti alle clausole {L}∪
C1σ e {~L}∪ C2σ, come implicazioni universalmente quantificate. In ogni caso si vede
che l’applicazione di RESext conserva la validità in un’algebra, giacchè le seguenti formule
sono valide:
((ϕ →ψ)→ ((ψ→χ)→ (ϕ →χ)))
conclusione transitiva (CT)
(ϕ → ((ϕ →ψ)→ ψ)
modus ponens (MP)
(ϕ → (~ϕ → false))
ex falso quodlibet (EFQ), false è una qualsiasi
formula senza un modello
{ L, A1,…,Ak }
Per k>0, j>0
L∨ A1∨…∨ Ak
~( A1∨…∨ Ak) → L
Per k>0, j = 0
L∨ A1∨…∨ Ak
~L → (A1∨…∨ Ak)
Per k = 0, j>0
L
Per k = 0, j = 0
L
TAVOLA
{~L,B1,…,Bj}
~L ∨ B1∨…∨ Bj
L → ( B1∨…∨ Bj)
~L
~L∨ B1∨…∨ Bj
L → ( B1∨…∨ Bj)
~L
22
{A1,…,Ak}∪{B1,…,Bj}
Grazie a CT
A1∨…∨ Ak∨ B1∨…∨ Bj
~(A1∨…∨ Ak) → (B1∨…∨ Bj)
Grazie a MP
A1∨…∨ Ak
Grazie a MP
B1∨…∨ Bj
Grazie a EFQ
Volgiamo ora la nostra attenzione alla prova della completezza del calcolo RES.
Completezza della Risoluzione come Calcolo per la Refutazione
Tale prova viene data in due tappe, la prima riguarda il caso delle clausole ground, la
seconda è l’estensione di questo risultato a clausole qualsiasi, grazie al lifting lemma.
Teorema Completezza per Clausole Ground
Sia M un insieme finito di clausole ground. Se M non possiede alcun modello allora la
clausola vuota può essere derivata da M in RES prop.
Dim.
Si può assumere che non sia in M. Si prova allora la seguente proposizione: per ogni
insieme di clausole ground {C1,…,Cn} senza un modello, la clausola vuota può essere
derivata in RESprop da tale insieme. Tale prova viene data per induzione sul numero
k(C1,…,Cn) = (numero di letterali in C1+…+ numero di letterali in Cn) − n.
Base
Sia k(C1,…,Cn) = 0. Ciò significa che ciascun Ci contiene esattamente un letterale Li, per i
= 1,…,n.
Se l’insieme {C1,…,Cn} non possiede un modello allora la formula L1∧…∧Ln non possiede
un modello, allora per il teorema su Validità e Soddisfacibilità di Congiunzioni e
Disgiunzioni di Letterali Ground (vedi pag. 14), {L1,…,Ln}contiene almeno una coppia di
letterali complementari, Li = ~Lj; allora, banalmente, può essere derivata da Ci = {~Lj} e
Cj = {Lj} con una sola applicazione di RES prop.
Induzione
Sia k(C1,…,Cn) > 0. Allora esiste almeno una clausola con almeno due letterali. Tale
clausola sia C1. Sia L un letterale in C1 e sia E = C1 − {L}. Evidentemente k({L},…,Cn) <
k(C1,…,Cn) e pure k(E,…,Cn)< k(C1,…,Cn). Inoltre né {{L},…,Cn} né {E,…,Cn}
possiedono un modello, giacchè questo sarebbe modello anche di{C1,…,Cn}, infatti C1 è
una disgiunzione di letterali.
Segue allora, per ipotesi induttiva, che la clausola vuota può essere derivata in RESprop da
{{L},…,Cn} e da{E,…,Cn}. Consideriamo un albero di derivazione di da {E,…,Cn}.
Questo è un albero binario, con clausole ground come nodi, la cui radice è e le cui foglie
sono calusole in {E,…,Cn}.
Le occorrenze di E nelle foglie ci disturbano, inseriamo allora nuovamente L dove era stato
cancellato così quelle foglie diventano nuovamente C1. Facciamo ora la seguente
osservazione. Se C” è derivato da C e C’ con una applicazione di RESprop, allora o C” ∪
{L}o C” può essere derivato da C ∪ {L} e C’ con una applicazione di RESprop, a seconda
che L non sia o sia anche in C’. Possiamo allora inserire L nell’albero di derivazione di
da {E,…,Cn} dalle foglie alla radice. Si ottiene così un albero di derivazione le cui foglie
sono clausole in{C1,…,Cn}; la sua radice è allora o o {L}. Nel primo caso si ha ciò che
si voleva. Nel secondo, si osserva che la deducibilità in RESprop di {L} da {C1,…,Cn} e di
da {{L},…,Cn}, ipotesi induttiva, implicano la derivabilità di da {C 1,…,Cn}.
Per generalizzare tale risultato al caso di clausole qualsiasi abbiamo bisogno del seguente
lemma del lifting.
Lemma Lifting (o del Trasporto)
Siano C1 e C2 clausole senza variabili in comune. Siano σ1 e σ2 sostituzioni tali che C1σ1 e
C2σ2 non spartiscano variabili. Infine, sia C ottenuta da C1σ1 e C2σ2 con una sola
23
applicazione di RESext via σ. Allora esistono una clausola C’ e sostituzioni µ e τ tali che C’
è ottenuto da C1 e C2 con una sola applicazione di RES via µ e C’τ = C. Ossia
C1
↓σ1
C1σ1
C2
C 1 RES C2
↓σ2
↓σ1 µ
↓σ2
C 2σ2 → C 1σ1 C’
C2σ2

RESext
trasportato in
σ
τ
↓
C
C
Dim.
Siano C1 = {L1,…, Lm}∪ R1 e C2 = {Lm+1,…, Lm+n}∪ R2 con m,n > 0, clausole tali che σ è
un unificatore di {L1σ1,…, Lmσ1, ~Lm+1σ2,…, ~Lm+nσ2} e C = R1σ1σ ∪ R2σ2σ. Si può
assumere che σ1 contenga solo coppie X/t con X in C1 e analogamente per σ2 e C2. Poiché
C1σ1 e C2σ2 non hanno variabili in comune, possiamo notare che (σ1∪σ2)σ è un
unificatore di {L1,…,Lm, ~Lm+1,…, ~Lm+n}. Sia µ un mgu di {L1,…,Lm, ~Lm+1,…, ~Lm+n}
e τ una sostituzione tale che (σ1∪σ2)σ = µτ.
Allora la clausola C’= R1µ ∪ R2µ è un risolvente di C1 e C2 via µ. Finalmente, possiamo
notare le seguenti uguaglianze:
C’τ =
(R1µ ∪ R2µ)τ =
R1µτ ∪ R2µτ =
R1(σ1∪σ2)σ ∪ R2(σ1∪σ2)σ =
R1σ1σ ∪ R2σ2σ = C.
24
Esempio
C1={p(X,Y), p(f(a),Z)}

σ1 = {X/f(a),Y/g(b),Z/g(b)}
↓
C1σ1={p(f(a),g(b))}
C 2 ={~p(f(X1),g(Y1)), ~q(X1, Y1)}

σ2 = {X1/a,Y1/b}
↓
C 2σ2 = {~p(f(a),g(b)), ~q(a, b)}
RES ext
σ = {}
C = {~q(a, b)}
è trasportato in
C1={p(X,Y), p(f(a),Z)}
C 2 ={~p(f(X1),g(Y1)), ~q(X1, Y1)}


RES
σ1
µ = {X/f(a),Y/g(Y1),Z/g(Y1),X1/a}
σ2
↓
↓
C 1σ1
C 2σ2
C’ = {~q(a, Y1)}

τ = {Y1/b}
↓
C = {~q(a, b)}
Possiamo ora dare il seguente conclusivo teorema.
Teorema Completezza della Risoluzione come Calcolo per la Refutazione
Se M è un insieme di clausole senza un modello, allora la clausola vuota è derivabile in
RES da variante(M).
Dim.
Per il teorema di Herbrand e quello di compattezza (vedi [1]), esiste un sottinsieme finito
di ground-instances(M) senza un modello. Per la completezza della risoluzione per
clausole ground si ha la derivazione della clausola vuota da tale sottinsieme finito di
ground-instances(M).
Ora possiamo trasportare, lavorando dalle foglie alla radice, un albero di derivazione della
clausola vuota da ground-instances(M) in accordo con il lemma del lifting. La radice
dell’albero così ottenuto è una clausola che è trasformata nella clausola vuota quando le è
applicata una opportuna sostituzione τ, ma questo è possibile solo se la radice è già la
clausola vuota.
Siamo ora interessati a trovare opportune semplificazioni e/o strategie di esecuzione della
Procedura (Ingenua) di Dimostrazione per Refutazione via RES, che la rendano
concretamente applicabile.
25
Strategie di Esecuzione della Dimostrazione per Refutazione via RES
L’intento è quello di ridurre il numero delle clausole parent che possono dar luogo a passi
di risoluzione al momento in cui si deve calcolare l’insieme dei risolventi del corrente
insieme di clausole. Alcune immediate semplificazioni sono:
• eliminazione delle tautologie (ad es. A∨~A)
• eliminazione di clausole già “comprese” in altre come la clausola p(f(W)) ∨
~q(f(a))∨ r(Z) che è compresa in p(X) ∨ ~q(f(Y)), ogni modello della seconda è
infatti anche modello della prima (ovvero se la seconda è soddisfacibile lo è anche
la prima).
Si capisce comunque, che questi tipi di semplificazioni devono essere affiancati da
strategie più generali: scegliendo infatti, secondo certi criteri, le clausole parent per ogni
passo di risoluzione, si ottengono buoni risultati.
La rappresentazione delle possibili dimostrazioni della insoddisfacibilità di un dato
insieme di clausole C, via RES, può essere data tramite un grafo, detto grafo di risoluzione,
che è una estensione del concetto già introdotto di albero di dimostrazione (via RES). Le
clausole di C sono nodi del grafo dai quali possono uscire archi. Un risolvente è un nodo
nel quale entrano due archi, provenienti dalla coppia di clausole parent.
Quindi una strategia deve limitare la dimensione di tale grafo, ponendo appunto dei vincoli
sulle modalità di generazione dei nodi intermedi.
Presentiamo qui di seguito alcune di queste strategie, per concludere con quella, detta
linear-input, che viene in pratica utilizzata nella Programmazione Logica ossia relizzata
nel Calcolo della Risoluzione SLD. L’insieme di clausole iniziale è l’ insieme C0 =
MC∪(~ϕ)C, dove ΨC è l’insieme della clausole corrispondenti all’insieme o formula Ψ, in
generale Ci sarà l’insieme di risolventi opportunamente calcolati basandosi sui Cj, j ≤ i.
1. Strategia in ampiezza (breadth-first): dato Ci con i ≥ 0, genera tutti i risolventi in Ci+1
utilizzando come parent una clausola in Ci ed una in Cj con j ≤ i. Il risparmio sta nel
non generare risolventi con entrambe le clausole a livello j < i, mantenendo la
completezza. Ad esempio, sia C0 = {1:{~a,c,d}, 2:{a,d,e}, 3:{~a,~c}, 4:{~d}, 5:{~e}},
dove le clausole 4 e 5 corrispondono alla negazione della formula ϕ = d∨e e le clausole
1-3 sono dati assiomi, applicando la procedura di risoluzione con la strategia in
ampiezza, otteniamo l’insieme dei risolventi di C0, C1={6:{c,d,e}, 7:{d,e,~c}, 8:{~a,c},
9:{a,e}, 10:{a,d},11:{~a,d}} con il risolvente 6 ottenuto da 1 e 2, 7 da 2 e 3, 8 da 1 e 4,
9 da 2 e 4, 10 da 2 e 5 e 11 da 1 e 3; continuando con il calcolo di C2 i risolventi in C1
non vengono più calcolati in quanto le loro clausole parent sono tutte al livello 0,ossia
in C0, staranno in C2 tutti i risolventi fra clausole entrambe in C1 o in C1 e C0. Si otterrà
allora, fra gli altri, il risolvente 12:{d} dalle 10 e 11 e, al passo successivo nel calcolo
di C3, verrà derivato il risolvente dalle 4 e 12.
2. Set of support: questa è una variante della strategia in ampiezza; essa impone che
almeno una delle clausole parent sia fra quelle derivate dalla negazione della formula ϕ
che si deve provare. Non è significativo infatti, assunta la consistenza della teoria
costituita dall’insieme degli assiomi, produrre risolventi fra clausole entrambe nella
teoria. La proprietà di completezza vale ancora.
3. Lineare: invece di portare avanti il calcolo di tutti i possibili alberi di derivazione,
come nella strategia set of support, si costruisce un albero alla volta. Allora una
clausola è sempre l’ultimo risolvente ottenuto o, inizialmente, in (~ϕ)C, l’altra è in MC
o tra i risolventi ottenuti in precedenza. Si ottiene così un albero come
26
g0
b0
g1
b1
:
:
gi
bi
:
:
C
dove g 0∈ (~ϕ) e bi∈ MC ∪{gj | j < i}.
Ad esempio, sia MC = {{~p,q}, {~q,p}, {p,q}} e (~ϕ)C = {{~p, ~q}}, un albero
di refutazione di MC ∪(~ϕ)C è il seguente
{~p,~q}
{~p,q}
{~p}
{p,q}
{q}
{p,~q}
{p}
{~p}
È evidente che occorre tenere in memoria la lista dei risolventi via via ottenuti, nell’ulti mo passo di risoluzione dell’esempio è stato infatti utili zzato un risolvente, {~p}, precemente ottenuto. Tale strategia risulta ancora completa. Al fine di ridurre l’occupazione
di memoria si può introdurre la seguente strategia (incompleta per le clausole generali).
4. Linear-Input: una clausola è sempre l’ultimo risolvente ottenuto o, inizialmente,
in (~ϕ)C, l’altra è sempre in MC. Il vantaggio è ovvio, occorre mantenere solo l’ultimo
risolvente ottenuto, è però incompleta per le clausole generali. Ad esempio, con
la teoria vista sopra si ha un albero come
{~p,~q} {~p,q}
{~p}
{p,q}
{ q}
{p,~q}
{p}
{~p,q}
{ q}
:
∞
Se invece consideriamo il seguente insieme MC = {{~uomo(X),mortale(X)},
{uomo(soc)}} e (~ϕ)C = {{~mortale(Y)}}, per cui le clausole non sono generali
ma sono, come si vedrà, clausole di Horn, tale strategia risulta ancora completa.
Ad esempio
{~mortale(Y)} {~uomo(X1),mortale(X1)}
µ1={Y/X1}
{~uomo(X1} {uomo(soc)}
µ2={X1/soc}
27
La Programmazione Logica
Il principio di risoluzione è stato illustrato nella sua applicazione alle clausole generali,
sulla cui struttura cioè nessuna restrizione è posta. È però nella sua applicazione alle
clausole di Horn che trova il suo più noto e vantaggioso impiego. La strategia seguita è
quella linear-input.
Le Clausole di Horn
La logica a clausole di Horn è un sottinsieme della logica a clausole generali.
Definizione Clausola di Horn
Una clausola di Horn è o una clausola definita o una clausola goal o la clausola vuota.
Una clausola definita è una clausola che contiene esattamente un letterale positivo e m ≥ 0
letterali negativi, quindi del tipo { A, ~B1,…, ~Bm }ossia A∨ ~B1∨…∨ ~Bm che è
l’implicazione (B1∧…∧Bm)→ A che conveniamo di scrivere come A ← B1,…,Bm. Quando
m > 0, tale clausola è detta regola di cui il letterale A è detto testa e la congiunzione di
letterali B1,…,Bm è detta corpo. Quando, invece, m = 0 tale clausola si riduce alla sola
testa ed è scritta come A←. In questo caso essa prende il nome di clausola unitaria o
fatto.
Una clausola goal contiene solo letterali negativi; in accordo a quanto sopra stabilito, è
rappresentata da ← B1,…,Bm. La clausola vuota, infine, che non contiene alcun letterale è
rappresentata da ← o .
Si ricorda che la clausola goal ← B1,…,Bm equivale la formula ~∃( B1∧…∧Bm) che è la
negazione della domanda ∃(B1∧…∧Bm), che si vuol quindi provare, per refutazione, essere
(o meno) conseguenza logica di un dato insieme di assiomi.
La Risoluzione SLD
In programmazione logica un programma è un insieme di clausole definite, gli assiomi di
una teoria, nell’ambito della quale si vogliono effettuare domande circa la verità
dell’essere date formule del tipo ∃( B1∧…∧Bm), teoremi (della data teoria) nel calcolo
della Risoluzione SLD, ovvero, stante la correttezza del calcolo stesso, dell’essere
conseguenza logica del dato programma.
Ciò che interessa è anche poter ottenere delle sostituzioni di risposta θ, per le variabili
presenti in ∃ ( B1∧…∧Bm), che rendano appunto testimonianza della esistenza di opportune
loro istanze tali che la formula ∀( B1∧…∧Bm)θ sia conseguenza logica del programma.
Così, dato il programma P:
add(X, 0, X) ←
add(X, s(Y), s(Z)) ← add(X, Y, Z)
possibili goal sono
1) ← add(0, s(0), W) che corrisponde a voler provare ∃Wadd(0,s(0),W) o
2) ← add(s(s(0)), s(0), Y),add(s(0), Y, Z)
che corrisponde
a voler provare
∃Y∃Z (add s(s(0)), s(0), Y)∧add(s(0), Y, Z)).
La strategia seguita nel portare avanti i vari passi di risoluzione è nota come Risoluzione
SLD, cioè Risoluzione Lineare per Clausole Definite con funzione di Selezione. Essa è
completa per le clausole di Horn.
Anche la Risoluzione SLD opera per contraddizione e considera quindi la negazione della
formula che si vuol provare, ottenendo una clausola goal. Dato allora un programma logico
P, insieme finito di clausole definite, ed una clausola goal G0, la risoluzione SLD cerca di
derivare la clausola vuota da P ∪{ G0}.
28
Ad ogni passo di risoluzione si ricava un risolvente (ancora una clausola goal) Gi+1, se
esiste, dalle clausole parent Gi, l’ultimo risolvente ottenuto, e da una variante Ci di una
clausola di P. Per fare ciò, in un passo di risoluzione occorre selezionare un atomo Am, che
è negato in Gi, e unificarlo, se possibile, con la testa (unico atomo positivo) A della
clausola Ci tramite la mgu θi. Allora, dato P se
Gi = ←A1,…,Am,…,Ak è il goal corrente e Am è l’atomo selezionato,
Ci = A← B1,…,Bq è la variante di una clausola in P,
Amθi = Aθi e θi è mgu, si può derivare il nuovo risolvente (ancora un goal)
Gi+1 = (←A1,…,B1,…,Bq,…,Ak) θi.
La strategia seguita è la linear-input, in quanto una delle clausole parent è l’ultimo
risolvente e l’altra è sempre la variante di una clausola in P.
Esempio
Dati P =
P1: add(X, 0, X) ←
P 2: add(X, s(Y), s(Z)) ← add(X, Y, Z) e
G0 = ← add(s(s(0)), s(0), W) si può costruire il seguente albero di refutazione
G0
C 0 = variante(P 2)= add(X1, s(Y1), s(Z1)) ← add(X1, Y1, Z1)
θ0={X1/s(s(0)),Y1/0,W/s(Z1)}
G 1=← add(s(s(0)),0,Z1) C 1 = variante(P 1) = add(X2, 0, X2)
θ1={X2/s(s(0)),Z1/s(s(0))}
Definizione Derivazione e Refutazione SLD
Una derivazione SLD per G0 dal programma logico P è una terna di sequenze di clausole
goal G0,…,Gn,di varianti di clausole di P C0,…,Cn-1 e di sostituzioni mgu θ0,…,θn-1, tale
che Gi+1 è ottenuto con un passo di risoluzione da Gi e Ci via θi. Una tale derivazione può
essere di tre tipi:
• di successo se per n finito Gn = ;
• di fallimento finito se per n finito da Gn non è possibile derivare alcun risolvente
e Gn non è ;
• infinita se ad ogni passo è sempre possibile derivare risolventi diversi da .
Una refutazione SLD di P ∪{G0} è una derivazione SLD di successo.
Esempio
Dato il programma P del precedente esempio e
a) il goal G 0 del precedente esempio, la terna (<G 0, G 1, >, <C 0, C1 >, <θ 0, θ 1>) è una
refutazione SLD;
b)il goal G 0 = ← add(0, s(0), 0), la terna (<G 0>, <>, <>) è una derivazione di fallimento
finito, in quanto l’unico atomo di G 0 non unifica con alcuna testa di variante di clausola in
P;
d)il goal G0= ← add(X0, Y0, Z0), si ha la derivazione infinita (<G 0, G 1,…>, <C 0, C1,… >,
<θ 0, θ1,…>), con Gi=←add(Xi,Yi,Zi),Ci = variante(P2), θi= {Xi/Xi+1,Yi/s(Yi+1),Zi/s(Zi+1)}.
29
Definizione Insieme di Successo
Dato il programma logico P, il suo insieme di successo, Is(P), è l’insieme di tutti gli atomi
ground A tali che P ∪{←A}possiede una refutazione SLD.
Abbiamo già notato che ciò che ci interessa è conoscere le istanze delle variabili in G0
determinate dalle sostituzioni via via applicate nei passi di risoluzione di una refutazione
SLD. A questo riguardo possiamo notare che dato il goal ← a1(t1,1,…,t1,k1),…,
an(tn,1,…,tn,kn), i termini ti,j ground rappresentano i parametri d’ingresso, e sono detti di tipo
+, mentre i termini variabili, destinati a essere istanziati sono parametri d’uscita e sono
detti di tipo -; i termini contenenti variabili, ma non costituiti da una sola variabile, sono
allora sia d’ingresso che d’uscita.
Definizione Risposta Calcolata
Dato il programma P ed il goal G0, una risposta per P∪{G0}è una sostituzione per le
variabili di G0; se consideriamo una refutazione SLD per P∪{G0}tale appunto che esista
un n finito tale che Gn = , una risposta calcolata per P∪{G0}è la sostituzione ottenuta
restringendo la composizione delle sostituzioni mgu θ0,…,θn-1, trovate nella refutazione,
alle variabili di G0.
Allora una risposta calcolata che sia anche corretta, è il “testimone” della dimostrazione
costruttiva di una formula quantificata esistenzialmente.
Definizione Risposta Corretta
Una risposta corretta per P∪{G0}è una sostituzione θ tale che se G0 = ← A1,…,Ak allora
la formula ϕ = ∀(A1∧…∧Ak)θ è conseguenza logica di P, ovvero tale che P∪{G0θ} è
insoddisfacibile.
Si può dimostrare che la risoluzione SLD è corretta mostrando che ogni risposta calcolata
è pure una risposta corretta; ancora si mostra che la risoluzione SLD è anche completa,
cioè se σ è una risposta corretta per P∪{G0} allora esiste una risposta calcolata θ per
P∪{G0}ed una sostituzione λ tali che σ = θλ|var(G0).
Esempio
Sia P =
P1: add(X, 0, X) ←
P 2: add(X, s(Y), s(Z)) ← add(X, Y, Z) e
G0 = ← add(W, 0, W).
P∪{G0}ha una risposta calcolata θ = {W/X1}, una risposta corretta per P∪{G0}è, d’altra
parte, σ = {W/s(0)} ed infatti esiste la sostituzione λ = {X1/s(0)}tale che σ = θλ|var(G0).
Osserviamo che nell’applicare la regola di risoluzione SLD si hanno due tipi di non
determinismo:
a. scelta (o selezione) dell’atomo Am nel goal corrente; tale non determinismo viene
risolto adottando una opportuna regola di calcolo ( o funzione di selezione);
b. scelta della clausola di P da utilizzare in un passo di risoluzione; tale non
determinismo verrà risolto definendo (ed adottando) una strategia di ricerca
nell’albero SLD di risoluzione (vedi avanti pag. 32).
30
Regola di Calcolo
Una regola di calcolo è una funzione dal dominio costituito dall’insieme dei goal in una
data segnatura nel dominio degli atomi, tale che dato un goal ←A1,…,Am,…,Ak un suo
atomo Am viene selezionato. Si potrebbe far dipendere questa scelta anche dalla lunghezza
della derivazione o da altre misure sulla derivazione corrente. Allora dati un programma
logico P, un goal G0 e una regola di calcolo R, una derivazione SLD da P∪{G0} via R è
una derivazione SLD da P∪{G0}nella quale ad ogni passo di risoluzione si usa R per
determinare l’atomo selezionato.
Esempio
Dato il programma P =
P1: add(X, 0, X) ←
P 2: add(X, s(Y), s(Z)) ← add(X, Y, Z) ed il goal
G0 = ← add(0, s(0), s(0)) , add(s(0), 0, s(0)), con la regola che seleziona l’atomo più a
sinistra si ha la seguente derivazione
G0 = ← add(0, s(0), s(0)) , add(s(0), 0, s(0))
C0 = variante(P2), | θ0 = {X1/0,Y1/0,Z1/0}
|
G1 =← add(0, 0, 0), add(s(0), 0, s(0))
C1 = variante(P1), | θ1 = {X2/0}
|
G 2 = ← add(s(0), 0, s(0))
C2 = variante(P1), | θ2 = {X3/s(0)}
|
G3 = ←
Mentre utilizzando la regola che seleziona l’atomo più a destra si ha la seguente
derivazione
G0 = ← add(0, s(0), s(0)) , add(s(0), 0, s(0))
C 0 = variante(P1), | θ0 = {X1/s(0)}
|
G 1 = ← add(0, s(0), s(0))
C1 = variante(P2), | θ1 = {X2/0,Y2/0,Z2/0}
|
G 2 = ← add(0, 0, 0)
C2 = variante(P 1), | θ2 = {X3/0}
|
G3 = ←
In generale si ha che la regola di calcolo influenza solo l’efficienza della computazione e
non la sua correttezza o completezza. Infatti una prima proprietà che suffraga tale
affermazione è data dal seguente teorema, altre proprietà verranno riportate in segui to.
Teorema Indipendenza dalla Regola di Calcolo
Dato un programma logico P, il suo insieme di successo non dipende dalla regola di
calcolo usata nei passi di risoluzione SLD.
31
Al fine di illustrare una computazione relativa alla ricerca di una refutazione per un dato
insieme P∪{G0}, quando si adotti una data regola di calcolo, si introduce il concetto di
albero SLD.
Alberi SLD
Una volta definita una regola di calcolo, resta un ulteriore grado di non determinismo,
possono infatti esistere più clausole la cui testa sia unificabile con l’atomo selezionato.
Dato infatti il goal ← add(0, X, Y) possono essere scelte sia la prima che la seconda
clausola del programma visto negli esempi sopra riportati per calcolare due risolventi.
Ciò è consistente con il fatto che possono esistere, ovviamente, più risposte (calcolate e
corrette) per un goal. Un dimostratore completo deve quindi generare tutte le possibili
soluzioni.
Una rappresentazione grafica per questo tipo di non determinismo della risoluzione SLD è
costituita dagli alberi SLD. In questi ogni percorso corrisponde ad una possibile
derivazione SLD.
Definizione Albero SLD
Dato un programma logico P, un goal G0 e una regola di calcolo R, un albero SLD per
P∪{G0} via R è così definito:
• ciascun nodo dell’albero è un goal (eventualmente vuoto);
• la sua radice è il goal G0;
• dato il nodo ←A1,…,Am,…,Ak, se Am è l’atomo selezionato da R allora questo
nodo ha un nodo figlio per ciascuna (variante di) clausola C = A← B1,…,Bq di P
tale che A e Am unificano tramite una mgu θ. Ciascun nodo figlio è etichettato con
il nuovo risolvente (clausola goal) (←A1,…,B1,…,Bq,…,Ak)θ ed il ramo dal padre
al figlio è eventualmente etichettato con C e θ;
• il nodo etichettato con la clausola vuota, ← o , non ha figli.
Allora in un albero SLD via R ogni percorso dalla radice ad una foglia etichettata con ←
rappresenta una refutazione SLD via R. La sostituzione di risposta per tale derivazione è
ottenuta componendo le sostituzioni mgu determinate lungo il percorso e restringendo la
sostituzione ottenuta alle variabili di G0.
La regola di calcolo influenza la struttura dell’albero (in ampiezza e profondità), comunque
il numero dei cammini di successo in tutti gli alberi SLD (rispetto ad ogni regola R) per P
∪{G0}è il medesimo ed R influenza solo il numero dei cammini di fallimento finito,
infinito o finito che sia tale numero. Infatti, oltre al teorema sulla Indipendenza della
Regola di Calcolo sopra riportato,vale il seguente teorema.
Teorema Alberi SLD e Regola di Calcolo
Dato un programma logico P ed un goal G0, allora o ogni albero SLD per
P ∪{G0} (ciascuno relativo ad una regola di calcolo R) ha infiniti percorsi di successo o
ogni albero ha il medesimo numero finito di percorsi di successo con associate sostituzioni
di risposta calcolate varianti di quelle trovate in ogni altro albero.
Esempio
Dato il programma P =
P1: add(X, 0, X) ←
P 2: add(X, s(Y), s(Z)) ← add(X, Y, Z)
G0 = ← add(0, W, 0) , add(0, W, K),
ed il goal
32
con la regola che seleziona l ’atomo più a sinistra si ha il seguente albero SLD
← add(0, W, 0) , add(0, W, K)
var.te(P1), | {W/0,X1/0}= θ0
|
← add(0, 0, K)
var.te(P1), | {K/0,X2/0}= θ1
|
←
con θ0θ1| {W,K} ={W/0, K/0};
con la regola che seleziona l’atomo più a destra si ha il seguente albero SLD
← add(0, W, 0), add(0, W, K)
var.te(P 2), {X3/0,W/s(Y3),K/s(Z3)}= θ2
var.te(P1),{X1/0,K/0,W/0}=θ1
← add(0, 0, 0)
var.te(P1), |{X2/0}=θ11
|
←
← add(0, s(Y3), 0), add(0, Y3, Z3)
var.te(P1),{X4/0,Y3/0,Z3/0}=θ21
←add(0,s(0),0)
{fail}
var.te(P2), {X5/0,Y3/s(Y5),Z3/s(Z5)}=θ22
← add(0, s(s(Y5)), 0), add(0, Y5, Z5)
{fail}
∞
con θ1θ11| {W,K} ={W/0, K/0}.
Rivolgiamo ora la nostra attenzione al secondo e rimanente grado di non determinismo,
quello relativo alla scelta delle clausole parent appartenenti al programma P, ovvero
vogliamo indagare le possibili strategie di costruzione/visita dell’albero SLD alla ricerca
dei percorsi di successo.
Strategie di Ricerca nell’Albero SLD
La effettiva realizzazione di un dimostratore basato sulla risoluzione SLD richiede oltre
alla definizione di una regola di selezione anche la definizione di una strategia di ricerca
che stabilisca una particolare modalità di esplorazione ovvero costruzione dell’albero SLD,
alla ricerca dei percorsi di successo.
In generale, dato un albero una strategia di ricerca, a partire dalla radice, visita i nodi
dell’albero secondo un particolare ordine, cercando di costruire un percorso dalla radice ad
un nodo N che sia soluzione del problema considerato, nel nostro caso un nodo etichettato
con ←.
Ciascuna strategia costruisce opportunamente una lista di nodi da visitare.
Al passo iniziale tale lista contiene la sola radice. Ad un generico passo se L è la lista
corrente dei nodi da visitare, viene estratto (visitato) il primo nodo N dalla lista L, vengono
determinati i suoi figli e quindi inseriti in L come nuovi nodi da visitare.
33
Il procedimento può terminare o quando L contiene un nodo soluzione, o, definitivamente,
quando L è vuota. Le diverse modalità di inserimento definiscono di fatto diverse strategie.
Si consideri, a titolo di esempio, il seguente albero con l’intento di visitarlo.
N1
N2
N3
|
N9
N5
N4
N6
N7
|
N8
Ricerca in Profondità (Depth-First)
Con questa strategia sono visitati per primi i nodi a maggiore profondità e selezionati
arbitrariamente quelli ad uguale profondità: in pratica si realizza tale strategia inserendo i
nodi generati ad ogni passo in testa alla lista L.
La modalità di visita che questa strategia comporta quando non esistono altri nodi lungo il
cammino considerato, è la seguente: è possibile innescare un meccanismo, chiamato
backtracking, che consente di esplorare altri rami a partire dal nodo antenato più vicino dal
quale parte un cammino alternativo a quello già esplorato. Si ottiene così una ricerca in
profondità con backtracking.
Nel caso dell’albero sopra visto avremo così:
L0 = [N1]
nodo visitato N1
L1 = [N2, N5]
„
„
N2
L2 = [N3, N4, N5] „
„
N3
L3 = [N9, N4, N5] „
„
N9, non sia soluzione o comunque se ne cercano
altre, allora con backtracking, antenato N2
L4 = [N4, N5]
„
L5 = [N5]
L6 = [N6, N7]
“
„
„
N4,non sia soluzione o comunque se ne cercano
altre, allora con backtracking, antenato N1
“
„
N5
N6, non sia soluzione o comunque se ne cercano
altre, allora con backtracking, antenato N5
L7 = [N7]
L8 = [N8]
L9 = [ ]
„
„
„
„
N7
N8
Nel caso di alberi SLD, attivare il backtacking implica che tutti i legami per le variabili
determinati dal punto di backtracking in poi, nel percorso dall’antenato più vicino fino al
nodo terminale, vengano rilasciati. Ad esempio, dato il seguente programma P
a1: p(s(s(X)), s(Y)) ← q(Y)
a2: p(s(0),Z) ←
a3: q(s(0))←
ed il goal ← p(s(W), s(0)), avremo la seguente visita, dove le sostituzioni sono ristrette alla
variabile W del goal:
← p(s(W), s(0))
v.te(a1),{W/s(X1)}
back.ng
v.te(a2), {W/0}
q(0)
34
Ricerca in Ampiezza (Breadth-First)
Questa strategia impone di visitare per primi i nodi a minore profondità, selezionando
arbitrariamente quelli ad uguale profondità. Questo stile di visita viene ottenuto inserendo i
nodi generati ad ogni passo in coda alla lista L. Riconsiderando l’albero di sopra si ha
allora
L0 = [N1]
L1 = [N2, N5]
L2 = [N5, N3, N4]
L3 = [N3, N4, N6, N7]
L4 = [N4, N6, N7, N9]
L5 = [N6, N7, N9]
L6 = [N7, N9]
L7 = [N9, N8]
L8 = [N8]
L9 = [ ].
nodo visitato N1
„
„
N2
„
„
N5
„
„
N3
„
„
N4
„
„
N6
„
„
N7
„
„
N9
„
„
N8
Nelle correnti implementazioni del linguaggio Prolog si ha la costruzione dell’albero SLD
con la strategia in profondità con backtracking, essa viene infatti efficientemente realizzata
attraverso l’uso di un unico stack di goal (vedi avanti). Tale stack rappresenta il cammino
che si sta esplorando; in tale stack sono pure presenti riferimenti ai percorsi alternativi da
esplorare in caso di fallimento o richiesta di soluzioni alternative. L’ordine dei nodi fratelli
è quello testuale delle clausole che li hanno generati. Tale strategia non è completa, perché
non è sempre in grado di trovare ogni cammino di successo nell’albero SLD, ovviamente
quando dei percorsi infiniti siano alla sinistra di percorsi di successo, come illustrato
nell’esempio seguente.
Esempio
Sia P il seguente programma
c1: par(X,Z) ← par(X, Y), par(Y, Z)
c2: par(a, b) ←
c3: par(b, c) ←
con il goal ← par(a, c) si ha il seguente albero SLD con regola che seleziona l’atomo più a
sinistra:
← par(a, c)
c1
← par(a, Y1), par(Y1, c)
c1
c2
← par(a, Y2),par(Y2, Y1), par(Y1,c)
← par(b, c)
∞
Come risulta evidente, il nodo non verrà mai visitato, potendo sempre trovare un nuovo
risolvente sul percorso infinito di sinistra.
35
Semantica della Programmazione Logica
Si possono considerare alcune semantiche per la programmazione logica grazie alle quali
parte dei concetti di correttezza e completezza della risoluzione SLD possono essere
immediatamente formulati.
La Semantica Operazionale
In programmazione logica una computazione consiste nella derivazione di una formula da
un dato programma: la semantica operazionale è allora stabilita dalla relazione di
derivabilità espressa dalla regola della risoluzione SLD.
Dato un programma logico P, la denotazione operazionale di P, Op(P), è l’insieme di tutti
gli atomi ground, p(t1,…,tn), dove i ti sono costruiti con gli operatori della segnatura
(implicitamente) definita da P, tali che esiste una refutazione SLD per P∪{← p(t1,…,tn)},
ossia tali che p(t 1,…,tn) ∈Is(P).
Esempio
Dato il programma P:
collega(a, b) ←
collega(c, b) ←
collega(X, Z)←collega(X, Y), collega(Y,Z)
collega(X, Y)←collega(Y, X)
La sua denotazione operazionale è data da Op(P) = Is(P) =
{collega(a, b), collega(c, b), collega(b, a), collega(b, c), collega(a, c), collega(c, a),
collega(a, a), collega(b, b), collega(c, c)}.
La Semantica a Modelli
Tale semantica è stabilita dalla relazione di conseguenza logica. Dato P, la denotazione
“a modelli” di P, Decl(P), è l’insieme di tutti gli atomi ground p(t1,…,tn) tali che
p(t1,…,tn) è conseguenza logica di P, cioè
Decl(P) = { p(t1,…,tn) | P |= p(t 1,…,tn)}.
Esempio
Se P è il programma dell’esempio precedente, allora Decl(P) = Op(P).
Vedremo tra breve come tale insieme possa essere caratterizzato anche in termini di
modelli alla Herbrand. A tale scopo introduciamo alcune semplici definizioni e
convenzioni.
Ancora Interpretazioni e Modelli alla Herbrand
Definizione Universo e Base di Herbrand di P
L’universo di Herbrand di P, H(P), è l’insieme di tutti i termini ground costruiti a partire
dagli operatori presenti in P. La base di Herbrand di P, B(P), è l’insieme di tutti i possibili
atomi ground, costruibili a partire dai simboli di predicato presenti in P, applicati agli
elementi di H(P).
Esempio
Sia P il programma ricordato nell’esempio precedente, allora H(P) = {a, b, c} e B(P) =
Op(P).
Sia P1 il seguente programma
36
nat(0)←
nat(s(X) ← nat(X),
allora H(P1) = {0, s(0), s(s(0)),…} e B(P1) = {nat(0), nat(s(0)), nat(s(s(0))),…}.
Sia P2 il programma
p(X)← q(f(X))
p(a)←
p(b)←
q(f(c))←
allora H(P2) = {a, b, c, f(a), f(b), f(c), f(f(a)), f(f(b)), f(f(c)),…} e
B(P2) ={p(a), p(b), p(c), q(a), q(b), q(c), p(f(a)), p(f(b)), p(f(c)), p(f(f(a))), p(f(f(b))),
p(f(f(c))),… q(f(a)), q(f(b)), q(f(c)), q(f(f(a))), q(f(f(b))), q(f(f(c))),…}.
Sia P3 il programma
add(X, 0, X)←
add(X, s(Y),s(Z)) ← add(X,Y, Z)
allora H(P3) = H(P1) e
B(P3) = {add(0, 0, 0), add(s(0), 0, 0), add(s(s(0)), 0, 0),…
add(0,s(0), 0), add(s(0), s(0), 0), add(s(s(0)), s(0), 0),…}.
Sia P4 il programma
a←
b←a
allora H(P4) = {} e B(P4) = {a, b}.
Ricordando la definizione di algebra di Herbrand, possiamo convenire che, in pratica, una
qualsiasi di queste algebre, diciamo I, per un programma logico P è unicamente
determinata da quel sottinsieme di B(P), composto dagli atomi ground p(t1,…,tn), tali che
(t1,…,tn)∈ pI. Per abuso di linguaggio chiamiamo allora algebra di Herbrand I anche quel
sottinsieme di atomi della base che sono veri in una data algebra di Herbrand I, rendendo
così valida la seguente equivalenza per un’algebra di Herbrand I relativa ad un programma
P ed un atomo A∈ B(P) : I |= A sse A ∈ I.
Allora l’insieme di tutte le interpretazioni di Herbrand di un programma logico P è
l’insieme potenza di B(P). Per i programmi visti nell’esempio precedente avremo allora le
seguenti possibili algebre di Herbrand:
I1(P1) = {nat(0)},
I2(P1) = {nat(0), nat(s(0)},
I3(P1) = B(P1),
I1(P2) = {p(a), p(b), p(c), q(f(c))},
I2(P2) = {p(a), q(f(a)},
I3(P2) = B(P2),
37
I1(P3) = {add(0, 0, 0), add(s(0), 0, s(0)), add(s(s(0)), 0, s(s(0))), …add(s(0), s(0), s(s(0)))},
I2(P3) = {add(0, s(0), s(s(0))},
I1(P4) = {},
I2(P4) = {a},
I3(P4) = {b},
I4(P4) = {a, b}.
Possiamo, a questo punto, riformulare la definizione della relazione di validità di una
formula in un’algebra, per formule atomiche e clausole definite, in un’algebra di Herbrand
I.
Definizione Relazione di Validità in un’Algebra di Herbrand
Dato un programma logico P e un’algebra di Herbrand I di P, la relazione di validità in I di
una clausola definita e sue sottoformule ϕ, I |= ϕ ovvero I è modello di ϕ, è così definita
• formula atomica ground : data la formula atomica ground A, I |= A sse A∈I;
• congiunzione di formule atomiche ground: data la congiunzione B1,…Bq di formule
atomiche ground, I |= B1,…Bq sse {B1,…Bq}⊆ I;
• clausola definita: data la clausola definita A← B1,…Bq, I |= A← B1,…Bq sse ogni sua
istanza ground è valida in I;
• clausola definita ground: data la clausola definita ground A← B1,…Bq,
I |= A← B1,…Bq sse {B1,…Bq}⊆ I implica A∈ I.
Ovviamente, un’algebra di Herbrand I è modello (di Hebrand) di un programma logico P,
denotato con M(P) = I, sse I è modello di ogni clausola di P.
Esempio
Per i programmi visti nell’esempio precedente alcuni modelli di Herbrand sono:
M(P) = B(P), { in P si definisce la relazione collega}
M1(P1) = I3(P1) = B(P1), {in P1 si definisce la relazione nat}
M1(P2) = {p(a), p(c), p(b), q(f(c))}, {in P2 si definiscono le relazioni p e q}
M2(P2) = {p(a), p(c), p(b), q(f(c)), q(f(a)), p(f(a))},
M3(P2) = {p(a), p(c), p(b), q(f(c)), p(f(a))},
possono infatti esistere più modelli per un dato programma P, però
M(P4) = B(P4) = {a, b} è l’unico modello di P4. {in P4 sono definite a e b}
Per P3 si ha {in P3 si definisce la relazione add}
M(P3) = {add(0, 0, 0), add(0, s(0), s(0)), add(0, s(s(0)), s(s(0))),…, add(s(0),0, s(0)),…}
ossia {add(sn(0), sm(0), sn+m(0))| n,m ≥ 0}.
Interpretazioni di Herbrand che non sono modelli sono:
I1(P1) = {nat(0)}, I2(P1) = {nat(0), nat(s(0))} perché falsificano la seconda clausola e
ancora
I2(P2) = {p(a), p(f(a))}, falsifica la terza e quarta clausola di P2.
38
Alcune Proprietà dei Modelli di Herbrand
Teorema Intersezione dei Modelli di Herbrand
Se P è un programma logico, l’intersezione di un qualunque insieme L di modelli di
Herbrand di P è ancora un modello di Herbrand di P.
Dim. Si supponga per assurdo che ∩L non sia un modello di P, falsifichi cioè almeno
un’istanza ground di una clausola di P. Sia Cσ = A← B1,…Bq, q ≥ 0. Poiché Cσ è falsa
in ∩L, A ∉ ∩L, mentre per ogni i =1,…,q Bi∈∩L. Deve esistere allora un elemento M di
L per cui Bi∈M per ogni i =1,…,q e A∉ M. Allora Cσ è falsa in M e quindi pure C è falsa
in M che è una contraddizione.
Quindi per un programma logico P, l’intersezione di tutti i suoi modelli è ancora un suo
modello detto il modello minimo di P, MP.
Poiché per un programma logico esiste sempre un modello, esattamente la sua base B(P),
segue che esiste sempre anche il suo modello minimo.
Esempio
Sia dato il seguente programma P5
p(X) ← q(X)
p(a) ←
p(b) ←
q(c) ←
I suoi modelli di Herbrand sono
M1(P5) = {p(a), p(b), p(c), q(a), q(b), q(c)} = B(P5),
M2(P5) = {p(a), p(b), p(c), q(a), q(c)},
M3(P5) = {p(a), p(b), p(c), q(b), q(c)},
M4(P5) = {p(a), p(b), p(c), q(c)}
Per cui MP = M4(P5) =
Mi( P5)
i =1,.., 4
Vale inoltre il seguente teorema nel quale si caratterizza la semantica a modelli di un
programma come, appunto, il suo modello minimo di Herbrand.
Teorema Modello Minimo di Herbrand e Conseguenze Logiche
Sia P un programma logico, allora M P = {A ∈ B(P) | P |= A} ossia M P = Decl(P).
Dim. A è conseguenza logica di P, ossia
P |= A sse
P ∪ {← A} è insoddisfacibile sse
P ∪ {← A}non ha modelli di Herbrand (teorema di Herbrand) sse
← A è falso in tutti i modelli di Herbrand di P sse
A è vero in tutti i modelli di Herbrand di P sse
A ∈ MP.
39
Correttezza e Completezza della Risoluzione SLD
La proprietà di correttezza segue da quella della Risoluzione per le Clausole Generali. Se
ne può dare, inoltre, una dimostrazione diretta che prova come le sostituzioni calcolate con
la risoluzione SLD siano anche sostituzione corrette ed in particolare che Op(P) ⊆ MP.
Per quanto riguarda la completezza, viene provato dapprima che MP ⊆ Op(P), stabilendo
così la completa equivalenza fra le due semantiche, operazionale e a modelli, e poi che per
ogni sostituzione corretta per P ∪ {G} esiste una refutazione SLD per P ∪ {G} con una
sostituzione calcolata almeno tanto generale quanto quella corretta.
Teorema Correttezza della Risoluzione SLD
Sia P un programma logico e G una clausola goal. Allora ogni risposta calcolata attraverso
una refutazione SLD di P ∪ {G} è una risposta corretta per P ∪ {G}.
Dim. Sia G = ← A1,…,Ak, e si assuma che esista una refutazione SLD per P ∪ {G}. Sia σ
= σ1…σn, la risposta calcolata. Dobbiamo mostrare che ∀(A1∧…∧Ak)σ è conseguenza
logica di P. Si dimostra per induzione sulla lunghezza, n, della refutazione.
Base Sia n = 1, allora G è costituito dal solo atomo A1 che unifica con la testa di una
variante di un fatto ( o clausola unitaria), A←, di P. Si ha cioè A1σ1= Aσ1. Ma A1σ1 è
allora una istanza di una clausola unitaria di P, quindi si ha che ∀A1σ1 è conseguenza
logica di P.
Induzione L’ipotesi induttiva assicura che l’asserto vale per risposte calcolate derivanti da
refutazioni di lunghezza n – 1. Sia A← B1,…,Bq, q ≥ 0, la variante della clausola scelta al
primo passo di risoluzione e A m l’atomo selezionato in G, per cui Amσ1 = Aσ1.
Dall’ipotesi induttiva segue che ∀(A1∧…∧ B1∧…∧Bq ∧…∧ Ak)σ è conseguenza logica di
P, ovvero σ è corretta per P ∪ {← A1,…, B1,…,Bq,…,Ak}.
Allora anche ∀( B1∧…∧Bq) σ è conseguenza logica di P. Dalla definizione della relazione
modello per una clausola segue allora, essendo A← B1,…,Bq variante di una clausola di P
e quindi valida in ogni modello di P ossia conseguenza logica di P, che per forza anche
∀Am σ è conseguenza logica di P. Ma allora anche ∀(A1∧…∧Am ∧…∧Ak)σ lo è.
Diretta conseguenza di questo teorema è allora il seguente corollario.
Corollario Op(P) ⊆ MP
L’insieme di successo di P, ovvero la sua semantica operazionale, è contenuto nel suo
modello minimo di Herbrand.
Completezza della Risoluzione SLD
Tale proprietà viene dimostrata facendo riferimento anche alle sostituzioni corrette per un
dato goal che vengono mostrate essere “calcolate” da sostituzioni almeno altrettanto
generali tramite refutazioni SLD. La prova viene data per passi successivi, dapprima
facendo vedere MP ⊆ Op(P) e quindi, tramite altri risultati intermedi e servendosi della
appropriata versione del lemma del lifting, provando la proprietà sopra riportata.
Lemma Lifting per la Risoluzione SLD
Sia P un programma logico, G un goal e σ una sostituzione. Se esiste una refutazione SLD
per P ∪ {Gσ}allora esiste una refutazione SLD anche per il goal più generale G.
Ovvero esiste una refutazione di P ∪ {G}della medesima lunghezza di quella per
P ∪ {Gσ} e tale che se σ1,…,σn sono le sostituzioni mgu utilizzate nella refutazione di
P ∪ {Gσ} e σ1’,…,σn’ sono quelle utilizzate in quella di P∪{G}, allora esiste una
sostituzione λ tale che σσ1…σn|var(G) = σ1’…σn’λ|var(G).
40
Esempio
Si consideri il programma P:
ad(s(X), 0, s(X)) ←
il goal G = ad(s(Y), 0, s(Y)) e la sostituzione σ = {Y/0}.
Esiste una refutazione SLD per Gσ, di lunghezza uno, con σ1={X1/0}; per G esiste allora
una refutazione SLD anch’essa di lunghezza uno con σ1’={Y/X2}, allora è possibile
trovare una sostituzione λ, con λ ={X2/0}, tale che σσ1|{Y} = {Y/0}{X1/0}|{Y}={Y/0} e
σ1’λ|{Y} = {Y/X2}{X2/0}|{Y}={Y/0}.
Teorema Completezza Debole
Sia P un programma logico, allora MP ⊆ Op(P), quindi, grazie al teorema di correttezza,
MP = Op(P) .
Dim.Immediata conseguenza della definizione della relazione modello per algebre di
Herbrand è che MP = {A∈B(P)| A← B1,…,Bq è un’istanza ground di una clausola di P e
{B1,…,Bq}⊆ MP} può essere calcolato per approssimazioni successive dalla sequenza di
insiemi In(P), n ∈ N (vedi [1] p. 179), con
I0(P) = {} e
In(P) = In-1(P)∪{A∈B(P) | A← B1,…,Bq, q ≥ 0, è una istanza ground di
una clausola di P e B i ∈ In-1(P) per i =1,…,q }, per n > 0.
Mostriamo allora per induzione su n che In(P) è un sottoinsieme di Op(P).
Base Per I0(P) la proprietà è banale.
Supponiamo come ipotesi induttiva che Ik(P) ⊆ Op(P).
Induzione Se A∈ Ik+1(P), deve esistere allora una clausola B← B1,…,Bq in P ed una
sostituizione mgu σ tale che A = Bσ. L’applicazione di σ al corpo di questa clausola o lo
rende ground oppure esiste una ulteriore sostituzione λ la cui applicazione lo rende ground,
si ha allora per la definizione di In(P) che comunque { B1σλ,…,Bqσλ}⊆ Ik(P). Esiste allora
per l’ipotesi induttiva una refutazione SLD di P∪{←B iσλ}per i = 1,…,q. Poiché i B iσλ
sono atomi ground deve esistere una refutazione per P∪{←B 1σλ,…,Bqσλ}.
Per il lemma del lifting esiste allora una refutazione SLD per P∪{←B1σ,…,Bqσ}. Di
conseguenza, esiste una refutazione SLD per P∪{←Bσ}= P∪{←A}.
Tale teorema stabilisce l’equivalenza fra la semantica operazionale e quella a modelli.
Occorre, d’altra parte considerare goal più generali come congiunzione di atomi anche non
ground con le relative risposte corrette e calcolate.
Teorema Insodisfacibilità e Refutazione SLD di P ∪ {G}
Sia P un programma logico, G un goal tali che P ∪{G}è insoddisfacibile, allora esiste una
refutazione SLD di P ∪{G}.
Dim.Sia G = ← A1,…,Ak. Se P ∪{G}è insoddisfacibile allora G non è vero nel modello
minimo di Herbrand di P MP. Deve allora esistere un’istanza ground di G, Gσ, che non è
valida in MP. Allora {A1σ,…,Akσ}⊆ MP. Per il teorema di completezza debole, poiché
tutti gli Aiσ sono ground, esiste una refutazione SLD di P ∪{← Aiσ}per ogni i = 1,…,k e
quindi anche di P ∪{Gσ}. Per il lemma del lifting esiste allora anche una refutazione per
P ∪{G}.
41
Lemma Goal Atomico A con P|= ∀(A)
Sia P un programma logico e A un atomo. Si supponga che ∀(A) sia conseguenza logica di
P, ovvero la sostituzione vuota è una risposta corretta per P ∪{← A}. Allora esiste una
refutazione SLD di P ∪{← A}con sostituzione identità come risposta calcolata.
Dim. Siano X1,…,Xn le variabili in A. Siano ora a1,…,an costanti distinte che non figurano
né in P né in A e sia θ = { X1/a1,…,Xn/an}. È evidente che Aθ è conseguenza logica di P e
dal momento che Aθ è ground, sappiamo per il teorema di completezza debole che
P ∪{←Aθ}ha una refutazione SLD. Giacchè le ai non compaiono né in A né in P,
sostituendo le ai con le Xi, per i = 1,…,n, in questa refutazione si ottiene una refutazione di
P ∪{←A} con la sostituzione identità come sostituzione calcolata.
Finalmente, possiamo dare il seguente teorema di completezza.
Teorema Completezza della Risoluzione SLD
Sia P un programma logico, G un goal, allora per ogni risposta corretta σ per P ∪{G}
esiste una risposta calcolata θ per P ∪{G}ed una sostituzione λ tali che σ = θλ.
Dim.Sia G = ← A1,…,Ak. Poichè σ è una risposta corretta si ha che ∀( A1∧…∧Ak)σ è una
conseguenza logica di P, allora per il teorema e lemma immediatamente precedenti esiste
una refutazione SLD di P ∪{← Aiσ}per ogni i = 1,…,k. Quindi, combinando queste
refutazioni, P ∪{Gσ}possiede una refutazione con risposta calcolata uguale all’identità.
Nella refutazione di P∪{Gσ}, siano σ1,…,σn le sostituzioni utilizzate, quindi Gσσ1…σn =
Gσ.
Allora si ha che (σ1…σn)|var(G) = {}. Per il lemma del lifting esiste allora una refutazione
di P ∪{G}, con una sequenza di mgu σ1’,…,σn’, ed una sostituzione λ tali che σ(σ1…σn) =
σ1’…σn’λ. Sia σ1’…σn’|var(G) = θ, allora σ = θλ.
Il Trattamento della Negazione
Nelle clausole definite unitarie e non unitarie non possono figurare formule atomiche
negate. Si ricorda infatti che le regole sono implicazioni di una formula atomica, il
letterale positivo (testa), da parte di una congiunzione di letterali positivi (corpo).
Osserviamo inoltre che tramite la risoluzione non è possibile derivare letterali negativi.
Infatti, se P è un programma logico e B(P) la sua base di Herbrand, per qualunque atomo
A∈ B(P) non è possibile provare che ~A è conseguenza logica di P: l’insieme
P∪{~(~A)}= P∪{A}è sempre soddisfacibile in quanto ha almeno un modello, esattamente
la sua base di Herbrand.
Esempio
Sia dato il programma P:
intero(0) ←
intero(1) ←
intero(2) ←
razionale(rapp(2,3))←
sia ϕ = ~ intero(rapp(2,3)).
Allora ϕ non è conseguenza logica di
P∪{intero(rapp(2,3))} risulta soddisfacibile.
P∪{intero(rapp(2,3))}
è infatti
42
P in quanto
Un modello
l’insieme P∪{~ϕ}=
di Herbrand per
{intero(0), intero(1), intero(2), razionale(rapp(2,3)), intero(rapp(2,3))}.
D’altra parte si osservi che, come ci si aspetta, neanche ~ϕ = intero(rapp(2,3)) è
conseguenza logica di P: infatti P∪{~(~ϕ)}= P∪{~ intero(rapp(2,3))} è soddisfacibile in
quanto ha come modelli tutti i sottoinsiemi di B(P) che non contengono l’atomo
intero(rapp(2,3)) e che soddisfano le clausole di P, ad esempio un modello è MP =
{intero(0), intero(1), intero(2), razionale(rapp(2,3))}.
Si perviene così all’idea di stabilire come falso, rispetto al programma P, tutto ciò che non
è conseguenza logica di P. È evidente che occorre un ampliamento del calcolo della
risoluzione per trattare la negazione, ossia almeno una regola di inferenza in più oltre a
quella della risoluzione.
Questo punto di vista è espresso dalla regola di inferenza nota come ipotesi del mondo
chiuso (Closed World Assumption) ovvero CWA.
Regola dell’Ipotesi del Mondo Chiuso
Tale regola stabilisce che se un atomo ground A non è conseguenza logica di un dato
programma, allora si può inferire ~A. Ovvero
P |≠ A
~A
(CWA)
Dato P sia B(P) la sua base di Herbrand e MP il suo modello minimo. L’insieme dei
letterali derivati tramite la CWA è allora, data la correttezza e completezza della
risoluzione SLD,
CWA(P) = {~A | A ∈ B(P) e non esiste una refutazione SLD per P ∪{← A}}.
Poiché Decl(P) = MP si ha
~A ∈ CWA(P) sse A ∈ B(P) − MP.
Esempio
Sia P il seguente programma
intero(2) ←
numero(X) ← intero(X)
numero(3) ←
Il modello minimo di P è
{intero(2), numero(2), numero(3)},
mentre la sua base è
{intero(2), numero(2), numero(3), intero(3)}
per cui
CWA(P) = {~intero(3)}.
Considerando attentamente questa regola, si possono rilevare due inconvenienti, di cui il
secondo assai grave:
• questa regola non è monotona: nuovi assiomi aggiunti a P possono far diminuire
il numero degli elementi di CWA(P), in quanto può aumentare MP;
• non esiste alcun algoritmo in grado di stabilire, in un tempo finito, che dato A
questo è o non è conseguenza logica di P. Infatti la risoluzione SLD è completa nel
senso che se A è conseguenza logica, allora la clausola vuota è, prima o poi,
derivata da P ∪{← A}, ma se non lo è, la ricerca della clausola vuota, nell’albero
43
SLD, può terminare con fallimento o continuare all’infinito, ossia si può avere non
terminazione.
Allora l’applicazione della CWA deve necessariamente essere ristretta agli atomi della
base di Herbrand la cui prova termina in tempo finito, se vogliamo la decidibilità.
Esempio
Dato il seguente programma P
numero(3) ← numero(3)
numero(2) ←
si ha
B(P) = {numero(2), numero(3)},
MP = {numero(2)},
CWA(P) = {~ numero(3)}
infatti numero(3) non è conseguenza logica di P, non vale in tutti i suoi modelli come
testimonia il modello minimo di P, ma la risoluzione SLD non termina per il goal
← numero(3), ossia l’albero SLD per P ∪{← numero(3)} è semplicemente infinito, senza
alcun percorso di successo o di fallimento (finito).
Si perviene allora alla sostituzione della CWA con una regola meno potente, che riesce a
dedurre un insieme eventualmente più piccolo di CWA(P) per un dato P, nota come la
regola della Negazione per Fallimento (o Negation as Failure) che “si limita” a derivare la
negazione di atomi di B(P), la cui dimostrazione termina con fallimento in tempo finito.
Regola della Negazione per Fallimento
Dato il programma P, l’insieme FF(P) (insieme di fallimento finito) è costituito da tutti gli
atomi A di B(P) per cui la dimostrazione dell’insoddisfacibilità di P ∪{← A}termina con
fallimento in un tempo finito. Tale regola , detta NF, viene espressa come
A ∈ FF ( P)
~A
(NF)
È evidente che se A ∈ FF(P) A non è conseguenza logica di P, ma non è detto che tutti gli
atomi che non sono conseguenza logica di P appartengano a FF(P).
Per il programma P dell’ultimo esempio visto, FF(P) è vuoto quindi non contiene
numero(3) anche se questo atomo non è conseguenza logica di P; allora se
NF(P) = {~A | A∈ FF(P)}
si ha NF(P) ⊆ CWA(P).
Questa regola è abbastanza facilmente realizzabile ed è quella adottata nelle
implementazioni della Programmazione Logica, ad esempio le realizzazioni del linguaggio
Prolog.
Caratterizzazione Operazionale della NF
Si fa qui uso della nozione di albero di fallimento finito, come di seguito introdotta.
Definizione Albero di Fallimento Finito
Dato un programma P ed un goal G, un albero SLD per P ∪{G}è detto di fallimento finito
se è finito e non contiene alcun percorso di successo.
44
Esempio
Sia dato il programma P
numero(X) ← razionale(X)
numero(X) ← intero(X)
intero(2) ←
razionale(rapp(2,3)) ←
uomo(francesco) ←
per il goal ← numero(francesco) si ottiene il seguente albero SLD di fallimento finito
← numero(francesco)
←razionale(francesco)
fail
← intero(francesco)
fail
non si ottiene un albero di fallimento finito per il goal ← numero(2), infatti
← numero(2)
←razionale(2)
fail
← intero(2)
|
←
è un albero finito ma contiene un percorso di successo.
Sia ora P il seguente programma
numero(X) ← numero(X)
numero(X) ← razionale(X)
intero(2) ←
razionale(rapp(2,3)) ←
e sia dato il goal ←numero(2); si ha ora il seguente albero SLD di fallimento non finito
← numero(2)
←numero(2)
|
←numero(2) ← razionale(2)
|
fail
:
∞
← razionale(2)
fail
Definizione Insieme di Fallimento Finito SLD
Dato un programma logico P, l’insieme di fallimento finito SLD di P, FFSLD(P), è l’insieme
di tutti gli atomi A∈ B(P) tali che esiste un albero SLD di fallimento finito per P ∪{← A}.
45
Il requisito circa l’“esistenza” è motivato dalla seguente considerazione. Se A non è
conseguenza logica di P, si possono avere alberi di fallimento finiti o infiniti a seconda
della regola di calcolo adottata.
Esempio
Sia dato il seguente programma P
città(X) ← città(X)
città_italiana(X) ← città(X), in_italia(X)
in_italia(bologna) ←
in_inghilterra(londra) ←
per il goal ← città_italiana(londra), adottando la regola leftmost si ottiene l’albero infinito
← città_italiana(londra)
|
← città(londra), in_italia(londra)
|
← città(londra), in_italia(londra)
|
:
∞
mentre adottando la regola rightmost si ha il seguente albero di fallimento finito
← città_italiana(londra)
|
← città(londra), in_italia(londra)
fail
Per la definizione di FFSLD(P) è sufficiente che esista almeno un albero SLD di fallimento
finito per P ∪{← A}.
È fondamentale quindi adottare una regola di calcolo che ci garantisca di trovare un albero
di fallimento finito, se esiste. Per questo introduciamo la nozione di fairness.
Definizione Fairness
Una derivazione SLD è detta fair se fallisce oppure se ciascun atomo (o una sua istanza)
presente (in un goal) nella derivazione, viene selezionato in un tempo finito.
Una regola R è detta fair se ogni derivazione SLD, in accordo a tale regola, è fair.
Una regola fair data in [3] pag.76 è riportata a pag.48 . L’adozione di una regola fair, R,
assicura che se esiste un albero di fallimento finito per P ∪{← A}, la risoluzione SLD via
R fallisce finitamente per P ∪{← A}. Vale infatti il seguente teorema.
Teorema Fallimento Finito e Regola Fair
Sia P un programma logico e A ∈ B(P). Le seguenti affermazioni sono equivalenti.
• A ∈ FF(P)
• Per ogni regola di calcolo fair R, l’albero SLD per P ∪{← A}via R è di
fallimento finito.
46
Allora la risoluzione SLD con regola di calcolo fair è una realizzazione corretta e completa
del concetto di “fallimento finito”.
Siamo allora in grado di presentare la definizione di un nuovo calcolo che incorpora sia la
regola di risoluzione SLD che la regola NF.
La Risoluzione SLDNF
Possiamo ora considerare goal generali che possono contenere anche letterali negativi.
Per questo definiamo una estensione della risoluzione SLD, nota come SLDNF, che
combina la risoluzione SLD e la regola di negazione per falliment o finito.
Sia ← L1,…,Lm il goal corrente dove ciascun Li è un letterale (positivo o negativo).
Un passo di risoluzione SLDNF può essere così schematizzato:
o Non viene selezionato alcun letterale negativo se non è ground (safeness).
o Se il letterale selezionato è positivo, si compie un ordinario passo di risoluzione
SLD.
o Se il letterale selezionato, Li, è ~A con A ground e l’albero SLD per P ∪{← A} è
di fallimento finito, Li ha successo e si ottiene il nuovo risolvente
← L1, …, Li-1, Li+1, …, Lm
(N.B. senza l’applicazione di alcuna sostituzione).
Una regola di calcolo è safe se seleziona un letterale negativo solo quando è ground.
Per dimostrare ~A, si costruisce quindi una dimostrazione per A. Se questa ha successo
allora la dimostrazione di ~A fallisce, mentre se la dimostrazione per A fallisce finitamente
~A è dimostrato con successo.
Purtroppo, per le solite ragioni di efficienza, nelle implementazioni del Prolog la safeness
viene rilasciata e si seleziona sempre il letterale più a sinistra senza controllare la sua
groundness. È evidente che questa è una relizzazione non corretta (né fair né safe) del
calcolo SLDNF.
Esempio
Sia P
capitale(roma) ←
capoluogo(bologna) ←
città(X) ← capitale(X)
città(X) ← capoluogo(X)
e G il goal ← ~capitale(X), città(X).
Adottando la regola leftmost si seleziona il letterale ~capitale(X) e si produce un
fallimento, in quanto esiste una derivazione di successo per P∪{← capitale(X)} con
sostituzione calcolata {X/roma}.
Si viene allora a perdere la completezza in quanto G corrisponde alla negazione della
formula ϕ = ∃X(~capitale(X) ∧ città(X)) per cui esiste l’istanza di X = bologna tale che
~capitale(bologna) è derivabile da P tramite NF e città(bologna) è conseguenza logica di P.
Si ottiene infatti una refutazione SLDNF per il goal
G = ← ~capitale(bologna), città(bologna).
Il problema nasce dal fatto che se il letterale negativo non è ground la quantificazione non
viene correttamente interpretata. Si voleva infatti dimostrare
47
ϕ = ∃X(~capitale(X) ∧ città(X))
corrispondente al goal
G = ← ~capitale(X), città(X).
Selezionando il primo letterale si tenta di verificare il fallimento finito della formula
∃X capitale(X).
In caso di fallimento finito ciò equivale a dimostrare
~∃X capitale(X) ossia ∀X ~capitale(X).
Allora, in Prolog, si dimostrano formule atomiche positive quantificate esistenzialmente e
formule atomiche negative quantificate universalmente, ma non formule atomiche negative
quantificate esistenzialmente.
Finalmente, tutto funziona bene quando il letterale negativo selezionato è ground.
Una Regola di Calcolo Fair
Si può avere la fairness, come indicatao in [3] pag.76, selezionando l’atomo più a sinistra
che si trova alla destra dell’insieme (eventualmente vuoto) di atomi introdotti al passo di
derivazione precedente, se tale atomo esiste; altrimenti, selezionando l’atomo più a sinistra.
48
Breve Excursus sul Linguaggio Prolog
Il linguaggio Prolog (Programming in Logic) è una relizzazione della programmazione
logica con l’uso della negazione, ovvero è un linguaggio la cui semantica operazionale è
data dalla Risoluzione SLDNF. Alle caratteristiche peculiari della programmazione logica,
peraltro realizzata per ragioni di efficienza in modo non completo (ricerca in profondità
nell’albero SLD) e non corretto (uso della regola di calcolo leftmost non safe e non fair,
assenza dell’occur check nell’algortmo di unificazione sintattica), aggiunge alcune
interessanti estensioni dovute alla possibilità di usare predicati built-in per
o controllo (predicati cut, call, setof, etc.),
o valutazione di espressioni e funzioni numeriche (predicato is etc.)
o Input/Output (predicati read, write, etc.)
o ispezione/manipolazione di termini(predicati atom, var, functor, arg etc.)
o ispezione/manipolazione del programma (predicati clause, assert, retract, etc.)
o definizione/uso di grammatiche context-free
o grafica
o etc..
Per quanto riguarda la sintassi usata nel Prolog, una clausola definita viene espressa come
A.
(clausola unitaria: A ←),
A :- B1,…,Bq. (regola: A ← B1,…,Bq)
ed una clausola goal come
?- B1,…,Bq. (clausola goal: ← B1,…,Bq)
dove il simbolo “?-“ è il prompt del sistema.
Le formule atomiche ed i termini seguono la sintassi già vista nella prima parte.
Usualmente fra le costanti (atomi come a, ab, c2) che il Prolog mette a disposizione sono
possibili numeri interi e reali secondo la solita sintassi e solite limitazioni dovute
all’implementazione, le variabili sono stringhe di caratteri di cui il primo è o il carattere
“_” o una lettera maiuscola. Una linea il cui primo carattere è %, è un commento, e anche
tutto ciò che è compreso fra /* e */ anche su più linee.
Strategia di Ricerca del Prolog: in Profondità con Backtracking Cronologico
Dato il letterale L1 selezionato nel goal corrente ?- L1, L2,…, Ln,viene scelta la prima
clausola, nell’ordine testuale in cui figura nel programma, la cui testa unifica con il valore
assoluto di L1. Allora la risoluzione di L1 viene considerata come un punto di scelta nella
refutazione se la clausola scelta non è l’unica la cui testa è un atomo che unifica con il
valore assoluto di L1. Se la variante della clausola scelta è A :- B1,…,Bq. con σ mgu fra A e
L1, il goal corrente viene ridotto al nuovo goal (risolvente)
?-(B1,…,Bq, L2,…, Ln)σ
Nel caso di fallimento in uno dei passi di risoluzione, il sistema ritorna, in backtracking,
all’ultimo punto di scelta in senso cronologico, ossia a quello più recentemente incontrato e
seleziona la successiva clausola utilizzabile.
Tutto questo corrisponde ad una ricerca in profondità con backtracking cronologico
nell’albero SLD per P ∪ {G}, per un certo programma P e goal G.
Sappiamo che la ricerca in profondità può comportare incompletezza. Si consideri infatti il
programma P
c1: p:-q,r.
c2: p.
c3: q:-q,t.
ed il goal G = ?-p.
L’ albero SLD per P ∪ {G}è allora
49
?-p.(punto scelta 1)
←
?-q,r.
|
?-q,t,r.
:
Quindi, come vediamo e sapevamo, la strategia utilizzata dal Prolog non è completa e
l’ordine delle clausole in un programma è rilevante.
Se consideriamo, infatti, il programma P’
c1: p.
c2: p:-q,r.
c3: q:-q,t.
sia ha che P e P’sono logicamente equivalenti, ma non sono programmi Prolog
equivalenti.
La ricerca nell’albero per P ∪ {G} della clausola vuota non termina (detto brevemente:
l’esecuzione di P non termina per G) mentre termina per P’. In questo caso l’ordine in cui
si trovano le clausole è stato determinante.
Come già accennato, il motivo della scelta di tale strategia risiede nella semplicità della sua
realizzazione, il processo di esecuzione può essere definito tramite una semplice macchina
(astratta) a stack , come vedremo più avanti.
Unificazione in Prolog
Per i soliti motivi di efficienza, l’algoritmo di unificazione realizzato nel sistema Prolog,
non compie la verifica dell’occorrenza (occur check) di una variabile in un termine, nella
regola di eliminazione di variabile, vedi il paragrafo sull’unificazione sintattica a pag. 5.
Volendo unificare i termini X e s(X) si dovrebbe avere un fallimento, non esiste infatti
alcuna sostituzione che applicata ai due termini li renda identici; in Prolog, invece, tale
goal, ?-X=s(X)., ha successo con la restituzione della sostituzione {X/s(s(…))}, dove
s(s(…)) è un termine infinito rappresentato finitamente.
Dato allora il programma P
p(X,s(X)).
q:-p(Y,Y).
il goal ?-q. ha successo anche se q non è conseguenza logica di P.
Dovrà allora essere cura dell’autore del programma e/o del goal di evitare tali erronee
situazioni, o, comunque sua responsabilità il saper leggere le risposte calcolate date dal
sistema.
Controllo dell’Esecuzione – Modello di Esecuzione per il Prolog
Il controllo dell’esecuzione di un programma, ovvero della ricerca/costruzione dell’albero
SLD per dati programma P e goal G, è un concetto assolutamente estraneo alla
programmazione logica. Nel suo uso pratico è nata comunque una tale esigenza di volere e
50
potere influenzare il processo di esecuzione della visita dell’albero SLD, nel senso, molto
drastico, di potarne opportunamente dei sottoalberi.
Lo scopo è quello di rendere meno onerosa la ricerca nell’albero, eliminando dei percorsi
che il programmatore sa non essere di interesse, pur mantenendo il programma del tutto
simile a quello che sarebbe stato scritto non tenendo conto del possibile risparmio di tempo
(e spazio).
D’altra parte si vedrà come tale meccanismo di controllo renda possibile di definire,
usando il Prolog stesso, la regola di negazione per fallimento.
Questi effetti sono tutti dovuti all’uso del predicato cut, denotato da “!”.
Questo è uno dei più importanti e complessi predicati predefiniti disponibili in Prolog. La
sua semantica non è definibile in modo dichiarativo, come per altri predicati predefiniti. Al
fine di illustrare il significato di questo predicato occorre far riferimento al modello di
esecuzione del Prolog.
Il modello di Esecuzione del Prolog
Si consideri il seguente programma P
c1: a:-p, b.
c2: a:-p, c.
c3: p.
ed il seguente goal G = ?- a.
Nella ricerca di una refutazione per P∪{G} nell’albero SLD corrispondente, essendo
presenti in P due clausole, c1 e c2, la cui testa unifica con a, viene selezionata c1 e si
procede. Se per tale strada non si perviene al successo, come in questo caso, il meccanismo
di backtracking porta a selezionare la clausola c2 per un ulteriore tentativo.
Per fare ciò si deve mantenere per ogni goal l’informazione sui possibili punti di scelta che
possono essere ancora intrapresi.
Il modello di calcolo a stack deve quindi prevedere una struttura (stack di backtracking)
per mantenere le informazioni sui punti di scelta incontrati nel corso della dimostrazione
ed una struttura (stack di esecuzione) per mantenere gli atomi via via selezionati con il
riferimento al successivo ed i legami per le variabili, detti record di attivazione.
Si supponga che le scelte per un certo goal atomico g siano contenute in una lista di
riferimenti ad ogni clausola la cui testa è unificabile con g, con la scelta corrente
opportunamente marcata.
Ad esempio, per la valutazione di ?- a. si avrà:
il caricamento (del record di attivazione) di a in testa allo stack di esecuzione e della lista
delle scelte per a in testa allo stack di backtracking:
Stack di Esecuzione
[a]
Stack di Backtracking
[[*c1,c2]]
scelte per a
a viene “ridotto” utilizzando la clausola c1(marcata); il record d’attivazione per l’atomo p
con il riferimento a b, che deve essere caricato dopo l’eventuale successo di p, viene
caricato in cima allo stack di esecuzione :
[p(b), a]
[[*c1,c2]]
scelte per a
p ha successo, utilizzando la c3 che è l’unica possibile, e può essere rimosso dallo stack. Il
passo successivo è allora la valutazione di b, il cui record d’attivazione viene caricato in
cima allo stack:
51
[b, a]
[[*c1,c2]]
scelte per a
ma nessuna clausola può essere scelta, e la valutazione di b genera un fallimento e quindi b
viene rimosso. Viene pertanto attivato il meccanismo di backtracking per verificare se
esistono delle alternative per la continuazione della dimostrazione. Per p non esistevano
alternative, mentre per a c’è ancora un’alternativa. La marca viene posta su c2 e siccome
non esistono altre scelte, l’intera lista delle scelte per a viene pure rimossa pervenendo a:
[p(c), a]
[]
dove in cima allo stack c’è il record d’attivazione per p con riferimento a c, come richiesto
dalla clausola c2; p ha successo e quindi rimosso pervenendo a
[c, a]
[]
il fallimento di c, porta infine al fallimento globale, essendo vuoto lo stack di backtracking.
Allora
•
l’interprete deve mantenere, ovviamente, una lista con le scelte per
ogni occorrenza del medesimo atomo (o sua istanza) che figura nel goal;
• le strutture per mantenere tutte le informazioni sono i due stack
o stack di esecuzione per mantenere i record d’attivazione dei goal atomici
o stack di backtracking per mantenere le liste dei punti di scelta.
La loro gestione può essere così schematizzata:
valutazione di un atomo A: il record d’attivazione per A viene caricato in testa allo
stack di esecuzione; esso contiene le variabili di A ed il punto di ritorno da cui
dovrà essere ripresa la valutazione, dopo l’eventuale successo di A; se vi sono più
alternative per la dimostrazione di A, viene caricata in testa allo stack di
backtracking una lista con le scelte per A con quella corrente marcata. Si devono
ora considerare i seguenti casi:
A ha successo: allora il record d’attivazione per A viene rimosso dallo stack
di esecuzione e viene caricato quello relativo al punto di ritorno presente nel
record d’attivazione di A, se questo esiste. Non viene effettuata alcuna
operazione sullo stack di backtracking, mantenendo così tutte le alternative
per A. Ciò consente sia di fornire soluzioni alternative, se richiesto, per A e
quindi per il resto del goal, sia, in caso di fallimento, di proseguire la ricerca
di una possibile refutazione. La ricerca di soluzioni alternative se richiesta,
tramite la digitazione del carattere “;” da parte dell’utente in risposta al
prompt di sistema, viene operata inducendo un fallimento e forzando così la
ricerca di soluzioni alternative.
A fallisce: allora il record d’attivazione per A viene rimosso. Viene attivato
il meccanismo di backtracking, ossia viene scelta la prima alternativa nella
lista in testa allo stack di backtracking, riprendendo la valutazione da tale
punto. Ciò grantisce che la strategia realizzata sia un backtracking di tipo
cronologico, ossia che venga considerato il più recente punto di scelta
incontrato.
Esempio
Si consideri i programma P:
52
c1: a:- p,b.
c2: a:- r.
c3: p:- q.
c4: p:- r.
c5: r.
ed il goal ?- a.
Vediamo l’evoluzione dei due stack durante la ricerca di un refutazione.
Stack di Esecuzione
[a]
Stack di Backtracking
[[*c1,c2]]
sc. per a
[p(b), a]
[[*c3,c4],[*c1 ,c2]]
sc. per p
[q, p(b),a]
sc. per a
[[*c3,c4],[*c1,c2]]
sc. per p
{q non è definito}
sc. per a
allora q fallisce e viene rimosso dallo stack di esecuzione ed attivato il meccanismo di
backtracking. Viene scelta la clausola c4 e non essendoci più alternative, la lista delle
scelte per p viene rimossa; viene quindi caricato in testa allo stack di esecuzione il record
d’attivazione per l’atomo r, che è definito da una sola clausola, per cui lo stack di
backtracking rimane invariato. Si perviene a
[r, p(b), a]
[[*c1,c2]]
sc. per a
la dimostrazione di r ha immediatamente successo, grazie alla clausola c5 ed r viene
rimosso dallo stack di esecuzione; poiché il record per r non prevede punto di ritorno, si ha
pure il successo di p che viene pure rimosso dallo stack di esecuzione ed il goal atomico b,
punto di ritorno per p, viene caricato in testa allo stack di esecuzione
[b, a]
[[*c1,c2]]
{b non è definito}
sc. per a
b fallisce, viene rimosso e si ha l’attivazione del backtracking che porta a scegliere la
clausola c2 per a, a rimuovere la lista delle scelte per a ed a caricare in cima allo stack di
esecuzione ancora r, che è definito da una sola clausola
[r, a]
[]
la dimostrazione di r ha successo e viene rimosso, non essendoci punto di ritorno, anche a
ha successo ed anche a viene rimosso, pervenendo al successo globale (lo stack
d’esecuzione è vuoto)
[]
[]
Si osservi che in questo caso l’eventuale richiesta di soluzioni alternative avrebbe avuto
una risposta negativa ( “no” emesso dall’interprete), in quanto lo stack di backtracking
risulta vuoto.
Possiamo ora riprendere la descrizione del predicato cut.
53
Il predicato Cut
Tale predicato costante, espresso da “!”, ha sempre successo.
Il suo principale effetto, che è extra logico, è quello di rendere definitive alcune scelte fatte
nel corso della dimostrazione; tale effetto è ottenuto con la eliminazione di alcune liste di
scelte dallo stack di backtracking.
Si consideri la clausola
p:- q1, q2,…,qi, !, qi+1,…,qn.
l’effetto della valutazione del goal “!” è il seguente:
o la valutazione di “!” ha sempre successo;
o tutte le scelte fatte nella valutazione dei goal q1, q2,…,qi ed in quella di p vengono
“congelate” ossia rese definitive: tutti i punti di scelta per tali goal vengono rimossi
dallo stack di backtracking.
Naturalmente, i punti di scelta per i goal precedenti p non vengono toccati dal cut. Come
conseguenza, se si ha un fallimento in uno dei goal qi+1,…,qn, il backtracking viene
limitato alle scelte eventualmente aperte per tali goal.
Se poi ?-qi+1,…,qn. fallisce si ha un fallimento globale per p, infatti tutti i punti di scelta per
q1,…,qi e p stesso sono stati rimossi.
In definitiva, la valutazione del cut modifica radicalmente lo spazio di ricerca, potando
alcuni rami dall’albero SLD per il dato goal e programma.
Esempio
Sia P
c1: g:-a.
c2: g:- s.
c3: a:- p, !, b.
c4: a:- r.
c5: p:- q.
c6: p :- r.
c7: q.
c8: r.
e sia dato il goal ?- g. Si avrà la seguente evoluzione dei due stack:
Stack di Esecuzione
[g]
Stack di Backtracking
[[*c1,c2]]
sc. per g
[a, g]
[[*c3,c4],[*c1,c2]]
sc. per a
[p(!,b), a, g]
sc. per g
[[*c5,c6],[*c3,c4],[*c1,c2]]
sc. per p
sc. per a
sc. per g
{valutazione di q e successo tramite c7, rimozione di q e p, caricamento di !(b)}
[!(b), a, g]
[[*c5,c6],[*c3,c4],[*c1,c2]]
sc. per p
sc. per a
sc. per g
{! ha successo e viene rimosso, tutte le scelte per p ed a ven gono rimosse, viene caricato b}
[b, a, g]
[[*c1,c2]]
sc. per g
{b fallisce e quindi anche a non essendoci alternative, vengono rimossi ed attivato il backtracking}
54
[s, g]
[]
La valutazione di s fallisce e quindi anche l’intero goal ?-g.
Il cut ha avuto un effetto deleterio, in questo caso, in quanto in sua assenza il goal ?- g.
avrebbe avuto successo: è stata infatti rimossa la parte dell’albero SLD dove si trova la
refutazione ( grazie alla clausola c4 per a e c8 per r), come illustrato nella figura seguente.
←g
←a
←p, !, b
← q, !, b
|
←b
fail
←s
fail
←r
|
← r, !, b ←
|
←b
fail
Esempio
Si consideri il seguente programma P
a(X, Y):- b(X), !, c(Y).
a(0, 0).
b(1).
b(2).
c(1).
c(2).
Alla richiesta di valutazione del goal
?- a(X, Y).
si ha la seguente risposta da parte del sistema
X = 1, Y = 1;
{con “;” si richiedono altre soluzioni, se esistono}
X = 1, Y = 2;
no
{i caratteri in grassetto evidenziano gli output del sistema}.
La valutazione del cut ha portato alla rimozione delle alternative per b ed a, ossia al
congelamento della sostituzione {X/1} determinata in relazione alla prima e unica scelta
fatta per la clausola la cui testa unifica con b(X).
Quindi le due alternative per risolvere con c(Y) sono le sole prese in considerazione,
essendo l’atomo c(Y) successivo al cut.
Si noti, d’altra parte, che in assenza del cut avremmo avuto come risposte anche
X = 2, Y = 1;
X = 2, Y = 2;
X = 0, Y = 0
Le proprietà del cut sono spesso utilizzate per ottenere un comportamento deterministico
nella valutazione di un goal.
55
Realizzazione del Determinismo
Spesso occorre definire una relazione tramite clausole che siano mutuamente esclusive,
cosa che comporta l’utilizzazione di predicati (guardie), all’inizio del corpo di ciascuna
clausola che assicurino tale proprietà.
Tale mutua esclusione è semplicemente ottenuta attraverso l’uso del cut.
Se la definizione di p(…) fosse la seguente:
p(…):- a(…), b.
p(…):- c.
dove il predicato a(…) dovrebbe rendere le clausole esclusive, secondo uno schema del
tipo
if a(…) then b else c,
non avremmo correttamente realizzato tale significato, in quanto con un eventuale
backtracking (forse avviato dal fallimento di b o per richiesta di soluzioni multiple)
verrebbe presa in considerazione la seconda clausola, anche nel caso che a(…) fosse
risultato valido.
La mutua esclusione sarebbe ottenuta utilizzando, se possibile, la guardia ~a(…) (in Prolog
not(a(…)), come vedremo in seguito) oppure utilizzando il cut nel modo seguente:
p(…):- a(…), !, b.
p(…):- c.
Quando a(…) è stato verificato e quindi anche ! è stato verificato, la seconda regola per
p(…) viene esclusa dalle possibili alternative, così come ogni alternativa per a(…). D’altra
parte, se a(…) fallisce il cut non viene valutato e l’alternativa per p(…) viene considerata
ottenendo la prova di c, se possibile.
In ogni caso si è ottenuta la mutua esclusione, come descritta nel comando if..then ..else di
sopra.
Esempio
Si consideri la seguente definizione della relazione ternaria, di nome intersect, di
intersezione fra insiemi rappresentati da liste ( vedi par. Le Liste pag. 69):
c1: intersect([], S, []).
c2: intersect([X|Xs], S, [X|Xs1]):- member(X,S),
intersect(Xs, S, Xs1).
c3: intersect([X|Xs], S, Xs1):- intersect(Xs, S, Xs1).
con l’idea che c2 e c3 dovrebbero escludersi a vicenda; ovviamente tale definizione non è
corretta. Tale esclusione viene ottenuta, se member(X, S) è ground, tramite l’inserimento
di not(member(X, S)) come primo letterale del corpo della c3, oppure facendo seguire il
cut a member(X, S) nel corpo della c2.
Ancora, la seguente è la realizzazione della cancellazione della prima occorrenza di un
elemento, che unifica con un dato elemento E, da una lista, ottenendo la lista privata di tale
prima occorrenza:
c1: delete(E, [], []).
c2: delete(E, [E|T], T):- !.
c3: delete(E, [E1|T], [E1|Td]):-delete(E, T, Td).
56
Per un goal costituito dall’atomo delete(t1, t2, t3), se questo unifica con la testa di c2, il cut
viene “eseguito” ed in questo caso la dimostrazione termina, senza possibilità di alternative
in quanto il backtracking non può aver luogo, viceversa se non c’è unificazione il cut non
viene eseguito e si procede dal punto di scelta con la risoluzione con c3.
Strutture Condizionali
In effetti possiamo realizzare una simulazione della struttura if..then..else, definendo il
seguente predicato if_then_else ternario:
if_then_else(Cond, G1, G2):- Cond, !, G1.
if_then_else(Cond, G1, G2):- G2.
dove Cond, G1 e G2 sono variabili che devono essere istanziate con dei termini che
rappresentano goal (variabili metalogiche). In effetti la sintassi del Prolog vorrebbe che,
per indicare il tipo di tali variabili, i corpi delle due clausole fossero scritti come
call(Cond), !, call(G1) e call(G2), dove il predicato predefinito call consente di invocare la
valutazione del goal, suo parametro, da parte dell’interprete del Prolog all’interno di un
programma Prolog. Usualmente tutto funziona anche senza l’uso esplicito di call.
Predicato fail e Iterazione (di relazioni con effetti collaterali)
La valutazione del predcato fail fallisce sempre. Esso permette di realizzare alcune forme
di iterazione. Si pensi che in un programma vi siano più calusole unitarie del tipo p(…) e
consideriamo il problema di voler chiamare la procedura q su ogni elemento X che
soddisfa p(X). La soluzione ricorsiva è assai difficile perché richiederebbe di testare che
l’argomento X attualmente soddisfacente p non sia già stato utilizzato, dovendo così
mantenere una lista di tali elementi già considerati.
Dopo aver determinato un elemento X per cui p(X) è soddisfatto e dopo aver chiamato
q(X) si genera un fallimento, il meccanismo di backtracking farà cercare un’ulteriore
soluzione per p(X), ottenendo così l’iterazione.
Si deve, comunque, garantire la terminazione con successo e può essere utile bloccare il
backtracking sulla verifica di q, per cui la relazione “itera” può essere definita come:
itera:- p(X), verifica_q(X), fail.
itera.
verifica_q(X):-q(X),!.
Il tutto viene lanciato dal goal ?-itera.
La dimostrazione di itera termina sempre, esattamente quando le alternative per p sono
esaurite. Inoltre il cut evita il backtracking su q. Tale forma di iterazione è utile nel caso in
cui la valutazione di q produca effetti collaterali, ad esempio se q “stampa” o “modifica”
qualcosa (vedi write e assert).
Di nuovo la Negazione in Prolog (il predicato not)
Il predicato not, predefinito, è definito in modo tale che
not(P) ha successo se e solo se la dimostrazione del goa l P, ground,
fallisce finitamente.
Tale predicato è definito in Prolog nella maniera seguente:
57
not(P):-call(P), !, fail.
not(P).
L’uso del cut nella prima clusola è necessario. Si ha il fallimento di not(P) appena call(P)
ha avuto successo: si evita così il backtracking che porterebbe al successo con la seconda
clausola. La seconda clausola, d’altra parte, fa sì che not(P) abbia successo quando call(P)
fallisce finitamente.
Quando P non sia ground ci possono essere notevoli problemi, come già osservato sopra.
Dato il goal ?-not(p(X))., il suo significato atteso dovrebbe essere ∃X not(p(X)), ma in
realtà viene verificato il goal ?-p(X)., ossia se ∃Xp(X); per cui ciò che viene verificato è
not(∃Xp(X)) ossia ∀Xnot(p(X)), come già discusso a pag. 47. Ancora un esempio.
Esempio
Dato il programma
disoccupato(X):-not(occupato(X)), adulto(X).
occupato(giovanni).
adulto(mario).
ed il goal ?-disoccupato(Y).
si ha come risposta dal sistema No, nonostante esista “mario”. Infatti il sistema verifica
“X è disoccupato se per ogni Y not(occupato(Y)) e X è adulto”.
Mentre, ovviamente, ?-disoccupato(mario). ha per risposta Yes, ossia ha successo. Al
momento della valutazione di ?-not(p(X)), X deve essere istanziata ad un termine ground.
Predicati setof, bagof e findall
In molte circostanze è necessario calcolare l’insieme S di elementi X che soddisfano il goal
p(X). I predicati standard predefiniti allo scopo sono:
1. bagof(X, P, S) e
2. setof(X, P, S)
dove X è un termine, P è un goal ed S una variabile (o una lista). Il loro significato è
1. S è la lista (con eventuali ripetizioni) delle istanze di X che soddisfano P
2. S è la lista senza ripetizioni (l’insieme) delle istanze di X che soddisfano P.
Il caso interessante è quello in cui X e P spartiscono delle variabili.
La lista prodotta da setof è ordinata secondo l’ordine totale sui termini definito nel sistema
(vedi avanti).
La lista prodotta da bagof, corrisponde, in generale a quello in cui le soluzioni sono state
trovate.
Si abbiano in un programma le seguenti clausole unitarie:
p(1).
p(0).
p(2).
p(1).
q(0).
si ha
?- setof(X, p(X), S).
X = ..
{con “_..” si denota il nome che il sistema assegna a
S = [0, 1, 2] una variabile, ad es X = _1425}
Yes e
58
?- bagof(X, p(X), S).
X = _..
S = [1, 0, 2, 1]
Yes e
?- setof(X, (p(X),q(X)), S).
X = _..
S = [0]
Yes e
?- setof(X, p(X), [0, 1, 2]).
X = _..
Yes e
?- setof(X, p(X), [1, 0, 2]).
no e
?- setof(X, r(X), S).
no
{si potrebbe volere S = [] invece del fallimento} e
?- setof(p(X), p(X), S).
X = _..
S = [p(0), p(1), p(2)]
Yes
Sono consentite quantificazioni esistenziali esplicite su P, secondo la seguente sintassi:
bagof(X, Y^P, S) e analogamente
setof(X, Y^P, S)
per cui S è la lista/insieme delle istanze di X per cui esiste un Y tale che P vale, in generale
al posto della sola variabile Y si può avere una composizione del tipo (Y1,…, Yk), ossia
(Y1,…, Yk)^P col significato ∃Y1…∃Yk P.
Siano in un programma presenti i seguenti fatti:
padre(gio, mar).
padre(gio, giu).
padre(mar, ald).
padre(mar, pa).
padre(giu, ma).
si ha allora
?- setof(X, padre(X, Y), S).
X = _.. Y = ald S = [mar] ;
X = _.. Y = giu S = [gio];
X = _.. Y = ma S = [giu] ;
X = _.. Y = mar S = [gio];
X = _.. Y = pa S = [mar] ;
No
59
vengono allora determinate le liste delle istanze di X per cui, con il medesimo valore di Y,
il goal
padre(X, Y) è verificato.
Invece si ha
?- setof(X, Y^padre(X, Y), S).
X = _.. Y = _.. S = [gio, giu, mar];
No
e ancora
?- setof((X, S), setof(Y, padre(X, Y), S), S1).
X = _.. Y = _.. S = _..
S1 = [(gio, [giu, mar]), (giu, [ma]), (mar, [ald, pa])].
Yes
Infine, un predicato analogo a bagof con quantificazione esistenziale automatica sulle
variabili diverse da quelle presenti in X, è
findall(X, P, S)
per cui, questa volta, se non ci sono soluzioni, invece di fallimento viene restituita la lista
vuota. Allora
?- findall(X, (p(X), q(1)), S).
X = _..
S = []
Yes
e
?- findall(X, padre(X, Y), S).
X = _.. Y = _..
S = [gio, gio, mar, mar, giu]
Yes.
Aritmetica
Nonostante l’equivalenza del paradigma di programmazione logica con tutti gli altri
sistemi formali per esprimere il concetto di calcolabilità, è stato ritenuto opportuno
introdurre nel Prolog l’usuale apparato aritmetico/logico.
Mostriamo comunque, brevemente come una qualsiasi funzione ricorsiva sia calcolabile
mediante un programma Prolog “puro”.
I numeri naturali possono essere rappresentati come termini costruiti sugli operatori 0,
costante, e s unario: 0, s(0), s(s(0)),…
A partire da tali rappresentazioni è possibile costruire l’intera classe delle funzioni
ricorsive sui naturali.
Funzioni Elementari Corrispondente relazione in Prolog
zero(X) = 0
zero(X, 0)
succ(X) = X+1
succ(X, s(X))
60
Una funzione unaria come f: X→ Z è rappresentata dalla relazione f(X, Z).
La classe delle funzioni ricorsive si ottiene a partire da tali funzioni di base, utilizzando i
meccanismi di composizione, ricorsione primitiva e µ-ricorsione.
Vediamo come tale classe di funzioni possa essere costruita mediante predicati in Prolog.
Composizione
Per definizione di funzione calcolabile se g1(X),…,gn(X) e f(Y1,…,Yn) sono calcolabili
allora è calcolabile anche la funzione:
h(X) = f(g1(X),…,gn(X)).
Siano, allora g1(X, Z),…,gn(X, Z) e f(Y1,…,Yn, W) le relazioni Prolog corrispondenti alle
g1(X),…,gn(X) e f(Y1,…,Yn), la definizione della relazione h è:
h(X, W):- g1(X, Y1),…,gn(X, Yn), f(Y1,…,Yn, W).
Ricorsione Primitiva
Per definizione di funzione calcolabile, se g(X) e h(X, Y, Z) sono calcolabili, allora è
calcolabile anche la funzione f(X, Y) così definita:
f(X, 0) = g(X)
f(X, succ(Y)) = h(X, Y, f(X, Y))
Siano g(X, W) e h(X, Y, Z, W) le relazioni Prolog corrispondenti, la relazione
corrispondente alla funzione f è la seguente:
f(X, 0, W):- g(X, W).
f(X, s(Y), W):-f(X,Y, T), h(X, Y, T, W).
µ-Ricorsione (cicli tipo while)
Per definizione di funzione calcolabile, se f(X, Y) è calcolabile, allora è calcolabile anche
la funzione g(X) così definita:
g(X) = µY(f(X, Y) = 0) {g(X) è il minimo Y per cui f(X, Y) = 0}
Sia f(X, Y, Z) il predicato Prolog corrispondente alla funzione f, allora la relazione g è la
seguente:
g(X, Z):- g1(X, 0, Z).
g1(X, Y, Y):-f(X, Y, 0), !.
g1(X, Y, Z) :-g1(X, s(Y), Z).
{la ricerca inizia da Y= 0}
{Y è il minimo t.c.f(X, Y, 0)}
{altrimenti si prova con s(Y)}
Predicato “is” per la Valutazione di Espressioni
Le espressioni aritmetiche sono termini Prolog costruiti a partire da costanti numeriche e
variabili utilizzando come operatori solo quelli aritmetici in accordo al seguente elenco:
Operatori Aritmetici
unari: −, exp, log, ln, sin, cos, tg
binari: +, −, *, /, mod, div (//)
La valutazione di un’espressione ,expr, è indotta dalla verifica del seguente predicato is:
T is expr (is è usato in modo infisso) dove
61
T
può essere un atomo numerico o una variabile, ed
expr deve essere un’espressione contenente eventuali
variabili, che siano correttamente istanziate.
Il risultato della verifica è la valutazione di expr, se possibile; in caso positivo il valore
risultante viene unificato con T.
Così si hanno le seguenti risposte ai seguenti goal:
?-X is 2+3.
X=5
e
Yes
?-X1 is 2+3, X2 is exp(X1), X is X1*X2.
X1 = 5
X2 = 148.413
X = 742.066
Yes
e
?-0 is 3−3.
Yes
e
?- X is Y+1.
No
e
?-X is 2+3, X is 4+2.
No
Il predicato is costituisce l’unico meccanismo dato dal Prolog per forzare esplicitamente la
valutazione di un’espressione, in qualsiasi altro contesto, all’infuori delle espressioni
relazionali, un’espressione è un qualsiasi termine Prolog, nella sua forma puramente
simbolica.
Così, dato il seguente programma
p(a, 2+3*5).
q(X, Y):-p(a,Y), X is Y.
ed il goal
?-q(X, Y).
X = 17
Y = 2+3*5
Yes
si ha
Operatori Relazionali
Questi sono tutti binari e sono:
<, >, =<, >=, = =, \= =, dove = = è l’uguaglianza e \= = la disuguaglianza.
Allora la verifica di un goal del tipo
expr1 rel expr2
con rel operatore relazionale
ed expr1, expr2 espressioni aritmetiche
comporta la valutazione delle espressioni, se possibile, come con is ed i risultati vengono
confrontati secondo la relazione rel.
Allora la relazione abs, può essere realizzata come
62
abs(X, X):- X >= 0, !.
abs(X, Y):- Y is –X.
ed il predicato pari sui naturali come
pari(0).
pari(X):- X>0, X1 is X-1, dispari(X1).
dispari(X):- X>0, X1 is X-1, pari(X1).
Definizione di Relazioni con Uso della Ricorsione
Quando nel corpo di qualche clausola la cui testa è del tipo p(…), figura un atomo del
medesimo tipo (medesimo nome p e medesima arietà), significa che la relazione è definita
usando la ricorsione (diretta). L’uso della ricorsione può avvenire anche indirettamente
(mutua ricorsione), nel senso di “chiamare” la relazione q(…) nel corpo della definizione
della relazione p(…) e “chiamare” p(…) nel corpo della definizione della q(…) come nella
definizione di pari e dispari vista sopra.
Esempio
La funzione fattoriale può essere calcolata dalla seguente relazione
fatt(0, 1).
fatt(N, Y):-N>0, N1 is N-1, fatt(N1, Y1), Y is N*Y1.
Il massimo comun divisore dalla seguente relazione
mcd(X, 0, X).
mcd(X, Y, Z):-Y>0, X1 is X mod Y, mcd(Y, X1, Z).
Il Prolog non offre nessun costrutto iterativo predefinito, solo il meccanismo della
ricorsione provvede alla sua completezza dal punto di vista della teoria della calcolabilità.
Conviene perciò cercare di farne un “buon”uso: analizziamo le definizioni di mcd e fatt
date nell’esempio.
Per mcd la chiamata ricorsiva è l’ultimo atomo nel corpo della clausola ricorsiva, mentre in
fatt il risultato della chiamata ricorsiva è utilizzato nell’ultimo atomo. Nel primo caso si ha
un esempio dello schema di definizione tramite la Ricorsione sulla Coda(Tail Recursion)
nel secondo un esempio di definizione tramite la Ricorsione Non Tail.
Ricorsione Tail
Una funzione f unaria è definita per ricorsione tail se la sua definizione segue lo schema:
f(X): if c(X) then g(X) else f(h(X))
dove
c(X) è una condizione su X,
g ed h sono certe date funzioni indipendenti da f.
Si ha ricorsione sulla coda se f è la funzione più esterna nella definizione ricorsiva, ovvero
se non vengono effettuate ulteriori operazioni sul risultato della chiamata ricorsiva.
In Prolog si potrà avere:
63
f(X,Z):-c(X), !, g(X,Z).
f(X,Z):-h(X,Y),f(Y,Z).
Tale stile di definizione è assimilabile alla definizione per iterazione, in quanto la
valutazione è puramente iterativa; in un linguaggio convenzionale avremmo:
fi(X): while ~c(X) do X:= h(X);
g(X)
Esecuzione di fi
L’esecuzione di fi può avvenire utilizzando, in una macchina astratta a stack, una singola
cella dello stack in cui viene mantenuto il valore della variabile X. Ad ogni passo viene
aggiunto allo stack il record d’attivazione per il calcolo della funzione h. Tale record può
essere immediatamente rimosso dallo stack non appena h(X) è stata calcolata.
La valutazione di fi avviene allora in spazio costante, indipendente dal valore di X.
Esecuzione di f
Nel caso di ricorsione la regola generale dice che la valutazione richiede l’uso dello stack
ed in particolare il caricamento in cima allo stack di una copia del record d’attivazione
della funzione per ogni chiamata della f stessa. Se la funzione è definita con ricorsione tail
l’uso dello stack è inutile.
Vediamo gli stati assunti dallo stack durante il calcolo di
f(X): if X = 0 then g(X) else f(X – 1)
per f(2)
f(2):
[<X=2,…>]
f
durante la valutazione di f(2), f viene chiamata sul valore 1, un nuovo record di attivazione
per f deve essere caricato in cima allo stack
f(1):
[<X=1,…>, <X=2,…>]
f
f
la nuova chiamata ricorsiva di f sul valore 0 comporta il caricamento di un ulteriore record
di attivazione
f(0):
[<X=0,…>,<X=1,…>, <X=2,…>]
f
f
f
finalmente la condizione X=0 è soddisfatta e quindi il caso terminale può essere attivato
chiamando la g(0)
g(0):
[<X=0,…>,<X=0,…>,<X=1,…>, <X=2,…>]
g
f
f
f
All’uscita delle varie chiamate ricorsive, i record d’attivazione vengono rimossi. Dapprima
viene rimosso il record per il calcolo di g(0), quindi quello per f(0) e così via fino a quello
per f(2).
64
L’esecuzione di f, definita con ricorsione tail, richiede pertanto uno spazio lineare nel
numero delle chiamate, ossia in X. Ma tutto ciò è inutile, poiché nella ricorsione tail la
chiamata ricorsiva è quella più esterna e sul risultato di tale chiamata non deve essere
eseguita alcuna operazione; l’uso dello stack sarebbe necessario proprio per effettuare tali
operazioni. In altri termini, una funzione definta per ricorsione tail potrebbe essere valutata
in spazio costante mediante un processo iterativo.
Si dice proprio ottimizzazione della ricorsione tail la seguente operazione:
valutare una funzione tail ricorsiva f mediante un processo iterativo, caricando un solo
record d’attivazione per f sullo stack di esecuzione.
In generale, un record d’attivazione per f, tail ricorsiva, può essere rimosso non appena la
funzione h è sta calcolata e può essere sostituito dal record d’attivazione per la chiamata
successiva della f stessa.
Il vantaggio della ottimizzazione della ricorsione tail è il seguente: invece di dover
utilizzare uno spazio proporzionale al numero delle chiamate ricorsive , è sufficiente uno
spazio costante (quello necessario per il calcolo del caso terminale della ricorsione).
Risultano inoltre semplificate le operazioni di gestione a tempo d’esecuzione dello stack e
quindi risulta pure ridotto il tempo d’esecuzione.
In Prolog la valutazione di un goal è fatta in modo simile a quello di una procedura,
eventualmente ricorsiva, in un linguaggio convenzionale.
Si è visto come con la macchina a due stack sia possibile dar conto dell’esecuzione di un
goal.
Nello stack di esecuzione vengono caricati i record d’attivazione dei singoli goal atomici,
contenenti le variabili ed il punto di ritorno. Un predicato definito ricorsivamente
comporterà un uso intensivo dello stack di esecuzione. È però possibile anche in Prolog,
nel caso di ricorsione tail, operare l’ottimizzazione della ricorsione tail trasformando il
processo tail ricorsivo in un processo di valutazione iterativo che in sostanza non utilizza
ulteriormente lo stack.
Tutto ciò è possibile quando la chiamata ricorsiva di un atomo sia fatta nell’ultima clausola
che definisce la relazione in questione, e questo sia l’ultimo atomo del corpo della clausola.
Si consideri ad esempio:
p(X):-c1(X), g(X).
a :p(X):-c2(X), h1(X, Y), p(Y).
b :p(X):-c3(X), h2(X, Y), p(Y).
Effettuare l’ottimizzazione della ricorsione tail per questo programma risulta difficile : vi
sono infatti due possibilità nella valutazione ricorsiva di un goal ?-p(Z).
Se viene scelta la a: si deve ricordare che b: è ancora una scelta possibile, se invece viene
scelta la b:, ossia l’ultima clausola della procedura, allora non è più necessario mantenere
le informazioni sulle scelte aperte, non ve ne sono infatti più e la rimozione di tale record
può essere fatta senza problemi.
Allora l’ottimizzazione della ricorsione tail è possibile solo se non vi sono alternative che
potrebbero essere scelte in caso di backtracking. Se queste ci sono, non possono essere
perse. Se tali alternative non ci sono, l’ottimizzazione può essere operata.
Quasi tutti gli interpreti del Prolog operano tale ottimizzazione, quando è possibile.
Cercheremo dunque di dare un’idea di come rendere tail ricorsivo un dato programma.
65
Da Ricorsione Non Tail a Ricorsione Tail
La ricorsione tail è senz’altro la forma più semplice di ricorsione, per contro si veda la
definizione non tail del fattoriale o delle relazioni pari e dispari mutuamente ricorsive. In
assenza di ricorsione tail non può essere effettuata l’ottimizzazione e quindi occorre un uso
intensivo dello stack. Per certe categorie di definizioni ricorsive non tail possono essere
applicate delle trasformazioni che portano ad una forma tail ricorsiva.
Sia dato il seguente schema di definizione non tail ricorsiva:
(NT)
f(X): if c(X) then g(X) else k(X, f(h(X)))
ovvero in Prolog
(NT)
f(X, Y):-c(X), !,g(X, Y).
f(X, Y) :-h(X, Y1), f(Y1, Y2), k(X, Y2, Y).
Rientra in questo schema la seguente definizione della funzione fattoriale:
(NTF)
fatt(X): if X = 0 the 1 else X*fatt(pred(X))
ovvero in Prolog
fatt(0, 1).
fatt(X, Y):-X>0, X1 is X – 1, fatt(X1, Y1), Y is X*Y1.
Consideriamo il goal ?-fatt(2, Y).
e l’evoluzione degli stati dello stack d’esecuzione con i record d’attivazione degli atomi
fatt(…)
S1)
[<fatt(2,Y), Y=..>]
durante la valutazione di fatt(2, Y), fatt viene ricorsivamente invocata con il valore 1 del
primo parametro, si ha così un nuovo record d’attivazione per fatt(1, Y1)
S2)
[<fatt(1,Y1) (Y is 2*Y1), Y1=..>,<fatt(2,Y), Y=..>]
e ancora
S3)
[<fatt(0,Y2)(Y1 is 1*Y2), Y2=..>,<fatt(1,Y1)(Y is 2*Y1), Y1=..>,<fatt(2,Y), Y=..>]
a questo punto fatt(0, Y2) può essere calcolata, ottenendo Y2 = 1 e la rimozione del
corrispondente record d’attivazione, così pervenendo, come punto di ritorno, alla
valutazione di Y1 is 1*1; tale valutazione ha luogo, ossia il goal Y1 is 1*1 ha successo, ed
ha pure successo il goal fatt(1, Y1). Si perviene pertanto al punto di ritorno Y is 2*1, che
istanzia Y a 2 ed al successo di fatt(2, Y).
È evidente che la necessità di dovere valutare il prodotto, ossia la funzione k, dopo la
chiamata ricorsiva, rende necessario il mantenimento del record d’attivazione sullo stack:
devono infatti essere mantenuti i legami per le variabili.
Un tale tipo di valutazione richiede uno spazio proporzionale al numero di chiamate
ricorsive, ossia al valore del primo parametro di fatt.
Analizziamo ora la seguente definizione:
66
(TR)
fatt(N, Y):-fatt1(N, 1, 1, Y).
fatt1(N, C, Acc, Acc):-C > N, !.
fatt1(N, C, Acc, Y):-Acc1 is C*Acc, C1 is C+1, fatt1(N, C1, Acc1, Y).
Intanto fatt1 è un predicato tail ricorsivo ed il quarto parametro è proprio il fattoriale del
primo, come è giustificato dalle seguenti considerazioni:
il primo argomento di fatt1 è il valore di cui si vuole calcolare il fattoriale;
il secondo, inizializzato ad 1 e incrementato ad ogni passo, determina lo stato di
fine calcolo, esso è pertanto un contatore;
il terzo è utilizzato come “accumulatore”, è inizializzato ad 1 e ad ogni passo è
moltiplicato per il valore corrente del contatore;
il quarto inizialmente è e rimane una variabile fino al momento in cui viene
unificato con il terzo argomento, ossia l’accumulatore, quando la ricorsione ha
termine.
Per le versioni (NTF) e (TR) avremo:
fatt(N) = N*fatt(N-1)
= N*((N -1)*fatt(N-2))
= ….
= N*((N -1)*(…1*1)…)
e per la (TR), dove l’indice di Acc è il contatore,
Acc0 = 1 (inizializzazione)
Acc1 = 1* Acc 0 = 1*1
Acc2 = 2* Acc 1 = 2*(1*1)
:
AccN = N* Acc N-1 = N*((N-1)*…*(2*(1*1))…)
Ma la versione (TR) è tail ricorsiva e quindi ottimizzabile.
La trasformazione impiegata è pertanto generalizzabile nel modo seguente.
Caso Generale
(NT1)
f(N): if N = 0 then a else k(N, f(N -1))
ovvero in Prolog
(NT1)
f(0, a).
f(N, Y):-N1 is N-1, f(N1, Y1), k(N, Y1, Y). {o Y is k(N, Y1)}
dove f viene calcolata come
f(N) = k(N, f(N-1))
= k(N, k(N -1, f(N-2)))
=…
= k(N, k(N -1, k(N-2,...k(1, a)…)))
La trasformazione di (NT1) per ottenere uno schema iterativo è la seguente
67
(IT1)
f(N, Y):-f(N, 1, a, Y).
f(N, C, Acc, Acc) :-C > N, !.
f(N, C, Acc, Y) :-k(C, Acc, Acc1), C1 is C+1, f(N, C1, Acc1, Y).
Per (IT1) si ha :
Acc0 = a (inizializzazione)
Acc1 = k(1, Acc0) = k(1, a)
Acc2 = k(2, Acc1) = k(2, k(1, a))
:
AccN = k(N, AccN-1) = k(N, k(N-1,…,k(2, k(1, a))…)
Allora i programmi (NT1) e (IT1) risultano equivalenti.
Ancora un esempio numerico.
Sia
N
f(N) =
∑ g (i )
1
la sua definizione ricorsiva è immediata:
f(0, 0):-!.
f(N, Y):-N1 is N-1, f(N1, Y1), g(N, Z), Y is Y1+Z.
ovvero
f(0, 0):-!.
f(N, Y):-N1 is N-1, f(N1, Y1), gs(N, Y1, Y).
gs(N, Y1, Y):-g(N, Z), Y is Y1+Z.
che è un’istanza dello schema (NT1) con a = 0 e k(N, Y1) = Y1+g(N); applicando la
trasformazione (IT1) si ottiene
f(N, Y):-f(N, 1, 0, Y).
f(N, C, Acc, Acc):-C>N, !.
f(N, C, Acc, Y):-g(C, Acc, Acc1), C1 is C+1, f(N, C1, Acc1, Y).
Si osservi poi che se la funzione k è associativa e commutativa è possibile definire un
ulteriore schema semplificato:
(IT2)
f(N, Y):-f(N, a, Y).
f(0, Acc, Acc):-!.
f(C, Acc, Y):-k(C, Acc, Acc1), C1 is C-1, f(C1, Acc, Y).
alla prima attivazione si ha il primo argomento unificato con N, tale valore verrà
poi diminuito fino ad ottenere 0;
il secondo argomento è l’accumulatore, l’applicazione di k è sul valore del primo
argomento, ovvero in modo invertito rispetto allo schema IT1, stanti le ipotesi su k;
il terzo argomento viene unificato con l’accumulatore al termine del calcolo
Avremo allora
Acc0 = a
Acc1 = k(N, Acc0) = k(N, a)
68
Acc2 = k(N-1, Acc1) = k(N-1, k(N, a))
:
AccN = k(1, AccN-1) = k(1, k(2,…,k(N-1, k(N,a))…))
Per cui se k è associativa e commutativa i tre schemi (NT1), (IT1) e (IT2) sono equivalenti.
La funzione fatt può essere trasformata con (IT2) in quanto il prodotto è associativo e
commutativo:
fatt2(N, Y):-fatt2(N, 1, Y).
fatt2(0, Acc, Acc) :- !.
fatt2(C, Acc, Y):-Acc1 is Acc*C, C1 is C-1, fatt2(C1, Acc1, Y).
Le Liste
Le liste costituiscono un sottoinsieme dei termini predefiniti in Prolog e sono così definite:
la costante (atomo in Prolog) [] è una lista che è vuota;
il termine .(H, T) è una lista se H è un termine e T è una lista. H è detto la testa
della lista e T la coda.
Tale definizione può essere data in Prolog, tramite la relazione is_list:
is_list([]).
is_list(.(H, T)):-is_list(T).
per cui
?-is_list(.(a,.(b,[])) vale e
?-is_list(.(a,b))
non vale.
In Prolog è consentita la notazione (abbreviata) seguente:
[H|T]
[a]
[a, b]
[a1,.., ak,…,an]
[a1,.., ak|[ak+1…,an]]
per
per
per
per
per
.(H, T),
[a|[]]
ovvero per .(a, []),
[a|[b]]
ovvero per .(a, .(b, [])) ed in generale
[a 1|[a2,…,ak,…,an]] oppure
[a 1|[a2,…,ak,…,an]]
Le usuali operazioni di selezione di elementi in una data lita, sono realizzate unificando la
lista con un’ altra lista contenente opportune variabili.
Dato P:
list([1,2,3,…,9]).
si ha
?-list([X|Xs])
X = 1 {testa}
Xs = [2,…,9] {coda}
Yes
e
69
?-list([_,X2|Xs])
X2 = 2
Xs = [3,…,9]
Yes
dove “_ “ è la variabile anonima, di cui non si richiede l’istanza.
Alcune Operazioni sulle Liste e Loro Trasformazione in Schema Iterativo
Lunghezza:
length([], 0).
length([H|T], N):-length(T,N1), N is N1+1.
Questa definizione rientra nello schema (NT). La si può trasformare in una definizione,
secondo lo schema (IT), ora (ITL), usando un accumulatore che verrà inizializzato a zero e
unificato col risultato quando la lista è vuota:
length1(L, N):-length1(L, 0, N).
lenght1([X|Xs], Acc, N):-Acc1 is Acc+1, length1(Xs, acc1, N).
length1([],Acc, Acc).
In generale, se si ha:
(NTL)
p([], a).
p([H|T], Y):-p(T, Y1), k(H, Y1, Y).
potrà essere trasformata in
(ITL)
p1(L, Y):-p1(L, a, Y).
p1([], Acc, Acc) :- !.
p1([H|T], Acc, Y):-k(H, Acc, Acc1), p1(T, Acc1, Y).
Appartenenza (verifica via unificazione)
member(X, [X|Xs]).
member(X, [Y|Xs]):-member(X, Xs).
Concatenazione di due liste
append([], Ys, Ys).
append([X|Xs], Ys, [X|Zs]):-append(Xs, Ys, Zs).
Esempi di inveritibilità:
?-append(Xs, [2,3], [1,a,2,3]).
Xs = [1,a]
Yes
?-append(Xs, Ys, [1,2]).
Xs = []
Ys = [1,2];
Xs = [1]
Ys = [2];
70
Xs = [1,2]
Ys = [];
no
?-append(Xs, [a], Zs).
Xs = []
Zs = [a];
Xs = [_..]
Zs = [_..,a];
Xs = [_..,_..]
Zs = [_..,_..,a] ;
:
Inversione di una lista
Si vuole definire la relazione binaria che vale fra una lista e la sua versione invertita, sia
questa reverse(L, Lr).
L’idea della definizione è quella per casi:
- [] è l’inversa di []
- se L = [H|T] allora la sua inversa è la concatenazione dell’inversa di T con
la lista costituita solo da H.
Allora
reverse([], []).
reverse([H|T], Lr):-reverse(T, Tr), append(Tr, [H], Lr).
Si vede che questa definizione segue lo schema (NTL).
Possiamo però trasformarla in una versione secondo loschema (ITL):
reverse(L, Lr):-reverse1(L, [], Lr).
reverse1([], Acc, Acc).
reverse1([H|T], Acc, Lr):-append([H], Acc, Acc1), reverse1(T, Acc1, Lr).
Ma l’uso di append è superfluo, infatti l’ultima clausola può (deve !) essere sostituita da
reverse1([H|T], Acc, Lr):-reverse1(T, [H|Acc], Lr).
che corrisponde all’idea di scorrere la lista, depositando gli elementi via via incontrati, uno
sull’altro, nella lista inizialmente vuota e che, al termine vale il risultato cercato.
Ordinamenti di liste
La prima versione intuitiva ed ingenua può essere quella di generare via via permutazioni
della data lista fino a che quella ordinata non sia stata trovata.
%%% Permutation Sort %%%
sort([], []).
sort([X|Xs], S):-permutation([X|Xs], S), sorted(S).
%% Permutazione di Xs : o Xs è vuota allora [] è l’unica permutazione
%% altrimenti si seleziona un elemento in Xs e lo si mette in testa ad una
%% permutazione della lista rimanente
permutation([], []).
71
permutation(Xs, [Z|Zs]):- select(Z, Xs, Ys), permutation(Ys, Zs).
select(X, [X|Xs], Xs].
select(X, [Y|Xs], [Y|Zs]):-select(X, Xs, Zs).
sorted([X]).
sorted([X1,X2|Xs]):-X1=<X2, sorted([X2|Xs]).
%%% Insertion Sort %%%
sort([], []).
sort([X|Xs], Ys):- sort(Xs, Zs), insert(X, Zs, Ys).
insert(X, [], [X]).
insert(X, [Y|Ys], [Y|Zs]):- X>Y, !, insert(X, Ys, Zs).
insert(X, [Y|Ys], [X,Y|Ys]).
%%% Quick Sort %%%
sort([], []).
sort([X|Xs], S) :-partition(Xs, X, Low, Great), {X è detto ”pivot”}
sort(Low, Ls), sort(Great, Gs),
append(Ls, [X|Gs], S).
partition([X|Xs], Y, [X|Los], Grs):-X=<Y, !, partition(Xs, Y, Los, Grs).
partition([X|Xs], Y, Los, [X|Grs]):- partition(Xs, Y, Los, Grs).
partition([], Y, [], []).
Le Liste Differenza
Nonostante la grande versatilità di questa struttura di dati, le liste presentano alcune
inefficienze riguardo al tempo di esecuzione di alcune procedure.
Ad esempio, la concatenazione di due liste richiede un tempo proporzionale alla lunghezza
della lista sulla quale si opera la ricorsione (il parametro ricorsivo).
L’uso di una particolare tecnica di rappresentazione delle liste consente di superare talune
inefficienze. Tale tecnica prende il nome di lista differenza( o differenza di liste). L’idea
base per tale rappresentazione è la seguente:
una lista L può essere rappresentata come la differenza, solamente indicata, fra due liste L1
e L2, così la lista [a,b,c] può essere “considerata” come la differenza fra due liste come:
[a,b,c] = [a,b,c] – [],
[a,b,c] = [a,b,c,d] – [d],
[a,b,c] = [a,b,c,d,e] – [d,e] ossia in generale può essere rappresentata da
[a,b,c] = [a,b,c|L] – L, dove L è una variabile.
La proprietà interessante di questa rappresentazione è quella di poter rappresentare una
lista come una struttura di dati parzialmente specificata. È proprio tale indeterminazione
che consente di ottenere maggiore efficienza nella esecuzione di alcune procedure.
Definizione Compatibilità
Due liste differenza X = X1 – X2 e Y = Y1 – Y2 si dicono compatibili sse X2 unifica con
Y1, ossia, alla Prolog, X2 = Y1.
72
Se due liste X = X1 – X2 e Y = Y1 – Y2 sono compatibili la loro concatenazione C si
ottiene in un solo passo:
C = X1 – Y2
come illustrato nella seguente figura:
X
X2 o Y1
---------------------|----------|-------Y
Y2
C
X1
Se il sottraendo è non specificato si raggiunge, ovviamente , la proprietà di compatibilità.
Infatti si abbia:
L1 = [1,2,3|A] –A e L2 = [4,5|B] – B , queste sono liste differenza compatibili e la loro
concatenazione è data da
C = [1,2,3|[4,5|B]] – B = [1,2,3,4,5|B] – B.
Allora la concatenazione fra liste differenza, che va bene nel caso di compatibilità, è
definita dalla relazione ap_d:
ap_d(X1 – X2, X2 – X3, X1 – X3).
Esempio sull’Uso delle Liste Differenza
Inversione di una lista tramite l’uso della sua rappresentazione come lista differenza
Considerando la relazione di inversione già vista, possiamo definire l’inversione con la
nuova relazione reverse_d:
reverse(L, Lr):-reverse_d(L, Lr-[]).
reverse_d([], Lr-Lr).
reverse_d([H|T], L1-L2):-reverse_d(T, L1-[H|L2]).
Lo schema della definizione di reverse_d è (ITL), l’uso delle liste differenza implica l’uso
implicito dell’accumulatore che è il sottraendo della lista differenza che inizialmente è la
lista vuota. Il minuendo di tale lista è una variabile che al termine della scansione della
data lista si istanzia con la lista invertita.
Quick Sort con Liste Differenza
Ripensando al quick sort e all’uso di append nella sua definizione, si può rendere più
efficiente quella definizione usando le liste differenza. Definiamo quindi la nuova
versione:
sort(L, S):-qs_d(L, S-[]).
Supponiamo che le liste ordinate vengano rappresentate da liste differenza nel modo
seguente:
la lista Ls degli elementi minori o uguali al pivot, già ordinata, è rappresentata da
L1 – Y1,
la lista Gs degli elementi maggiori del pivot, già ordinata, è rappresentata dalla lista
L2 – Y2
73
Poiché l’obiettivo è quello di concatenare Ls e [X|Gs], si devono concatenare le due liste
differenza L1 – Y1 e [X|L2] – Y2.
Si deve pertanto mantenere la compatibilità fra le due liste, ossia deve essere soddisfatta la
relazione di unificabilità Y1 = [X|L2]. In tal caso la concatenazione è data semplicemente
da L1 – Y2. Tale condizione è verificata imponendo che il risultato dell’ordinamento di
Low sia L1 – [X|L2]. Si ha perciò:
qs_d([], L – L).
qs_d([X|Xs], L1 – Y2) :-partition(Xs, X, Low, Gr),
qs_d(Low, L1 – [X|L2]),
qs_d(Gr, L2 – Y2).
I Termini: loro Ispezione e Manipolazione
Operatori e Definizione di Alcune loro Proprietà
In Prolog i termini composti sono utilizzabili esattamente come definiti in un linguaggio
della logica dei predicati del primo ordine, ossia come un operatore seguito da una coppia
di parentesi tonde all’interno delle quali si trovano gli argomenti, ricorsivamente ancora
termini, separati da virgole. Questa è l’usuale rappresentazione prefissa. Quando si voglia
utilizzare, per gli operatori binari o unari, una posizione diversa da quella prefissa, come in
“X mul s(0)” o in “b fat”, dove mul e fat sono gli operatori, il Prolog mette a disposizione
un predicato, “op”, che consente di definire di volta in volta tali caratteristiche per gli
operatori che l’utente desidera utilizzare in modo non standard. Per utilizzare tali forme
rendendo non ambigua la rappresentazione, occorre specificare opportune regole di
associatività e mutue precedenze. Così scrivendo “X mul b fat” con l’idea che il termine
rappresentato sia “mul(X, fat(b))” e non “fat(mul(X, b))”, occorre che fat leghi più
strettamente di mul, abbia cioè una precedenza maggiore di quella di mul. Si noti poi che
stabilire una certa associatività di un operatore significa stabilire una precedenza di un
operatore con sé stesso. Diciamo subito che in Prolog la relazione fra mul e fat vista sopra,
viene espressa come “mul ha priorità maggiore di quella di fat”, ossia la proprietà viene
espressa in modo inverso rispetto all’usuale.
Per tutti gli usuali operatori aritmetici, +, *, - , e / la regola adottata dal Prolog è quella di
associatività a sinistra, così 5+3+2 corrisponde a (5+3)+2 ossia al termine +(+(5,3),2), la
medesima cosa vale per gli operatori con la medesima precedenza come in 5 + 3 – 2 che
corrisponde a (5+3) – 2; per altri operatori viene impedita qualsiasi associatività come in
5=2=X, ossia questo non è un termine sintatticamente corretto.
In definitiva un operatore (unario o binario) è caratterizzato da :
nome
numero di argomenti (uno o due)
priorità (precedenza rispetto agli altri operatori), espressa tramite un numero intero
associatività:
o non associativo
o associativo a sinistra o a destra
Con queste caratteristiche è possibile associare ad una espressione esattamente un albero,
ossia un termine; in particolare si può dare la seguente definizione ricorsiva della
trsformazione di un’espressione in un albero:
data l’espressione costituita dal solo atomo “a”, l’albero corrispondente ha il solo
nodo “a“;
data (E) il suo albero è quello di E;
74
data
E1 op1 E2 op2…Ei-1 opi-1 Ei opi Ei+1…opn-1 En
dove ciascuna Ej è
un’esprtessione atomica o un’espressione fra parentesi o è vuota (alcuni operatori
possono essere unari), il suo operatore principale, opi, viene così selezionato:
o sia “op” l’operatore, fra quelli presenti, a massima priorità (quello che lega
meno fra tutti),
o se “op” è associativo a sinistra opi è l’occorrenza più a destra di op
nell’espressione,
o se “op” è associativo a destra opi è l’occorrenza più a sinistra di op
nell’espressione,
o se “op” non è associativo allora deve esistere una sola occorrenza di op,
altrimenti “errore”
si costruisce allora l’albero, ossia il termine opi(a1, a2), dove
a1 è l’albero ottenuto dalla trasformazione di E1 op1 E2 op2…Ei-1 opi-1 Ei e a 2 dalla
trasformazione di Ei+1…opn-1 En
Esempio
Data l’espressione 2*3+5+4*8*2, con +>*, avremo il termine +(+(*(2,3),5), *(*(4,8),2))
ossia l’albero
+
+
*
*
5 * 2
2
3 4 8
Come sopra accennato in Prolog è disponile il predicato “op” atto a definire le
caratteristiche di un operatore:
op(P, A, Nome)
dove
o Nome è un atomo alfanumerico con primo carattere alfabetico, o una lista di atomi
o P è un numero, in generale tra 0 e 1200, che specifica la priorità dell’operatore
o A specifica tanto il numero degli argomenti quanto la regola di associatività ed è
fx o fy per operatori unari prefissi
xf o yf per operatori unari postfissi
xfx, yfx, xfy
per operatori binari, dove “f” indica la posizione
dell’operatore, “x” indica un argomento che è un atomo o un’espressione
fra parentesi o un’espressione il cui operatore principale ha priorità
strettamente minore di quella dell’operatore che si sta definendo, “y” indica
un argomento che è un atomo o un’espressione fra parentesi o
un’espressione il cui operatore principale ha priorità minore o uguale a
quella dell’operatore che si sta definendo; così
xfx
indica un operatore non associativo
yfx
indica un operatore associativo a sinistra e
xfy
indica un operatore associativo a destra
Una possibile precedenza fra gli operatori usata dal sistema Prolog, è data dalla tabella
op(1200, xfx, [:-, →]).
op(1100, fx, [:-, ?-]).
op(500, yfx, [+, −]).
op(500, fx, [+, −]).
op(400, yfx, [*, /, //]).
op(300, xfx, mod).
op(200, xfy, ^).
75
Quando si voglia usare una data specifica di operatori nel corso dell’esecuzione di un
programma, si utilizza il predicato op(…) sotto forma di direttiva, ossia preceduto dal
simbolo “:-“; tale direttiva (goal) viene direttamente valutata al momento della
consultazione del programma. Attenzione: tali direttive possono anche ridefinire quelle di
default del sistema.
Ispezione/Manipolazione di Termini
I termini sono l’unica struttura di dati utilizzabile in Prolog. Per la loro manipolazione è
conveniente avere a disposizione certi predicati predefiniti. Non li esporremo tutti, pertanto
si consiglia di completarne la conoscenza consultando un manuale Prolog, ad es. [2].
Relazioni fra Termini
Abbiamo già visto le relzioni numeriche, ci soffermeremo ora sulle relazioni di
unificazione e uguaglianza tra termini.
T1 = T2 verifica se T1 e T2 sono unificabili, viene generata la sostituzione
mgu
altrimenti si ha fallimento;
T1 = = T2 verifica se T1 e T2 sono identici. Nessuna sostituzione viene generata;
T1 \= = T2 verifica se T1 e T2 non sono identici.
Ricordiamo che nel Prolog è definito un ordinamento totale dei termini (vedi [2]).
Verifica del Tipo di un Termine
Sono disponibili i seguenti predicati per la determinazione della natura di un termine T: se
T è una variabile o un atomo etc.
atom(T)
verifica se T è un atomo (ossia una costante) non numerica;
number(T)
verifica se T è un numero (intero o reale);
integer(T)
verifica se T è un numero intero;
atomic(T)
verifica se T è un atomo oppure un numero;
var(T)
verifica se T è una variabile attualmente non istanziata
nonvar(T)
verifica se T non è una variabile
compound(T) verifica se T è un termine composto
Tutti questi predicati verificano una proprietà del termine che è rappresentato da T al
momento della valutazione.
Predicati di Selezione
Questi predicati consentono la selezione di componenti di un termine e, viceversa, la loro
ricostruzione a partire da certe componenti.
functor(T, F, N)
consente di determinare l’operatore principale (funtore
principale) di un termine: il termine T ha per operatore principale F ed N
argomenti; vi sono quattro usi principali di functor:
a) se tutti gli argomenti sono istanziati, viene effettuata una verifica che F sia
l’operatore di T che ha N argomenti;
b) se solo T è istanziato, allora F ed N vengono opportunamente istanziati;
c) se T è una variabile ed F ed N sono istanziati (F un atomo e N un intero),
allora T viene istanziata al termine che ha F per operatore principale e N
argomenti come altrettante variabili;
d) se T e almeno uno fra F e N sono istanziati, l’argomento variabile viene a
sua volta istanziato.
Così si ha:
76
?-functor(f(2,3), f, 2).
Yes
?-functor(k, F, N).
F=k
N=0
Yes
?-functor(T, f, 3).
T = f(_.., _.., _..)
Yes
arg(N, T, A)
A viene unificata con l’N-mo argomento del termine T. Il
primo argomento, N, deve essere sempre istanziato ad una costante intera. Vi sono
tre usi fondamentali di arg:
a) se T ed A sono istanziati, viene effettuata la relativa verfica: l’N-mo
argomento di T deve unificare con A;
b) se T è istanziata e A è variabile, A viene unificata con l’N-mo argomento di
T: si ha allora la selezione di un argomento;
c) se A è istanziata l’argomento N-mo di T viene unificato con A: inserimento
di un argomento in T.
Ad esempio:
?-arg(2, f(a, b), b).
Yes
?-arg(2, f(a, Y), b).
Y=b
Yes
?-arg(1, f(a, b), A).
A=a
Yes
?-arg(3, f(a, b, c(X)), A), arg(1, A, A1).
A = c(_1)
A1 = _1
Yes
T =.. [F|As]
Il predicato =.. consente di ottenere, dato il termine T, il suo
operatore principale, F, e la lista, As, dei suoi argomenti. Se entrambi gli
argomenti di =.. sono istanziati, si ha una verifica. Altrimenti:
a) se T è istanziato ed il secondo parametro è una variabile, questa viene
unificata con la lista che ha l’operatore principale di T come primo
elemento e la lista dei suoi argomenti come coda;
b) se T è una variabile ed il secondo una lista con un operatore come testa, e n
successivi termini come coda, T viene istanziato al termine che ha per
funtore la testa della lista e per argomenti gli n elementi della coda.
Ad esempio:
77
?-f(a, b) =.. [f, a, b].
Yes
?-f(a, b) =.. [F|As].
F=f
{è una sostituzione?}
As = [a, b]
Yes
?-T =.. [f, X, 2].
T = f(_.., 2)
Yes
Il predicato name(X, Y) converte nomi di costanti a stringhe di caratteri, ovvero liste
dei corrispondenti codici ASCII e viceversa: il goal ?-name(X, Y) ha successo se X è
un atomo e Y è la lista dei codici ASCII corrispondenti, per esempio il goal
?-name(log, [108,111,103]) ha successo.
Alcune Relazioni per la Manipolazione di Termini
Ricordiamo che dato un termine t = f(t1,…,tn), una posizione o occorrenza in t è una
sequenza, diciamo una lista, di numeri naturali maggiori di zero eventualmente vuota,
[n1,…,nk], tale che:
t | [] = t
f(t1,…, tk,…, tn) | [k|Ps] = t k| Ps
è la definizione della funzione binaria T|P di selezione del sottotermine di T alla posizione
P.
Riportiamo di seguito alcune relazioni di particolare ineteresse nella manipolazione di
termini.
Sottotermine ad una data posizione
“se subterm(Sub, Term, O) vale allora Sub è il sottotermine di Term alla posizione O”
subterm(Term,Term,[]).
subterm(Sub,Term,O) :compound(Term), functor(Term,F,N), subterm1(N,Sub,Term,O).
subterm1(N,Sub,Term,O) :N > 1, N1 is N-1, subterm1(N1,Sub,Term,O).
subterm1(N,Sub,Term,[N|O]) :arg(N,Term,Arg), subterm(Sub,Arg,O).
Varie altre letture, sono ovviamente possibili, ad esempio determinare la (le) posizioni in
cui un termine, Sub, figura in un termine Term, etc.
Sostituzione di un termine in un altro ad una data occorrenza
“Se substo(New, Term, Term1, O) vale allora Term1 è il termine Term in cui il
sottotermine alla posizione O è New”
78
constant(X) :integer(X).
constant(X) :atom(X).
substo(New,Old1,New,[]):-!.
substo(New,Term,Term1,[I|Is]) :compound(Term),
substo(I,New,Term,Term1,Is),!.
substo(N,New,Term,Term1,Is) :arg(N,Term,Arg), substo(New,Arg,Arg1,Is),Term=..[F|As],
elem(N,As,Arg1,As1),Term1=..[F|As1].
elem(1,[A|As],B,[B|As]):-!.
elem(N,[A|As],B,[A|Bs]):-N>0,N1 is N-1,elem(N1,As,B,Bs).
Applicazione di una sostituzione [X/T] ad un termine Told
ap_subst([X/T],Told,Tnew):-findall(P,(subterm(S1,Told,P),S1==X),Ps),
substo_list(T,Told,Tnew,Ps).
substo_list(T,Tnew,Tnew,[]).
substo_list(T,Told,Tnew,[P|Ps]):-substo(T,Told,Tn,P),
substo_list(T,Tn,Tnew,Ps).
Una Realizzazione della Relazione di Riscrittura
Si ricorda che dato un sistema di riscrittura R, i termini s e t stanno nella relazione di
riscrittura definita da R, s →R t, se esiste una posizione p di s ed una regola di R, l → r ,
tali che s|p= lµ e t = s[rµ]p, ovvero t è il termine s il cui sottotermine alla posizione p è stato
rimpiazzato da rµ, dove µ è la sostituzione matching fra s| p e l.
Pensando di mantenere il sistema di riscrittura nel programma in cui definiremo la
relazione di riscrittura, possiamo rappresentarlo come un insieme di clausole unitarie,
ciascuna della forma “rule(L, R)”, corrispondente alla regola l → r del dato sistema.
Ciò consente l’accesso a ciascuna regola semplicemente valutando il goal ?-rule(L, R).
Per far le cose veloci, supponiamo di voler determinare il matching solo con s ground; ciò
ci consente di utilizzare l’unificazione del Prolog in luogo della vera relazione di matching
(provate a realizzarla, vedi in [4] la definizione e l’algoritmo per il matching).
Se rewrite è il nome della relazione di riscrittura, avremo allora:
rewrite(S, T):- matching(S,P,R),
substo(R, S, T, P).
matching(S,P,R):- subterm(Sub, S, P), rule(L, R),
Sub = L. % allora tutte le variabili di L (e R!)si istanziano opportunamente
La sua chiusura riflessiva e transitiva, it_rewrite, sarà allora:
it_rewrite(S, T):-rewrite(S, T1), it_rewrite(T1, T).
it_rewrite(S, S):-not(matching(S,_,_)).
così se nel medesimo programma esistono i seguenti fatti
rule(X+0, 0).
rule(X+s(Y), s(X+Y)).
79
si avrà:
?-it_rewrite(s(0)+s(s(0)), S).
S = s(s(s(0)))
Yes
Ispezione/Manipolazione di un Programma
Daremo, anche per questo gruppo di predicati detti di metalivello, le idee essenziali con
qualche esempio di utilizzazione, si veda comunque [2].
Visto che in Prolog un programma è costituito da clausole definite che sintatticamente
hanno la medesima struttura dei termini e l’unica possibilità di manipolazione è data
dall’unificazione, si puo pensare di ispezionare e manipolare le clausole del programma
stesso come dei termini, durante la valutazione di opportuni goal. Esamineremo dunque tali
possibilità date dai predicati di sistema clause, assert e retract.
In effetti il Prolog riguarda una clausola unitaria h. come la formula h: - true, dove true è il
predicato costante usuale, che scritta secondo la sintassi dei termini standard in forma
prefissa, assume la forma :-(h, true); analogamente una clausola nella forma di regola come
h:-b1, b2,…,bn è il “termine” :- (h,”,”( b1, “,”( b2,…”,”(bn-1, bn)…))), dove il corpo della
regola è la “composizione” dei suoi goal atomici, data dall’operatore binario “,” (virgola).
Tale composizione è descritta anche con il termine (b1, b2,…,bn) o semplicemente
b1, b2,…,bn in forma infissa, con “,” associativo a destra. Alla stessa maniera la clausola
goal ?- b1, b2,…,bn. viene vista come, ovvero è il termine ?-( b1, b2,…,bn), con funtore “?-“.
Il Predicato Clause di Ispezione del Programma
Il predicato
clause(H, B)
vale se :-(H, B) è una clausola del corrente programma,
ovvero se H e B unificano con la testa ed il corpo di un clausola del programma.
H deve essere istanziata ad un termine non numerico e B può essere una variabile o un
termine che rappresenta il corpo della clausola.
Sia dato il seguente programma P:
q(2, Y):-d(Y).
q(X, a):-p(X), r(a).
p(1).
Si ha allora
?-clause(q(X, Y), B).
X=2
Y = _1
B = d(_1);
X = _2
Y=a
B = p(_2), r(a);
No
e
?-clause(H, true).
No
80
Modifica del Programma(Database)
Si può avere la modifica dinamica del programma, sostanzialmente aggiungendo o
togliendo clausole dal programma.
Aggiunta: Assert
Con il predicato
assert(C)
la clausola C viene aggiunta al programma, C deve
ovviamente essere un termine che denota una clausola.
Con il predicato
asserta(C)
la clausola C viene aggiunta all’inizio del programma
e con il predicato
assertz(C)
la clausola C viene aggiunta alla fine del programma.
Rimozione: Retract
Con il predicato
retract(C)
la prima clausola del programma che unifica con C
viene rimossa, ovviamente C non può essere una variabile.
Con il predicato
abolish(N/A)
tutte le clausole che definiscono la relazione di
nome N con A argomenti, vengono rimosse, N ed A devono essere istanziati. Vedi anche
retractall(C) che elimina tutte le clausole che unificano con C.
Un metainterprete del Prolog in Prolog
Vediamo come possa essere definito in Prolog un metainterprete che opera con la ricerca
depth-first, ossia come quello built-in. Usa anche l’unificazione del Prolog, potrebbero
pertanto essere pensati altri metainterpreti con caratteristiche diverse. Il nome del
metainterprete è solve e si applica ad un goal, atomico o composto.
solve(true).
solve((A, B)):-solve(A), solve(B).
solve(A):-clause(A, B), solve(B).
Nella seguente relazione solve viene costruito, come secondo parametro, il termine
(albero) di dimostrazione del dato goal, primo argomento:
solve(true, true).
solve((A, B), (PA, PB)):-solve(A, PA), solve(B, PB).
solve(A, (A ← PB)):-clause(A, B), solve(B, PB).
Dato il programma
p:-q, r.
q:-s.
r.
s.
si ha
?-solve(p, P).
P = p ← (q ← (s ← true)), (r ← true)
Yes
Nel prossimo paragrafo verrà pure esemplificato l’uso del predicato assert.
Le Grammatiche a Clausole Definite (Definite Clause Grammars)
Si considera il problema di voler definire una grammatica context-free e disporre di un suo
analizzatore top-down in maniera diretta in Prolog. Questo in effetti viene reso
81
direttamente disponibile agli utenti del Prolog, vedi DCG nel manuale del sistema in uso.
Vediamo come il tutto sia possibile.
Data la produzione
<frase>→<frase_nominale><frase_verbale>
possiamo costruire una clausola che definisce la relazione “frase” unaria sulle liste
(rappresentazione di stringhe) di terminali che è verificata per ogni lista corrispondente ad
una stringa appartenente al linguaggio <frase> generato dalla grammatica contenente le
opportune altre produzioni:
frase(L):- append(FN, FV, L), frase_nominale(FN),
frase_verbale(FV).
Si noti l’uso di append che realizza in questo caso, essendo nel modo -, -, +, lo split della
lista L, in backtracking, fino a che i successivi goal non siano verificati.
Analogamente dovranno essere definite le relazioni “frase_nominale” e “frase_verbale”.
Le produzioni che definiscono le stringhe di terminali possono essere direttamente
realizzate da clausole unitarie del tipo:
articolo([il]).
articolo([un]).
nome([cane]).
:
Un passo decisivo nell’efficienza utilizzando tali relazioni viene compiuto eliminando il
goal append(…) ed usando le liste differenza. Avremo perciò:
frase(S-S0):-frase_nominale(S-S1), frase_verbale(S1-S0).
con il significato:
S è la lista che rappresenta la stringa contenente quella che deve
essere riconosciuta, a partire dall’inizio, e S0 una sua opportuna coda in
modo tale che S -S0, la differenza fra S e S0, verifica frase se esiste una
opportuna coda S1 tale che la differenza S -S1 verifica frase_nominale e
la differenza S1-S0 verifica frase_verbale.
Se si hanno anche le seguenti clausole, ossia una grammatica a clausole definite (DCG),
frase_nominale(S-S0):-articolo(S-S1), nome(S1-S2), aggettivo(S2-S0).
frase_nominale(S-S0):-articolo(S-S1), nome(S1-S0).
frase_verbale(S-S0):-verbo(S-S1), frase_nominale(S1-S0).
frase_verbale(S-S0):-verbo(S-S0).
articolo([il|S]-S).
articolo([un|S]-S).
nome([cane|S]-S).
nome([gatto|S]-S).
verbo([mangia|S]-S).
verbo([ama|S]-S).
aggettivo([bello|S]-S).
aggettivo([cattivo|S]-S).
82
data la frase S = [il, cane, cattivo, mangia, il, gatto] si ha
?- S = [il, cane, cattivo, mangia, il, gatto], frase(S-[]).
S = [il, cane, cattivo, mangia, il, gatto]
Yes
grazie alla seguente derivazione
?-frase([il, cane, cattivo, mangia, il, gatto] – [])
| {S1/[il, cane,…,gatto], S01/[ ]}
? -frase_nominale([il, cane,…, gatto]-S11),frase_verbale(S11-[])
| {S2/[il, cane,…,gatto], S02/ S11}
?-articolo([il, cane,…, gatto]-S12),nome(S12, S22),aggettivo(S22-S11),frase_verbale(S11-[])
| {S12/[cane,…,gatto]}
? -nome([cane,…, gatto]-S22), aggettivo(S22-S11),frase_verbale(S11-[])
| {S22/[cattivo,…,gatto]}
? -aggettivo([cattivo,…,gatto]-S11),frase_verbale(S11-[])
| {S11/[mangia,…,gatto]}
? -frase_verbale([mangia, il, gatto]-[])
| {S3/[mangia, il, gatto], S03/[ ]}
? -verbo([mangia, il, gatto]-S13), frase_nominale(S13-[])
| {S13/[il, gatto]}
? -frase_nominale([il, gatto]-[])
| {S4/[il, gatto], S04/[ ]}
? -articolo([il, gatto]-S14), nome(S14-[])
| {S14/[gatto]}
? -nome([gatto]-[])
| {S5/[ ]}
Alcune Possibili Estensioni
Possiamo estendere la definizione delle DCG, inserendo opportuni ulteriori parametri oltre
alla lista differenza. Possiamo così ottenere direttamente l’albero d’analisi, man mano che
la lista viene riconosciuta, utilizzando il corrispondente argomento come in:
frase(frase(FN, FV), S -S0):-frase_nominale(FN, S-S1), frase_verbale(FV, S1-S0).
dove il termine “frase(FN, FN)”, primo argomento di frase, rappresenta l’albero d’analisi
della frase individuata da S-S0, essendo FN e FV gli alberi d’analisi della frase_nominale
e verbale rispettivamente. Analogamente avremo:
frase_nominale(frase_nominale(A, N, AG), S-S0):-articolo(A, S-S1),
nome(N, S1-S2),
aggettivo(AG, S2-S0).
:
articolo(articolo(il), [il|S]-S).
:
nome(nome(cane), [cane|S]-S).
:
Una ulteriore estensione riguarda l’accordo in numero fra soggetto e verbo e/o articolonome-aggettivo, che rende le DCG più ampie delle grammatiche context -free.
83
Tale accordo può essere realizzato con l’aggiunta di un argomento, Num, a frase_nominale
e frase_verbale, ottenendo:
frase(frase(FN, FV),S-S0):-frase_nominale(FN, S-S1, Num),
frase_verbale(FV, S1 -S0, Num).
Evidentemente, perché il goal ?-frase_nominale(...,Num), frase_verbale(…, Num). sia
verificato occorre che esista una istanza comune di Num, ossia che il soggetto ed il verbo
abbiano il medesimo numero (singolare o plurale). Come detto la verifica di tale accordo
va oltre le potenzialità di una grammatica context-free. Così avremo
frase_nominale(frase_nominale(A, N, AG), S-S0, Num):-articolo(A, S-S1, Num),
nome(N, S1-S2, Num),
aggettivo(AG, S2-S0, Num).
e
articolo(articolo(il), [il|S] -S, singolare).
articolo(articolo(i), [i|S] -S, plurale).
e
frase_verbale(frase_verbale(V), S-S0, Num):-verbo(V, S-S0, Num).
verbo(verbo(mangia), [mangia|S] -S, singolare).
verbo(verbo(m angiano), [mangiano|S]-S, plurale).
Un traduttore da Produzioni a DCG
Riportiamo un programma logico che definisce la relazione translate(P, C), che data la
produzione P costruisce la clausola corrispondente. Si assume che le produzioni siano
termini della forma L → R (con → infisso) ed L atomo e R composizione di atomi e liste
di terminali come in “articolo → [il]” o “espressione → termine,[+],espressione”.
translate((Lhs -> Rhs),(Head:-Body)):translate(Lhs,Head,Xs-Ys),translate(Rhs,Body,Xs-Ys).
translate((A,B),(A1,B1),Xs-Ys):translate(A,A1,Xs-Xs1),translate(B,B1,Xs1-Ys).
translate(A,A1,S):non_terminal(A),functor(A1,A,1),arg(1,A1,S).
translate(Xs,true,S):terminals(Xs),
sequence(Xs,S).
non_terminal(A):-atom(A).
terminals([X|Xs]).
sequence([X|Xs],[X|S]-S0):-sequence(Xs,S-S0).
sequence([],Xs-Xs).
Si noti che nel membro destro di una produzione possono figurare stringhe ovvero liste di
terminali, nella traduzione tale caso è trattato dalla clausola “translate(Xs, true, S)” che
inserisce nel corpo della clausola l’atomo true al posto dei terminali e trasferisce
opportunemente tale lista di terminali nel terzo parametro. Avremo allora, ad esempio, la
seguente derivazione:
84
?-sequence([il], L)
| {L/[il|S1]-S01}
?-sequence([], S1-S01)
| {S1/S01}
con l’istanza di L alla lista differenza [il|S0 1]-S01.
Disponendo di un tale traduttore ed avendo le produzioni presenti nel programma sotto la
forma di fatti del tipo, come visto sopra, “ L → R”, possiamo allora generare le calusole
corrispondenti a ciascuna produzione e farle diventare parte del programma, asserendole;
si ottiene in questo modo la DCG corrispondente alle date produzioni. Il tutto è ottenuto
valutando il seguente predicato “dcg” che itera la translate su ciascuna produzione:
dcg:-(L->R),translate((L->R),(H:-B)),assertz((H:-B)),fail.
dcg.
Si potrà, infine, procedere al riconoscimento di una data lista di terminali, E, invocando il
seguente predicato “rec”:
rec(E):-E=..[F,L],E1=..[F,L-[]],call(E1). %Es. E=esp([a,+,b])
I Predicati di I/O
Il sistema mette a disposizione vari predicati per la trasmissione di informazioni: dal livello
della trsmissione dei caratteri a quello dei termini. Raccomandiamo per quanto riguarda i
predicati alivello dei caratteri di consultare il manuale del linguaggio in uso.
Il principale effetto di questi predicati, se verificati, è quello di produrre effetti collaterali.
I predicati a livello dei termini sono:
read(X)
che unifica, se possibile, il termine X con il termine letto dal
mezzo d’ingresso corrente, di solito la keyboard; usualmente
X è una variabile;
write(X) che scrive sul mezzo corrente d’uscita, di solito il video, il termine X;
nl
che scrive una linea vuota;
writeln(X) che scrive il termine X su una linea.
I mezzi correnti di I/O possono essere definiti dall’utente tramite l’uso di opportuni
predicati (see, tell etc. vedi manuale o help in linea).
Eseguendo il goal ?-read(X). il sistema scrive il prompt “|:” di seguito al quale l’utente
dovrà scrivere il termine che deve unificare con X seguito da “.” (punto).
Così si avrà ad esempio
?-read(X).
|:f(b).
X = f(b)
Yes
85
Testi di Riferimento
[1] V. Sperschneider, G. Antoniou – Logic a Foundation for Computer Science,
Addison Wesley, 1991
[2] L. Console, E. Lamma, P. Mello, M. Milano - Programmazione Logica e Prolog,
Utet Libreria, 1997
[3] J.W. Lloyd – Foundation of Logic Programming, Second Extended Edition
Springer-Verlag, 1987
[4] G. Aguzzi - Logica Equazionale e Sistemi di Riscrittura, Appunti del Corso di
Metodi per il Trattamento dell’Informazione, Fac. di SMFN,
Università di Firenze
L’indirizzo del sito SWI-Prolog, da cui si può scaricare un compilatore Prolog free è:
http://www.swi-prolog.org/
86