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