Salta al contenuto
Note per Studenti Prolog - fatti, regole, ricorsione, liste e cut

Prolog - fatti, regole, ricorsione, liste e cut

In questa pagina 11

Argomento del corso da 9 CFU (laboratorio 4). Fonte: slide del laboratorio di Intelligenza Artificiale, Ing. dell'Informazione UniPD, con le query provate su SWISH. Teoria alla base: Logica del primo ordine - unificazione, forward e backward chainingLogica del primo ordine (oggetti, relazioni, funzioni, quantificatori e errori tipici), sostituzioni, istanziazione universale ed esistenziale (Skolem), proposizionalizzazione e suoi limiti, unificazione con occur check, risoluzione nel primo ordine con conversione in CNF, esempio Criminal(West), clausole definite, forward e backward chaining, ingegneria della conoscenza.Logica del primo ordine - unificazione, forward e backward chaining →. I risultati delle query e le tracce sono stati verificati con un piccolo interprete scritto in Python (unificazione, ricerca in profondità, cut, is).

Cos'è Prolog

Prolog (PROgrammation en LOGique) è un linguaggio dichiarativo: si specifica cosa si vuole, non come ottenerlo; ci pensa il motore di inferenza.

  • Approssima la logica del primo ordine ed è basato sulla dimostrazione automatica di teoremi; un programma è un insieme di clausole di Horn (disgiunzione di letterali con al più un letterale positivo).
  • Manipola simboli, non numeri.
  • Nato tra fine anni '60 e inizio anni '70 (1972 viene indicato come data di nascita) a Marsiglia e Edimburgo; primo compilatore nel 1977; standard ISO nel 1995. Usi: ragionamento a regole, linguaggio naturale, sistemi esperti, basi di dati con query complesse, didattica della logica.
  • Implementazione usata: SWI-Prolog; SWISH è l'ambiente web in cui si scrive il programma (a sinistra) e si lanciano le query.

Va pensato in modo dichiarativo, non procedurale, ma conta come si scrive la conoscenza: l'ordine delle clausole e dei sotto-obiettivi cambia il comportamento (vedi più sotto).

Anatomia di un programma

Un programma Prolog è un insieme di clausole di Horn che rappresentano:

  • fatti su oggetti e relazioni;
  • regole (se... allora...) che definiscono oggetti e relazioni;
  • goal (obiettivi): clausole senza testa, ciò che l'utente vuole sapere.

Ogni clausola finisce con un punto.

prolog
% regola: due persone sono colleghe se lavorano per la stessa azienda
colleague(X,Y) :- work(X,Z), work(Y,Z).
% fatti
work(emp1, deepmind).
work(emp2, deepmind).
work(emp3, ibm).
work(emp4, ibm).

Nella regola la parte a sinistra di :- è la testa (head), quella a destra il corpo (body). Query (goal), a prompt ?-:

prolog
?- colleague(emp1, emp2).    % true: lavorano entrambi per deepmind
?- colleague(emp1, emp3).    % false

Operatori e traduzione in logica:

Prolog Logica Significato
A :- B B→AB \to A implicazione, "A se B"
A , B A∧BA \land B congiunzione
A ; B A∨BA \lor B disgiunzione
  • Fatto pred(c1,…,cn)pred(c_1,\dots,c_n): se contiene variabili, ad esempio likes(antonio, Thing)., vale per ogni valore: ∀Thing likes(antonio,Thing)\forall Thing\ likes(antonio, Thing). Nell'esempio, Antonio ama qualunque cosa.
  • Regola eats(Person, Thing) :- likes(Person, Thing), food(Thing).: per ogni persona e cosa, la persona mangia la cosa se la ama e la cosa è un cibo. Le variabili che compaiono solo nel corpo sono quantificate esistenzialmente: A(X) :- B(X, Y) significa ∀x (∃y B(x,y))→A(x)\forall x\ (\exists y\ B(x,y)) \to A(x).
  • Query ?- goal(X1,...,Xn).: "esistono valori che rendono il goal derivabile dal programma? Quali?". Prolog trova tutti i valori delle variabili, uno alla volta.
prolog
likes(mary, pizza).
likes(antonio, Thing).
likes(john, icecream).
food(pizza).
food(icecream).
eats(Person, Thing) :- likes(Person, Thing), food(Thing).

?- likes(john, pizza).     % false
?- eats(john, X).          % X = icecream
?- eats(antonio, X).       % X = pizza ; X = icecream

Un fatto è una regola con corpo vero: likes(john, pizza). equivale a likes(john, pizza) :- true. L'insieme dei fatti e delle regole con la stessa testa pred(… )pred(\dots) (stesso nome e stessa arità) è la procedura pred.

Il motore di inferenza

Il motore usa unificazione e risoluzione (in pratica il backward chaining della logica del primo ordine). Esempio dal laboratorio:

prolog
parent(tommaso,francesca).
parent(tommaso,vittorio).
parent(francesca,linda).
parent(vittorio,bianca).
grampa(X,Y) :- parent(X,Z), parent(Z,Y).

Query ?- grampa(tommaso, bianca). Il goal si unifica con la testa della regola: X=tommasoX = tommaso, Y=biancaY = bianca. Il corpo diventa parent(tommaso,Z), parent(Z,bianca).

  1. parent(tommaso,Z): la prima clausola applicabile è parent(tommaso,francesca), quindi Z=francescaZ = francesca (binding, legame).
  2. parent(francesca,bianca): nessun fatto: falso. Prolog fa backtracking: disfa il legame Z=francescaZ = francesca e cerca un'altra scelta per parent(tommaso,Z).
  3. parent(tommaso,vittorio), quindi Z=vittorioZ = vittorio; parent(vittorio,bianca) è un fatto: vero. Risposta true.

Query con variabile ?- grampa(tommaso, Who). Who e Y sono la stessa variabile. Albero di ricerca (search tree, o albero SLD della risoluzione di Prolog: radice il goal, un nodo per ogni scelta di clausola, foglie di successo o fallimento):

?- grampa(tommaso, Who)
 |  clausola 5, {X = tommaso, Y = Who}
 ?- parent(tommaso, Z), parent(Z, Who)
    |- parent(tommaso, francesca)   {Z = francesca}
    |     ?- parent(francesca, Who)
    |          |- parent(francesca, linda)   {Who = linda}   SUCCESSO: Who = linda
    |- parent(tommaso, vittorio)    {Z = vittorio}      (dopo backtracking)
          ?- parent(vittorio, Who)
               |- parent(vittorio, bianca)   {Who = bianca}  SUCCESSO: Who = bianca

SWISH risponde Who = linda e, se si chiede un'altra soluzione (;), Who = bianca. Le due risposte sono le foglie di successo.

Esempio di backtracking con due sotto-obiettivi.

prolog
likes(mary,food).   % clausola 1
likes(mary,wine).   % clausola 2
likes(john,beer).   % clausola 3
likes(john,wine).   % clausola 4

?- likes(mary,X), likes(john,X).     % X = wine

likes(mary,X) dà X=foodX = food (clausola 1); likes(john,food) fallisce, quindi si ritorna indietro e likes(mary,X) dà X=wineX = wine (clausola 2); likes(john,wine) è la clausola 4: successo con X=wineX = wine. In generale, trovato un insieme di legami che soddisfa goal_1, Prolog vi si impegna e prova goal_2; se goal_2 riesce cerca soluzioni extra, se fallisce cerca altri legami per goal_1. In entrambi i casi ritorna indietro.

Strategia di ricerca: in profondità (come in Visite di grafi - BFS e DFSVisite in ampiezza (BFS) e in profondità (DFS) come design pattern; etichette discovery, cross e back edge; BFS tree e distanze; complessità Theta(n+m) con liste di adiacenza; applicazioni (connettività, componenti, spanning tree, cammini minimi non pesati, cicli, vertici a distanza al più d); esempio svolto su un grafo di 7 vertici.Visite di grafi - BFS e DFS →, ma con ritorno all'ultima scelta):

  • le clausole del programma si considerano dall'alto in basso;
  • i sotto-obiettivi nel corpo da sinistra a destra;
  • se una scelta si rivela sbagliata si torna indietro.

La ricerca in profondità non è completa: Prolog può percorrere un ramo infinito senza trovare la soluzione anche se esiste. Contano quindi l'ordine delle clausole e quello dei sotto-obiettivi. Regole pratiche: in una procedura prima i fatti, poi le regole; nel corpo prima gli obiettivi più facili da provare o confutare.

Esempio di ramo infinito. Con gli stessi fatti di parent, aggiunti parent(linda, carlo).:

prolog
anc(X,Y) :- parent(X,Y).
anc(X,Y) :- parent(X,Z), anc(Z,Y).      % ricorsione a destra: OK
?- anc(tommaso, Y).    % francesca, vittorio, linda, carlo, bianca

anc2(X,Y) :- anc2(X,Z), parent(Z,Y).    % ricorsione a SINISTRA: richiama se stessa
anc2(X,Y) :- parent(X,Y).               % subito, senza consumare nulla: ramo infinito

anc2(tommaso, carlo) richiama anc2(tommaso, Z) con gli stessi argomenti, all'infinito (nel nostro interprete, RecursionError; in SWI-Prolog, errore di stack). La versione a destra consuma un parent a ogni chiamata e termina: se il grafo è aciclico si arriva sempre ai fatti.

Per seguire l'esecuzione c'è il comando trace: mostra ogni goal eseguito, se riesce e i passi di backtracking.

Termini

Fatti, regole e query sono fatti di termini.

  • Atomi: valentino, valentino_rossi, valentino46, 'hello world' (con apici). Iniziano per minuscola.
  • Numeri: 12, -34, 345.01, 0.328.
  • Variabili: iniziano per maiuscola o _: X, Who, _variable. Il simbolo _ è la variabile "non mi interessa" (anonima): ogni _ è diverso dagli altri.
  • Termini composti (strutture): un funtore (atomo) applicato ad argomenti, a loro volta termini: book('Le tigri di Mompracem', author(emilio, salgari)), father(father(antonio)), son(X,Y) :- parent(Y,X).

Non c'è differenza sintattica tra fatti, regole, goal e strutture usabili come argomenti: una sequenza di goal è una struttura con , come funtore principale, una clausola una struttura con :-. Prolog è tipizzato dinamicamente; si può interrogare il tipo:

Predicato È vero se il termine è Esempi dalle slide
var(T) una variabile non istanziata var(X) true; X = 5, var(X) false
nonvar(T) non è una variabile libera nonvar(X) false; X = 5, nonvar(X) true
integer(T), float(T) intero, virgola mobile integer(5) true; integer(5.0) false
number(T) intero o float number(5) e number(5.0) true; number(atom) false
atom(T) un atomo (i numeri non sono atomi) atom(hello) e atom('hello world') true; atom('123') true (apici: è testo)
atomic(T) atomo o numero atomic(5) true; atomic(f(x)) false

(Il caso X = Y, var(X), var(Y) è vero: dopo aver unificato due variabili libere restano entrambe libere, ma la stessa.)

Unificazione

L'operatore = significa unifica: due termini unificano se e solo se sono identici o possono essere resi identici legando variabili a termini. Le regole sono quelle viste in Logica del primo ordine - unificazione, forward e backward chainingLogica del primo ordine (oggetti, relazioni, funzioni, quantificatori e errori tipici), sostituzioni, istanziazione universale ed esistenziale (Skolem), proposizionalizzazione e suoi limiti, unificazione con occur check, risoluzione nel primo ordine con conversione in CNF, esempio Criminal(West), clausole definite, forward e backward chaining, ingegneria della conoscenza.Logica del primo ordine - unificazione, forward e backward chaining →:

  • costante con costante: unificano se sono la stessa;
  • variabile con qualsiasi termine: si lega;
  • termine composto con termine composto: stesso funtore, stessa arità e argomenti che unificano a coppie;
  • ogni altro caso fallisce.
prolog
?- tommaso = tommaso.                       % true
?- tommaso = X.                             % X = tommaso
?- grampa(X,Y) = grampa(bianca, Z).         % X = bianca, Y = Z
?- f(X,b) = f(a,Y).                         % X = a, Y = b
?- f(X,X) = f(a,b).                         % false: X non può essere a e b
?- 3+2 = 5.                                 % false: 3+2 è la struttura +(3,2), non il numero 5

Occur check. Si può unificare XX con father(X)father(X)? Richiederebbe X=father(father(father(… )))X = father(father(father(\dots))). Il controllo dice: una variabile si lega a un termine solo se non vi compare. Prolog non lo fa per default (per velocità): X = father(X) "riesce" creando un termine ciclico. Per il controllo si usa unify_with_occurs_check(T1, T2): unify_with_occurs_check(X, father(X)) è false.

Confronti utili: = unifica (può legare variabili); \= riesce se non unificano; == controlla identità senza legare (X == Y è false se sono variabili diverse).

Aritmetica

= non calcola. Per valutare un'espressione aritmetica si usa is:

prolog
?- 3+2 = X.         % X = 3+2        (solo unificazione: nessun calcolo)
?- X is 3+2.        % X = 5
?- 5 is 3+2.        % true
?- X is 3.0 + 2.0.  % X = 5.0
?- 5 is 3 + X.      % errore: Arguments are not sufficiently instantiated

A destra di is l'espressione deve essere completamente istanziata: is non risolve equazioni. I confronti <, >, =<, >=, =:= (uguale come valore), =\= valutano entrambi i lati: 7 =:= 3+4 è true, mentre 7 == 3+4 è false.

Ricorsione

Come in RicorsioneFunzioni che chiamano se stesse: caso base e passo ricorsivo, stack delle chiamate, esempi su numeri, stringhe e liste, ricorsione multipla e suo costo, divide et impera, confronto con l'iterazione.Ricorsione →: un caso base e un passo che riduce il problema. Fattoriale:

prolog
factorial(0,1).
factorial(N,L) :- N>0, N1 is N-1, factorial(N1,L1), L is N*L1.

?- factorial(6,Result).     % Result = 720

Esecuzione di factorial(3,L) (verificata):

  1. 3>0, N1 is 3-1 (N1=2N1 = 2), si chiama factorial(2,L1);
  2. 2>0, N1=1N1 = 1, factorial(1,L2); 1>0, N1=0N1 = 0, factorial(0,L3);
  3. factorial(0,L3) si unifica col fatto: L3=1L3 = 1;
  4. si risale: L2=1⋅1=1L2 = 1 \cdot 1 = 1, L1=2⋅1=2L1 = 2 \cdot 1 = 2, L=3⋅2=6L = 3 \cdot 2 = 6.

Il controllo N>0 serve: senza, quando si chiede un'altra soluzione dopo factorial(0,1) si prova anche la seconda clausola con N=0N = 0, che richiama factorial(-1, ...) e non termina mai. Con N>0 il ramo fallisce subito.

Liste

Una lista è una sequenza finita di elementi: [antonio, vittorio, tommaso], [antonio, 2, 3.0], [antonio, father(antonio), X, Y], [] (vuota), [antonio, [2, [b,c]], f(X), []] (elementi di qualsiasi tipo, anche liste).

Una lista non vuota si divide in testa (head, il primo elemento) e coda (tail, la lista senza il primo). L'operatore | scompone una lista:

prolog
?- [Head|Tail] = [antonio, vittorio, tommaso].
% Head = antonio, Tail = [vittorio, tommaso]

?- [Head|Tail] = [antonio, [2,[b,c]], f(X), []].
% Head = antonio, Tail = [[2,[b,c]], f(X), []]

?- [Testa|Coda] = [].                          % false: la lista vuota non ha testa

?- [One, Two|Remaining] = [antonio, [2,[b,c]], f(X), []].
% One = antonio, Two = [2,[b,c]], Remaining = [f(X), []]

?- [One, Two|Remaining] = [antonio, 2, [b,c], f(X), []].
% One = antonio, Two = 2, Remaining = [[b,c], f(X), []]

La coda è sempre una lista (anche []); la testa può essere qualsiasi termine. [a,b] è la notazione breve di [a|[b|[]]].

length(?List, ?Int) è vero se Int è il numero di elementi:

prolog
?- length([a,b,c], 3).             % true
?- length([], 0).                  % true
?- length([pippo,[a,b]], 3).       % false: ha 2 elementi (la sotto-lista [a,b] conta uno)

append(?List1, ?List2, ?List1AndList2) è vero se la terza lista è la concatenazione delle prime due. Si può usare in tutte le direzioni (i ? indicano che ogni argomento può essere input o output):

prolog
?- append([a,b], [1,2,3], X).               % X = [a,b,1,2,3]
?- append(X, [1,2,3], [a,b,1,2,3]).         % X = [a,b]
?- append(X, Y, [1,2,3]).
% X = [],      Y = [1,2,3] ;
% X = [1],     Y = [2,3]   ;
% X = [1,2],   Y = [3]     ;
% X = [1,2,3], Y = []

L'ultima query elenca le 44 possibili divisioni di una lista di 33 elementi (in generale n+1n+1). La stessa definizione serve per tutti gli usi perché è relazionale:

prolog
append([], L, L).                                % caso base: [] davanti non cambia L
append([H|T], L, [H|R]) :- append(T, L, R).      % la testa H passa nel risultato

Esempio append([a,b],[1],X) (verificato): la regola ricorsiva con H=aH = a chiama append([b],[1],R1), poi con H=bH = b chiama append([],[1],R2), che per il fatto dà R2=[1]R2 = [1]; risalendo R1=[b,1]R1 = [b,1] e X=[a,b,1]X = [a,b,1].

Altre definizioni nello stesso stile, tutte testate sull'interprete: member(X,[X|_]). con member(X,[_|T]) :- member(X,T).; sumlist([],0). con sumlist([H|T],S) :- sumlist(T,S1), S is S1+H. (sumlist([3,5,2],S) dà S = 10); last([X],X). con last([_|T],X) :- last(T,X). In ogni procedura sulle liste: un caso base per [] (o per la lista di un elemento) e un passo che scompone [H|T].

Il cut

Cut (!) è un predicato senza argomenti che controlla il backtracking. Riesce sempre (dal punto di vista dichiarativo equivale a true) ma ha un effetto collaterale: quando viene eseguito, impegna Prolog sulle scelte fatte da quando il goal padre è stato chiamato. Cioè elimina le alternative rimaste aperte da quando il goal padre (quello la cui clausola contiene il cut) si è unificato con la testa della regola, compresa la scelta di usare quella clausola.

Esempio: perché Vittorio non è sano. Programma senza cut:

prolog
antibodies_owner(maria).
dance(linda).   sing(linda).   sing(gianni).
noproblems(vittorio).

healthy(X) :- happy(X).
healthy(X) :- antibodies_owner(X).
happy(X) :- sing(X), dance(X).
happy(X) :- noproblems(X).

?- healthy(X).      % X = linda ; X = vittorio ; X = maria

Ordine di esecuzione: healthy(X) clausola 1 → happy(X) clausola 1: sing(linda), dance(linda) riesce: X=lindaX = linda. Ritorno: sing(gianni), dance(gianni) fallisce. happy(X) clausola 2: noproblems(vittorio): X=vittorioX = vittorio. Poi healthy clausola 2: antibodies_owner(maria): X=mariaX = maria.

Con il cut dopo sing(X):

prolog
happy(X) :- sing(X), !, dance(X).
happy(X) :- noproblems(X).

?- healthy(X).      % X = linda ; X = maria

Con X=lindaX = linda il cut viene eseguito e dopo di esso Prolog non può più ritornare né su sing(X) (niente gianni) né sulla seconda clausola di happy (niente vittorio): happy(X) ha solo la soluzione linda. Resta healthy clausola 2, con maria. Quindi la query aperta healthy(X) non restituisce Vittorio, come dicono le slide. Attenzione però: ?- healthy(vittorio). risponde ancora true (sing(vittorio) fallisce prima del cut, che non viene mai raggiunto, e si passa alla seconda clausola): il cut ha reso il programma non reversibile, cioè dipendente dall'uso che se ne fa.

Cut verde e cut rosso.

  • Cut verde: cambia l'efficienza ma non il significato (con o senza, le soluzioni sono le stesse).
prolog
min(X,Y,X) :- X =< Y, !.
min(X,Y,Y) :- Y < X, !.

Le due condizioni sono già mutuamente esclusive: il cut serve solo a non provare la seconda clausola dopo la prima. min(3,5,M) dà M=3M = 3, min(5,3,M) dà M=3M = 3.

  • Cut rosso: cambia il significato. Esempio, il programma dei sani sopra: healthy(X) perde vittorio. Un altro: si toglie la condizione dalla seconda clausola, contando sul cut.
prolog
min2(X,Y,X) :- X =< Y, !.
min2(X,Y,Y).

min2(3,5,M) e min2(5,3,M) danno ancora il minimo. Ma min2(3,5,5) risponde true (sbagliato): la prima clausola non si unifica con la testa (X = 3, ma terzo argomento 5), il cut non viene raggiunto e la seconda clausola, senza condizione, riesce. Qui il programma funziona solo quando il terzo argomento è libero: ha un significato procedurale, non dichiarativo.

Cut e fail. !, fail fa fallire subito l'obiettivo, senza provare le clausole successive della stessa procedura (che in assenza di cut verrebbero provate). Si usa per le eccezioni: "tutti gli uccelli volano, tranne pinguino e struzzo".

prolog
bird(nightingale).  bird(blackbird).
penguin(king_penguin).  ostrich(camelus_ostrich).

fly(X) :- penguin(X), !, fail.
fly(X) :- ostrich(X), !, fail.
fly(X) :- bird(X).

?- fly(blackbird).         % true
?- fly(king_penguin).      % false
?- fly(camelus_ostrich).   % false
?- fly(X).                 % false   <-- ATTENZIONE

Con fly(king_penguin) la clausola 1 vale (penguin(king_penguin)), il cut elimina la clausola 2 e la 3, fail fa fallire tutto: false. Con la query aperta fly(X) il primo legame è X=kingpenguinX = king_penguin (primo fatto di penguin): cut, fail, e non si prova la clausola 3, quindi la risposta è false anche se nightingale vola. Ancora una volta il programma funziona solo con l'argomento istanziato.

Nota sul programma delle slide: king_penguin e camelus_ostrich non sono dichiarati bird, quindi fly(king_penguin) fallirebbe anche senza le clausole di eccezione (la clausola 3 non avrebbe bird(king_penguin)). Le eccezioni hanno senso, e si vede la differenza, se i pinguini sono anche uccelli, aggiungendo bird(king_penguin). e bird(camelus_ostrich).: i risultati delle query qui sopra restano gli stessi, ma ora è il cut-fail a impedire fly(king_penguin).

Negazione come fallimento

Cut-fail dà una forma di negazione, la negazione come fallimento (negation as failure): not(G) riesce se non si riesce a dimostrare G, e fallisce se G si dimostra (in SWI-Prolog anche \+ G). Si riscrive fly:

prolog
bird(nightingale).  bird(blackbird).
penguin(king_penguin).  ostrich(camelus_ostrich).
fly(X) :- bird(X), not(penguin(X)), not(ostrich(X)).

?- fly(blackbird).         % true
?- fly(king_penguin).      % false
?- fly(camelus_ostrich).   % false
?- fly(X).                 % X = nightingale ; X = blackbird

Il programma è più leggibile e fly(X) ora enumera gli uccelli che volano (verificato). (Come sopra, nelle slide i pinguini non sono bird: aggiungendo bird(king_penguin). e bird(camelus_ostrich). i risultati restano gli stessi e a escluderli sono i due not.) Ma non è la negazione logica: not(G) vuol dire "G non è derivabile dal programma" (ipotesi del mondo chiuso: ciò che non è nel programma è falso). Inoltre l'ordine conta: not va dopo i goal che istanziano le variabili. Con

prolog
bird(nightingale).  bird(king_penguin).  penguin(king_penguin).
fly(X)  :- not(penguin(X)), bird(X).
fly2(X) :- bird(X), not(penguin(X)).

?- fly(X). risponde false: not(penguin(X)) con X libera prova penguin(X), che riesce (X = king_penguin), quindi il not fallisce prima di aver legato X a un uccello. fly2(X) dà X = nightingale.

Prolog e logica

Esempi svolti: Esercizio - Prolog - fatti, regole e alberi SLD (laboratorio 4), Esercizio - Prolog - aritmetica, ricorsione e liste (laboratorio 4), Esercizio - Prolog - cut e negazione come fallimento (laboratorio 4).

Domande d'esame

  1. Risultato di ?- [H|T] = [a]. e di ?- [H|T] = []. Il primo dà H = a, T = [] (la coda di una lista di un elemento è la lista vuota); il secondo false (la lista vuota non ha testa).
  2. X is 3+2, Y = 3+2 cosa dà? E 3+2 = 5? X = 5 (is calcola) e Y = 3+2 (= unifica soltanto, Y è la struttura +(3,2)); 3+2 = 5 è false perché +(3,2) e 5 non unificano.
  3. Con happy(X) :- sing(X), !, dance(X). quali soluzioni dà healthy(X)? E healthy(vittorio)? healthy(X) dà linda e maria; healthy(vittorio) è true perché sing(vittorio) fallisce prima del cut e si prova la seconda clausola di happy. Il cut è rosso.
  4. Perché con le clausole cut-fail ?- fly(X). risponde false anche se nightingale è un uccello? Il primo legame è X = king_penguin; la prima clausola esegue il cut e fail, e il cut impedisce di provare la clausola 3.

Versione ripasso

  • Prolog: dichiarativo, programma = clausole di Horn, base nella logica del primo ordine (Logica del primo ordine - unificazione, forward e backward chainingLogica del primo ordine (oggetti, relazioni, funzioni, quantificatori e errori tipici), sostituzioni, istanziazione universale ed esistenziale (Skolem), proposizionalizzazione e suoi limiti, unificazione con occur check, risoluzione nel primo ordine con conversione in CNF, esempio Criminal(West), clausole definite, forward e backward chaining, ingegneria della conoscenza.Logica del primo ordine - unificazione, forward e backward chaining →). Ogni clausola finisce con .. Fatti, regole (testa :- corpo), goal (?- ...). , = ∧\land, ; = ∨\lor, A :- B = B→AB \to A.
  • Significato logico: fatto con variabili = ∀\forall; variabili solo nel corpo = ∃\exists (A(X) :- B(X,Y) = ∀x (∃y B)→A\forall x\ (\exists y\ B) \to A). Un fatto è una regola con corpo true. Procedura = clausole con stessa testa (nome e arità). Query = ricerca di tutti i valori delle variabili.
  • Motore: backward chaining in profondità: clausole dall'alto in basso, sotto-obiettivi da sinistra a destra, backtracking sulle scelte sbagliate. Non completo: rami infiniti (ricorsione a sinistra anc2(X,Y) :- anc2(X,Z), parent(Z,Y)). Regole: prima i fatti, poi le regole; prima gli obiettivi facili.
  • Esempio grampa(X,Y) :- parent(X,Z), parent(Z,Y): grampa(tommaso,Who) prova Z=francescaZ = francesca (dà Who=lindaWho = linda), poi Z=vittorioZ = vittorio (dà Who=biancaWho = bianca). likes(mary,X), likes(john,X): X=foodX = food fallisce, X=wineX = wine riesce.
  • Termini: atomi (minuscola), numeri, variabili (maiuscola o _), composti funtore(arg,...). Test di tipo: var, nonvar, integer, float, number, atom (i numeri non sono atomi), atomic.
  • Unificazione =: identici o rendibili tali con legami; f(X,X) = f(a,b) false; 3+2 = 5 false. Occur check: Prolog non lo fa; unify_with_occurs_check(X, father(X)) è false. Altri: \= non unifica, == identità senza legare.
  • Aritmetica: X is 3+2 dà 55 (3+2 = X dà X = 3+2); 5 is 3 + X errore, a destra di is tutto istanziato. Confronti <, >, =<, >=, =:=, =\=.
  • Ricorsione: factorial(0,1). factorial(N,L) :- N>0, N1 is N-1, factorial(N1,L1), L is N*L1. factorial(6,R) dà 720720; senza N>0 non termina al backtracking.
  • Liste: [H|T] testa e coda (la coda è una lista); [] non ha testa; [One,Two|Rest]. length(L,N). append([],L,L). append([H|T],L,[H|R]) :- append(T,L,R). Usabile in ogni direzione: append(X,Y,[1,2,3]) dà 44 divisioni.
  • Cut !: riesce sempre, elimina le alternative dal goal padre in poi (anche le altre clausole). Verde: non cambia il significato (min con =< e <). Rosso: lo cambia (healthy(X) con sing(X), !, dance(X) perde vittorio, ma healthy(vittorio) è true; min2(3,5,5) è true per errore).
  • Cut e fail: !, fail fa fallire subito senza provare le clausole successive. fly(X) con cut-fail risponde false perché il primo legame è king_penguin.
  • Negazione come fallimento: not(G) riesce se G non è dimostrabile (mondo chiuso). fly(X) :- bird(X), not(penguin(X)), not(ostrich(X)). dà nightingale e blackbird. not va dopo i goal che istanziano le variabili.
  • Programmi di riferimento:
prolog
grampa(X,Y) :- parent(X,Z), parent(Z,Y).               % Z = francesca poi Z = vittorio
factorial(0,1).
factorial(N,L) :- N>0, N1 is N-1, factorial(N1,L1), L is N*L1.
append([], L, L).
append([H|T], L, [H|R]) :- append(T, L, R).
happy(X) :- sing(X), !, dance(X).                      % cut rosso: perde vittorio
fly(X) :- penguin(X), !, fail.                          % cut-fail: eccezione
fly(X) :- bird(X), not(penguin(X)), not(ostrich(X)).    % negazione come fallimento
  • Tracce verificate: grampa(tommaso,bianca): Z=francescaZ = francesca, parent(francesca,bianca) fallisce, Z=vittorioZ = vittorio, riesce. likes(mary,X), likes(john,X): X=foodX = food fallisce, X=wineX = wine riesce. factorial(3,L): N1=2,1,0N1 = 2, 1, 0, poi L3=1L3 = 1, L2=1L2 = 1, L1=2L1 = 2, L=6L = 6. append([a,b],[1],X): X=[a,b,1]X = [a,b,1].
  • Clausola di Horn: A :- B1,...,Bn = B1∧⋯∧Bn⇒AB_1 \land \dots \land B_n \Rightarrow A; fatto = corpo vuoto. Esecuzione = backward chaining in profondità con unificazione. Albero SLD: radice il goal, un ramo per clausola applicabile, foglie di successo o fallimento.
  • Cut, riassunto: eseguito, impegna sulle scelte da quando il goal padre è stato unificato con la testa (anche sulle clausole successive). Verde = stesse soluzioni; rosso = soluzioni diverse; non reversibile (healthy(X) perde vittorio ma healthy(vittorio) è true).
  • Query sui tipi (dalle slide): var(X) true; X = 5, var(X) false; X = 5, nonvar(X) true; integer(5.0) false; number(5.0) true; atom('hello world') true; atomic(f(x)) false.
  • Query su unificazione e liste: grampa(X,Y) = grampa(bianca,Z) dà X = bianca, Y = Z; [H|T] = [antonio,[2,[b,c]],f(X),[]] dà H = antonio, T = [[2,[b,c]],f(X),[]]; [One,Two|Rest] = [antonio,2,[b,c],f(X),[]] dà One = antonio, Two = 2; length([pippo,[a,b]],3) false; append(X,[1,2,3],[a,b,1,2,3]) dà X = [a,b].
  • Ordine di esecuzione di healthy(X) (senza cut): linda (sing e dance), gianni fallisce su dance, poi vittorio (noproblems), poi maria (antibodies_owner). Con ! dopo sing(X): solo linda e maria.
  • Punto di attenzione: fly(X) con cut-fail risponde false con variabile libera (primo legame king_penguin, cut, fail). Nelle slide i pinguini non sono bird: aggiungendoli le risposte non cambiano.
  • Errori tipici: ricorsione a sinistra; = al posto di is; is con variabile a destra; cut messo male (rosso involontario); not su variabile libera; dimenticare N>0.

Esercizi su questo argomento

Teoria collegata