Le langage Prolog

Transcription

Le langage Prolog
Le langage Prolog
Cours n°3
Notion de coupure
Les listes
Plan du cours
Notion de coupure
Les structures de données
les listes
Notion de coupure (1)
La recherche systématique de toutes les
solutions possibles est une caractéristique
importante de Prolog.
Mais elle constitue également son talon d’Achille
car elle conduit parfois à une explosion
combinatoire qu’il est nécessaire de couper si
l’on veut voir son programme se terminer.
Notion de coupure (2)
Différents noms utilisés: coupure, cut ou coupe
choix
Introduit un contrôle du programmeur sur
l'exécution de ses programmes
en élaguant les branches de l’arbre de recherche
rend les programmes plus simples et efficaces
Notation: ! (ou / dans le prolog de Marseille)
Le coupe choix est un prédicat prédéfini qui
réussit toujours.
Notion de coupure (3)
Le coupe choix permet de signifier à Prolog qu’on
ne désire pas conserver les points de choix en
attente
Utile lors de la présence de clauses exclusives
Exemple les 2 règles suivantes :
humain(X) :- homme(X).
humain(X) :- femme(X).
s'écrit humain(X) :- homme(X), !.
s'écrit humain(X) :- femme(X), !.
Notion de coupure (4)
Le coupe-choix permet :
d'éliminer des points de choix
d'éliminer des tests conditionnels que l’on sait inutiles ⇒
plus d'efficacité lors de l'exécution du programme
Quand Prolog démontre un coupe choix, il ignore
toutes les règles du même prédicat, qui le suivent.
Un coupe choix élague toutes les solutions pour la
conjonction de buts qui apparaissent à sa gauche dans
la règle
En revanche il n’affecte nullement la conjonction de buts
qui se trouve à sa droite
Notion de coupure (5)
Exemples d’utilisation :
Pour prouver que x est un élément d’une liste, on peut s'arrêter
dès que la première occurrence de x a été trouvée.
Pour calculer min(a,b) :
min(A, B, A) :- A<B.
min(A, B, B) :- A >=B.
min(A, B, A) :- A<B, !.
min(A, B, B) :- A >=B.
Pour calculer la racine carrée entière d’un nombre, on utilise un
générateur de nombres entiers entier(k). L’utilisation du coupechoix est alors indispensable après le test K*K>N car la
génération des entiers n’a pas de fin.
entier(0).
entier(N1) :- entier(N), N1 is N+1.
racine(N, R) :- entier(K), K*K >N, ! , R is K-1.
Notion de coupure (6)
Repeat et fail :
Le prédicat fail/0 est un prédicat qui n’est jamais
démontrable, il provoque donc un échec de la
démonstration où il figure.
Le prédicat repeat/0 est une prédicat prédéfini qui est
toujours démontrable mais laisse systématiquement
un point de choix derrière lui. Il a une infinité de
solutions.
Notion de coupure (7)
L’utilisation conjointe de repeat, fail et du coupe
choix permet de réaliser des boucles.
Exemples :
but1:- between(1,10,X), write(X), nl, fail.
but1.
but2:- repeat, between(1,10,X), write(X), nl, fail.
but2.
but3:- repeat, !, between(1,10,X), write(X), nl, fail.
but3.
Notion de coupure (8)
Dangers du coupe-choix : on peut perdre des
solutions
Considérez les programmes suivants :
enfants(helene, 3).
enfants(corinne, 1).
enfants(X, 0).
enfants(helene, 3) :- !.
enfants(corinne,1) :- !.
enfants(X, 0).
et l’interrogation enfant(helene, N).
1) N=3 et N=0
2) N=3
Notion de coupure (fin)
En résumé, les utilités du coupe-choix sont :
éliminer les points de choix menant à des échecs
certains
supprimer certains tests d’exclusion mutuelle dans
les clauses
permettre de n’obtenir que la première solution de
la démonstration
assurer la terminaison de certains programmes
contrôler et diriger la démonstration
Les Listes
A une suite, ordonnée ou non, on associe la liste
de ses éléments.
suite e1, e2, … est représentée en Prolog par la liste
[e1, e2, … ]
Exemples :
suite des variables X et Y est représentée par [X,Y]
suite fraises, tarte, glace par [fraises, tarte, glace]
Liste vide : [ ]
Propriété fondamentale (1)
La notation [X|Y] représente une liste dont la
tête (le 1er élément) est X et la queue (le
reste de la liste) est Y.
Cela constitue la base de l’utilisation des listes
dans les programmes Prolog.
Cas général : [Tete | Queue]
≡ [Car | Cdr ] (cf. Scheme)
[a, b, c] ≡ [a | [b | [c | [ ] ] ] ]
Propriété fondamentale (2)
Exemple de programme menu :
entrees([sardine, pate, melon, avocat]).
viandes([poulet, roti, steack]).
poissons([merlan, colin, loup]).
desserts(gateau, fruit, glace]).
Et si on pose la question :
entrees(E).
L’effacement de ce but (synonyme de question)
provoque l’affectation suivante :
E= [sardine, pate, melon, avocat]
Propriété fondamentale (3)
Nous savons donc affecter une liste à une
variable.
Nous avons aussi besoin de pouvoir considérer
les éléments d’une liste de façon individuelle, par
exemple pour récupérer un élément particulier.
Supposons que l’on dispose du prédicat :
element_de(X, L) capable d’extraire un objet X d’une liste L.
Propriété fondamentale (4)
Nous avions :
menu(X, Y, Z) :- entree(X), plat(Y), dessert(Z).
plat(X) :- viande(X).
plat(X) :- poisson(X).
Avec la nouvelle déclaration commune des entrées
comme liste, ce programme échouera. Il faut donc le
modifier.
Pour cela nous allons construire une nouvelle
définition du prédicat entree (ne pas confondre avec le
prédicat entrees qui définit la liste des entrées).
Propriété fondamentale (5)
On utilise la propriété suivante : si X est une entrée,
alors X est un élément quelconque de la liste des
entrées. On peut donc écrire :
entree(X) :- entrees(E), element-de(X, E).
Et de façon analogue :
viande(X) :- viandes(V), element-de(X, V).
poisson(X) :- poissons(P), element-de(X, P).
dessert(X) :- desserts(D), element-de(X, D).
Unification de listes
[T|Q] = [1, 2, 3, 4].
[X,Y] = [1, 2].
[X,Y] = [1, 2, 3].
[X,Y | Z] = [1, 2, 3].
[X,Y | Z] = [1, 2].
?
?
?
?
?
Unification de listes
[T|Q] = [1, 2, 3, 4]
[X,Y] = [1, 2]
[X,Y] = [1, 2, 3]
[X,Y | Z] = [1, 2, 3]
[X,Y | Z] = [1, 2]
T = 1, Q = [2, 3, 4]
X = 1, Y = 2
No (échec)
X = 1, Y = 2, Z = [3]
X = 1, Y = 2, Z = []
Accès aux éléments d’une liste
Il s’agit de rechercher les règles qui vont assurer le
fonctionnement du prédicat element-de(X, L) qui
signifie que X est l’un des éléments de L.
On utilise la remarque selon laquelle toute liste peut se
décomposer simplement en deux parties, la tête et la
queue de liste. Cela conduit à distinguer deux cas :
X est élément de L si X est la tête de L.
X est élément de L si X est élément de la queue de L.
Ce qui se traduit directement en Prolog par :
(R1)
(R2)
element-de(X, L) :- est-tete(X, L).
element-de(X, L) :- element-de-la-queue(X,L).
R1 et R2 sont là en tant que repères et ne rentrent pas dans les règles.
Accès aux éléments d’une liste
On sait que L peut toujours être représentée sous
la forme [U|Y], ce qui nous donne :
Si X est en tête de [U|Y] alors X = U.
Si X est un élément de la queue de [U|Y] alors X est
élément de Y.
D’où la version :
(R1)
element-de(X, [X|_]).
(R2)
element-de(X, [U|Y]) :- X\=U, element-de(X, Y).
Accès aux éléments d’une liste
(R1)
(R2)
On interprète ces deux règles de la façon
suivante :
element-de(X, [X|_]).
element-de(X, [U|Y]) :- X\=U, element-de(X, Y).
R1 : X est élément de toute liste qui commence par X.
R2 : X est élément de toute liste de queue Y, si la liste
ne commence pas par X.
Il s’agit donc d’un appel récursif. La plupart des
problèmes sur les listes ont des solutions mettant
en jeu la récursivité.
Récursivité
(R1)
(R2)
element-de(X, [X|_]).
element-de(X, [U|Y]) :- X\=U, element-de(X, Y).
On observe deux types de règles et deux types
d'arrêt :
Une ou plusieurs règles provoquent la récursivité,
généralement sur des données assez simples, et assurent
le déroulement de l’itération.
Dans notre exemple, il s’agit de la règle R2.
Une ou plusieurs règles stoppent la séquence d’appels
récursifs. Dans notre exemple, c’est R1 qui s’en charge.
Sauf impératif contraire, les règles d'arrêt sont placées en
tête.
Récursivité
(R1)
(R2)
element-de(X, [X|_]).
element-de(X, [U|Y]) :- X\=U, element-de(X, Y).
Il apparaît deux types d'arrêt :
Un arrêt explicite. Par exemple, dans R1, l'identité
entre l'élément cherché et la tête de la liste fournie, ce
qu’exprime la notation X et [X|Y]
Un arrêt implicite. En particulier par la rencontre d’un
terme impossible à démontrer, par exemple:
element-de(X,[]).
Il existe cependant une forme d’erreur pour laquelle
ces deux blocages se révèlent insuffisants, c’est la
rencontre de listes infinies.
Construction d’une liste (1)
Nous avons vu comment parcourir une liste.
Voyons comment en construire une.
Examinons le problème suivant : L une liste
contenant un nombre pair d'éléments.
L = [0,1,2,3,4,5]
Cherchons à construire une nouvelle liste W en
prenant les éléments de rang impair dans L.
Par exemple :
Dans notre exemple cela donnerait : W = [0,2,4]
Soit elements-rang-impair ce prédicat:
elements-rang-impair([0,1,2,3,4,5], X).
X = [0,2,4]
Construction d’une liste (2)
Relation de récurrence pour l’énumération des éléments de rang
impair :
elements-rang-impair([X1, X2 | Y]) :- elements-rang-impair(Y).
Règle d'arrêt : il faut s'arrêter si la liste vide :
elements-rang-impair([]).
On modifie le programme de façon à construire la nouvelle liste à
partir du parcours de l’ancienne. D’où le programme suivant:
(R1)
(R2)
elements-rang-impair([], []).
elements-rang-impair([X1, X2|Y], [X1|L]) :- elements-rang-impair(Y, L).
Interprétation de ces deux règles :
R1 : prendre un élément sur deux de la liste vide donne la liste vide
R2 : prendre un élément sur deux dans la liste [X1, X2 |Y], donne la liste
[X1|L], si prendre un élément sur deux dans la liste Y donne la liste L.
Construction d’une liste (3)
Ce programme est incomplet :
elements-rang-impair([], []).
elements-rang-impair([X, _|Y], [X|L]) :- elements-rang-impair(Y, L).
Exemples d’exécution :
?- elements-rang-impair([a,b,c,d],L).
L = [a, c]
?- elements-rang-impair([a,b,c],L).
false
Pourquoi cet échec ? :
⇒
Le cas des listes à nombre impair d’éléments n’est pas traité !
Construction d’une liste (4)
D’où le programme final suivant :
elements-rang-impair([], []).
elements-rang-impair([X], [X]).
elements-rang-impair([X, _|Y], [X|L]) :- elements-rang-impair(Y, L).
Règle introduite : si une liste est formée d’un seul élément X, la liste
résultat est cette liste [X].
Exemples d’exécution :
?- elements-rang-impair([a,b,c,d],L).
L = [a, c]
?- elements-rang-impair([a,b,c],L).
L = [a, c]
Remarque : les éléments dans la liste résultat sont rangés dans le même
ordre que dans la liste initiale.
Construction d’une liste (5)
Construction d’une liste au moyen d’une liste
auxiliaire :
Problème : Ecrire un prédicat inverser qui inverse l’ordre
des éléments d’une liste donnée.
Soit D une liste données, R est la liste D inversée.
On est dans une situation différente la précédente à cause de
l’ordre inverse des éléments. Nous allons introduire une liste
auxiliaire pour y ranger les éléments énumérés au fur et à
mesure de leur parcours.
Le prédicat inverser aura donc 3 arguments:
(1) la liste à inverser, (2) la liste auxiliaire, (3) la liste inversée
Construction d’une liste (6)
On obtient les règles suivantes :
inverser([], L, L).
inverser([X|Y], L, R) :- inverser(Y, [X|L], R).
Les 2 premiers arguments sont consultés, le troisième est élaboré
Au lancement, la liste auxiliaire est vide : inverser([a,b,c],[],R).
L’élément qui apparaît en tête de la liste est retiré et placé en tête
de la liste auxiliaire. Il y a donc inversion de l’ordre des éléments
Après le parcours de la liste donnée, L et R sont identiques.
Interprétation de ces deux règles :
Inverser la liste vide, à partir de la liste auxiliaire L, donne la liste
résultat L.
Inverser la liste [X|Y], à partir de la liste auxiliaire L, donne la liste
résultat R, si inverser la liste Y, à partir de la liste auxiliaire [X|L],
donne cette même liste R.
Construction d’une liste (7)
Si l’on souhaite disposer d’un prédicat inverser/2 qui
ne mentionne que les listes donnée et résultat, on peut
introduire le prédicat suivant :
inverser(L,R) :- inverser(L,[],R).
inverser([], L, L).
inverser([X|Y], L, R) :- inverser(Y, [X|L], R).
Exécution :
?- inverser([a,b,c], [], R).
R = [c, b, a]
?- inverser([a,b,c],R).
R = [c, b, a]
Listes et sous-listes
Dans certaines situations, il peut être intéressant de
structurer une liste en sous-listes
Exemple du programme menu :
Volonté de poursuivre le regroupement des données.
Liste unique qui regrouperait tous les mets du menu
L= [sardine, poulet, … , glace]
Cette représentation n’est pas adéquate car il n’y a pas de
distinction entre entrée, plat ou dessert.
Il est donc nécessaire de découper la liste en sous-listes qui
rassembleront les entrées, plats et desserts.
Listes et sous-listes
Cela nous donne la structure suivante :
L = [ [sardines, pate, melon, celeri], [poulet, roti, steack],
[merlan, colin, loup], [fraises, tarte, glace] ]
L est de la forme : [Entrées, Viandes, Poissons, Desserts]
La sous-liste des plats est elle-même divisée en
deux parties, les viandes et les poissons :
L = [ [sardines, pate, melon, celeri],
[ [poulet, roti, steack], [merlan, colin, loup] ],
[fraises, tarte, glace] ]
L est de la forme : [Entrées, Plats, Desserts]
Parcours d’une liste
Cas de base :
parcours([],v). v = valeur associée à la liste vide
Cas général :
Parcours([T|Q],R):- parcours(Q,RI), R is f(RI,T).
R est calculé à partir de RI et de T
Exemple : somme des éléments d’une liste
somme([], 0).
somme([T|Q], S) :- somme(Q, Sq), S is T + Sq.
Construction d’une liste à partir
d’une liste
Généralisation de l’exemple de l’inversion d’une liste
codeverslettres([],[]).
codeverslettres([Code|L],[Lettre|R]):- name(Lettre,[Code]),
codeverslettres(L,R).
L’ordre des éléments de la liste produite correspond à l’ordre
des éléments de la liste de départ.
Construction d’une liste à partir d’une
liste : solution avec « accumulateur »
Principe: la liste est construite pendant les appels récursifs. Il
s’agit d’introduire un argument supplémentaire correspondant
à une liste « accumulatrice »
Nous avons déjà utilisé ce principe de construction pour
inverser une liste.
à l’appel initial, la liste accumulatrice vaut la liste vide: []
à chaque appel récursif on ajoute des éléments à la liste
construite
en fin de parcours, la liste construite (« accumulée ») constitue le
résultat du calcul.
Construction d’une liste à partir d’une
liste : solution avec « accumulateur »
Exemple : construire le prédicat chaine_invers_liste qui
transforme une chaine de caractères en sa liste de lettres
inversée.
?- chaine_invers_liste("hello", L).
L = [o, l, l, e, h].
Rappel: une chaine entre " est une liste de codes ascii :
?- L="hello".
L = [104, 101, 108, 108, 111]
Prolog fournit le prédicat name/2 qui convertit une chaine (liste de
codes) en atome ou inversement un atome en une liste de codes :
?- name(hello,L).
L = [104, 101, 108, 108, 111].
?- name(L,"hello").
L = hello.
Construction d’une liste à partir d’une
liste : solution avec « accumulateur »
Nous allons utiliser
Rappel: une chaine entre " est une liste de codes ascii :
chaine_invers_liste(CH,L):- chaine_invers_liste(LC,[],L).
chaine_invers_liste([],LAcc,LAcc).
chaine_invers_liste([Code|L],LAcc,R):- name(Lettre,[Code]),
chaine_invers_liste (L,[Lettre|LAcc],R).
Grâce à la liste auxiliaire, l’ordre des lettres est inversé
Quelques prédicats prédéfinis
sur les listes
Dans la documentation Prolog :
- les arguments consultés sont précédés d'un +
- les arguments élaborés sont précédés d'un - les arguments qui peuvent être soit consultés,
soit élaborés (suivant le sens de l'unification)
sont précédées d'un ?
Exemple : numlist(+Min, +Max, -List).
?- numlist(2,8, L).
L = [2, 3, 4, 5, 6, 7, 8]
Concaténation : append
append est le prédicat prédéfini pour la concaténation de
listes : append(?List1, ?List2, ?List3).
?- append([a,b,c],[d,e],L).
L = [a, b, c, d, e]
append est complètement symétrique et peut donc être
utilisé pour d’autres opérations :
Trouver tous les découpages d’une liste en 2 sous-listes:
?- append(L1,L2,[a,b,c,d]).
L1 = [], L2 = [a, b, c, d] ;
L1 = [a], L2 = [b, c, d] ;
L1 = [a, b], L2 = [c, d] ;
L1 = [a, b, c], L2 = [d] ;
L1 = [a, b, c, d], L2 = []
append :
autres utilisations possibles
Trouver le dernier élément d’une liste :
?- append(_,[X],[a,b,c,d]).
X=d
Couper une liste en sous-listes :
?- append(L2,L3,[b,c,a,d,e]), append(L1,[a],L2).
L2 = [b, c, a]
L3 = [d, e]
L1 = [b, c]
Quelques prédicats prédéfinis sur les
ensembles (listes sans doublons)
list_to_set(+List, -Set).
transforme List en un ensemble Set (suppr doublons)
subset(+Subset, +Set).
Subset ⊂ Set
intersection(+Set1, +Set2, -Set3).
Set3 = Set1 ∩ Set2
union(+Set1, +Set2, -Set3).
Set3 = Set1 ∪ Set2
subtract(+Set, +Delete, -Rest).
Rest = Set - Delete
merge_set(+Set1, +Set2, -Set3).
Set3 = fusion de Set1 et Set2
(Set1, Set2 et Set3 sont des ensembles triés)
sort(+List, -SetTrié).
transforme List en un ensemble trié SetTrié
maplist : application d’un prédicat à tous les
éléments d’une liste
Les prédicats maplist appliquent un prédicat sur tous les membres
d'une liste ou jusqu'à ce que le prédicat échoue.
maplist(+Pred,+List).
Pred est d’arité 1
?- maplist( integer, [15,8,5,6,9,4] ).
true
maplist(+Pred, ?List1, ?List2).
Pred est d’arité 2
maplist( <(3), [15,8,5,6,9,4] ).
true
?- maplist(double, [3,4,6], L).
L = [6, 8, 12]
avec :
double(X,D):- D is X*2.
maplist(+Pred, ?List1, ?List2, ?List3).
Pred est d’arité 3
maplist(plus, [0, 1, 2], [4, 8, 16], L).
X = [4, 9, 18]
avec :
plus(?Int1, ?Int2, ?Int3) Int3 = Int2 + Int1
Liste des solutions d'un prédicat
Les prédicats findall/3, bagof/3 et setof/3 permettent
de faire la liste des solutions d'un prédicat.
findall(+Motif, +But, -Liste).
bagof(+Motif, +But, -Liste).
Trouve toutes les solutions de But et en fait une Liste selon le
modèle Motif
Trouve toutes les solutions de But (par groupes), échoue si
But n'a pas de solution
setof(+Motif, +But, -Liste).
Trouve toutes les solutions de But (par groupes), mais
supprime les doublons ; échoue si But n'a pas de solution
findall, bagof, setof : exemples (1)
Soit le programme suivant :
?- findall(C, foo(A,B,C), L).
L = [c, f, d, f, f, e, g]
?- bagof(C, foo(A,B,C), L).
A = a, B = b, L = [c, f, d, f] ;
A = b, B = c, L = [f, e] ;
A = c, B = c, L = [g]
les variables A et B sont
unifiées. On a 3 solutions:
une pour chaque couple
(A,B) disctinct.
?- setof(C, foo(A,B,C), L).
A = a, B = b, L = [c, d, f] ;
A = b, B = c, L = [e, f] ;
A = c, B = c, L = [g]
L est ordonnée
foo(a, b, c).
foo(a, b, f).
foo(a, b, d).
foo(a, b, f).
foo(b, c, f).
foo(b, c, e).
foo(c, c, g).
findall, bagof, setof : exemples (2)
?- bagof(C, A^foo(A,B,C), L).
B = b, L = [c, f, d, f] ;
B = c, L = [f, e, g] ;
?- setof(C, A^foo(A,B,C), L).
B = b, L = [c, d, f] ;
A^ signifie “ne pas unifier A”
seule la variable B est
unifiée, la variable A
restant libre. On a 2
solutions: une par valeur
de B différente.
B = c, L = [e, f, g] ;
?- bagof(C, A^B^foo(A,B,C), L).
L = [c, f, d, f, f, e, g]
?- setof(C, A^B^foo(A,B,C), L).
L = [c, d, e, f, g]
les variables A et B
ne sont pas unifiées.
On n'a alors plus
qu'une solution.
(~ findall)
foo(a, b, c).
foo(a, b, f).
foo(a, b, d).
foo(a, b, f).
foo(b, c, f).
foo(b, c, e).
foo(c, c, g).

Documents pareils

Le langage Prolog - --- programmation en logique

Le langage Prolog - --- programmation en logique Une variable (ou inconnue) peut remplacer n’importe quel terme Prolog. variable instanciée à un terme : la variable a pour valeur ce terme. variable libre : la variable n’est instanciée à aucun...

Plus en détail