Vous êtes sur la page 1sur 92

Programmation logique

Michel Gagnon
École Polytechnique de Montréal

1er février 2016

Table des matières


1 Introduction à la programmation logique 3
1.1 Concepts de base . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 Definition de prédicats . . . . . . . . . . . . . . . . . . . . . . . 4
1.3 Requêtes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4 Clauses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.5 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9

2 Variables et unification (pour extraires les informations) 10


2.1 Algorithme d’unification . . . . . . . . . . . . . . . . . . . . . . 10
2.2 Extraction de l’information . . . . . . . . . . . . . . . . . . . . . 12
2.3 Variables anonymes . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.4 Opérateur d’unification . . . . . . . . . . . . . . . . . . . . . . . 14
2.5 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

3 Algorithme d’exécution d’un programme 16


3.1 Algorithme de résolution . . . . . . . . . . . . . . . . . . . . . . 16
3.2 Exemple d’exécution . . . . . . . . . . . . . . . . . . . . . . . . 17
3.3 Retour arrière avec plusieurs points de choix . . . . . . . . . . . . 20
3.4 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22

4 Opérateurs arithmétiques 23
4.1 Opérateurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.2 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

1
5 Listes 25
5.1 Définitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
5.2 Unification de listes . . . . . . . . . . . . . . . . . . . . . . . . . 26
5.3 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28

6 Processus récursif 28
6.1 Definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
6.2 Vérification de l’appartenance à une liste . . . . . . . . . . . . . . 29
6.3 Concaténation de listes . . . . . . . . . . . . . . . . . . . . . . . 34
6.4 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37

7 Interprétations déclarative et procédurale 41

8 Construction de nouvelles structures 44


8.1 Appariement . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
8.2 Utilisation d’un accumulateur : . . . . . . . . . . . . . . . . . . . 46
8.3 Autres exemples : . . . . . . . . . . . . . . . . . . . . . . . . . . 47
8.4 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49

9 Ordre des clauses 52

10 Manipulation dynamique de programmes 65

11 Recupération de solutions multiples 67


11.1 Utilisation du retour arrière . . . . . . . . . . . . . . . . . . . . . 67
11.2 Prédicat findall . . . . . . . . . . . . . . . . . . . . . . . . . 68
11.3 Prédicats bagof et setof . . . . . . . . . . . . . . . . . . . . . 70
11.4 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72

12 Mécanismes de contrôle 72
12.1 Coupe-choix : . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
12.2 Dangers avec le coupe-choix . . . . . . . . . . . . . . . . . . . . 81
12.3 Négation : . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
12.4 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83

13 Quelques prédicats prédéfinis 85


13.1 Vérification du type d’un terme . . . . . . . . . . . . . . . . . . . 85
13.2 Comparaison et unification de termes . . . . . . . . . . . . . . . . 87

2
14 Conseils pour bien programmer en Prolog 87
14.1 Erreurs fréquentes . . . . . . . . . . . . . . . . . . . . . . . . . . 87
14.2 Trace d’exécution . . . . . . . . . . . . . . . . . . . . . . . . . . 88

1 Introduction à la programmation logique


1.1 Concepts de base
Écrire un programme en Prolog consiste essentiellement à définir un ensemble
d’entités et des relations entre ces entités. Pour se référer à une entité, on utilise
des termes. Un terme est une constante, une variable ou un terme composé :

— Constante : Désigne une entité du monde. Tout identificateur qui com-


mence par une lettre minuscule est une constante. Si on veut utiliser une
constante qui commence par une lettre majuscule, il faudra la délimiter par
des apostrophes.
Exemples : paulo, 3, ’UFPR’.
— Variable : En Prolog, une variable sert à désigner une entité inconnue mais
qui existe. Aussitôt que cette entité devient connue (on dira alors que la va-
riable est instanciée), elle sera toujours associée à celle-ci. Ceci est différent
du concept de variable dans les langages traditionnels de programmation
(comme C++), où le contenu d’une variable peut changer.
En Prolog, une variable est n’importe quel identificateur qui commence par
une lettre majuscule. Une variable peut aussi commencer par le symbole
"_" (il s’agira alors d’une variable anonyme, comme on le verra plus tard).
— Terme composé : Formé par une constante fonctionnelle (aussi appelée
foncteur) et des arguments qui sont eux-mêmes des termes, il sert à désigner
indirectement une entité du monde.
Par exemple, en utilisant un foncteur suc et la constante 0, on peut repré-
senter les nombres naturels. La valeur 1, qui est le successeur de 0, serait
représentée par le terme suc(0). Le numéro 3 serait représenté par le
terme suc(suc(suc(0))).
L’expression (8−4)∗3 pourrait être représentée par le terme mult(moins(8,4),3).
un terme composé est essentiellement récursif, puisqu’il contient des argu-
ments qui sont eux-même des termes.
Notons qu’une variable peut apparaître dans un terme composé. Suppo-
sons par exemple que nous utilisons le foncteur pere_de pour représenter
le père d’une personne. Ainsi, le terme pere_de(maria) représente le
père de Maria. Rien n’empêche d’écrire le terme pere_de(X), qui dé-
signe une entité qui est le père de quelqu’un.

3
1.2 Definition de prédicats
Pour définir des relations en Prolog, on utilise des prédicats. Un prédicat est
une expression qui retourne une valeur de vérité quand elle est appliquée à certains
arguments (les arguments d’un prédicat sont des termes). Si un prédicat accepte
seulement un argument, on dit qu’il définit une propriété. Par exemple, l’expression
intelligent(michel) représente une proposition qui sera vraie si Michel est
intelligent, et fausse dans le cas contraire.
Si un prédicat a plus d’un argument, il exprime une relation entre des entités du
monde. Par exemple, on peut écrire une expression mere(michel, marie_berthe)
pour indiquer que Marie-Berthe est la mère de Michel.
L’argument d’un prédicat peut être n’importe quel terme. Il peut être un terme
composé, comme dans acheter(laura, appartement(145, edifice(a))).
Il peut aussi être une variable, comme dans mere(X, marie_berthe). Cette
dernière expression peut être comprise de la manière suivante : Marie-Berthe est la
mère de quelqu’un, mais on ne sait pas qui.
Nous savons déjà qu’une expression prédicative, c’est-à-dire un prédicat avec
ses arguments, représente une propriété ou une relation qui peut être vraie ou
fausse. Mais comment déterminer cette valeur de vérité ? Une manière simple de
le faire consiste à déclarer un fait. Ainsi, en Prolog, pour indiquer que Michel
est intelligent, on ajoutera le fait suivant (notez l’usage du point qui clôt la décla-
ration) :

intelligent(michel).

Un ensemble de faits est le programme le plus simple que nous pouvons écrire
en Prolog. Voici un exemple de programme :

homme(paulo).
homme(joao).
homme(francisco).
femme(maria).
femme(roberta).
femme(ana).
pere(paulo,francisco).
pere(paulo,joao).
pere(francisco,ana).
mere(roberta,rita).
mere(roberta,joao).
mere(roberta,francisco).
mere(maria,roberta).

4
Remarquez que pere n’est pas un foncteur, mais plutôt un prédicat. Il n’est
pas utilisé ici comme un fonction qui désigne le père de quelqu’un. Quand nous
écrivons pere(francisco,paulo), nous indiquons que la relation pere est
vraie dans le cas de Francisco et Paulo. Qui est le père de qui dépend du pro-
grammeur. L’ordre des arguments n’a pas d’importance en Prolog. L’important est
d’être consistant dans l’usage qu’on fait d’un prédicat. Si le programmeur écrit
pere(francisco,paulo) pour indiquer que le premier est le père du second,
et non le contraire, il devra utiliser cette convention à tous les endroits où ce pré-
dicat sera utilisé. Ici, nous utilisons la convention où l’entité représentée par le
premier argument est le père de celle exprimée par le second argument. Ainsi, dans
notre exemple, Paulo est le père de Francisco. Idem pour la relation mère.

1.3 Requêtes
En Prolog, on utilise un programme en lui soumettant des requêtes. Une re-
quête est n’importe quelle expression prédicative entrée après l’invite ?-. Prolog
répondra Yes si elle est vraie, et No dans le cas contraire. Avec le programme
simple présenté plus haut, on peut faire diverses requêtes. On peut vérifier si Maria
est une femme, ou encore si João est le père de Paulo :

?- femme(maria).
Yes
?- pere(joao,paulo).
No

Une requête peut contenir des variables. Pour comprendre ce qui se produit
dans ce cas, il faut d’abord définir deux concepts importants :

Substitution : Une substitution θ est un ensemble fini (possiblement vide) de


paires de la forme Xi = ti , où Xi est une variable et ti est un terme, qui respecte
les contraintes suivantes :
— Pour tout Xi = ti ∈ θ et Xj = tj ∈ θ, où i 6= j, on ne peut pas avoir
Xi = Xj .
— Soit Xi = ti ∈ θ. Pour tout Xj = tj ∈ θ, Xi ne peut pas apparaître dans tj .

Par exemple, les ensembles {X = paulo, Y = 2} et {X = paulo, Y = f(Z)} sont


des substitutions. L’ensemble {X = paulo, Z = f(X)} n’est pas une substitution lé-
gale, parce que la variable X ne peut pas apparaître dans un autre terme. L’ensemble
{X = paulo, Y = 2, X = maria} n’est pas une substitution parce que X apparaît deux
fois à gauche du signe =. Remarquez que {X = p(Z), Y = Z} est une substitution
valide.

5
Normalement, une substitution est appliquée à un terme composé ou une ex-
pression prédicative. L’effet de l’application d’une substitution θ à une expression
A, dénotée θA, est le suivant : pour toute variable X présente dans A, on rem-
place toutes les occurences de celle-ci par le terme t qui lui est associé dans θ. Par
exemple, la substitution {X = paulo, Y = 2}, appliquée à l’expression p(X,f(Z),Y),
résulte en une nouvelle expression p(paulo,f(Z),2). Voici d’autres exemples :

θ A θA
{X = paulo, Y= 2} p(X,X,maria) p(paulo,paulo,maria)
{X = paulo, Y = 2} p(X,Z) p(paulo,Z)
{X = paulo, Y = q(Z)} p(fun(Y),X,r(X),W) p(fun(q(Z)),paulo,r(paulo),W)
{X = Y} p(X) p(Y)
Instance : Une expression A est une instance d’une autre expression B s’il
existe une substitution θ telle que A = θB. Par exemple, p(paulo,Y) est
une instance de p(X,Y). De même, p(paulo,maria) est une instance de
p(X,Y).

Considérons maintenant la requête suivante :

?- homme(X).

En utilisant le programme ci-dessus, cette requête réussira s’il existe un homme


dans les faits qui sont déclarés dans le programme. De plus, l’interpréteur Prolog
retournera une substitution pour toutes les variables qui apparaissent dans la re-
quête. Dans ce cas-ci, cela revient à identifier l’entité qui satisfait le prédicat. Dans
notre programme, il y a trois possibilités. Prolog retournera la première :

?- homme(X).
X = paulo

Une fois cette première réponse donnée, Prolog attend une directive. Si nous en-
trons <enter>, il s’arrêtera et répondra YES :

?- homme(X).
X = paulo
Yes

Si nous entrons ’ ;’ (le point-virgule), il tentera de retourner une autre réponse :

?- homme(X).
X = paulo ;
X = joao

6
Si encore une fois nous entrons ’ ;’ il retourne la troisième réponse possible :

?- homme(X).
X = paulo ;
X = joao ;
X = francisco

Si nous le faisons encore une fois, il répondra No, puisqu’il n’y a pas d’autres
possibilités :

?- homme(X).
X = paulo ;
X = joao ;
X = francisco ;
No

Il est possible de construire une requête qui contient plus d’une expression pré-
dicative (dans le jargon de Prolog, on dit d’une telle expression prédicative qu’elle
est un but). Tous les buts d’une requête sont combinés en utilisant l’opérateur vir-
gule. Par exemple, pour identifier un fils de Roberta et Paulo, nous ferions la re-
quête suivante :

?- mere(roberta,X),pere(paulo,X).
X = joao ;
X = francisco ;
No

1.4 Clauses
Si nous avions seulement la possibilité d’écrire des faits, Prolog ne serait pas
très utile. Normalement, la determination d’une relation entre deux entités se fait
de manière indirecte. Par exemple, une personne est propriétaire d’un objet si elle
a acheté cet objet. Dans ce cas, nous utilisons une clause, qui définit un prédicat en
fonction d’autres prédicats :

proprietaire(X,Y):-
acheter(X,Y).

Cette clause doit être comprise de la manière suivante : pour savoir si X est le
propritétaire de Y, il faut vérifier si X a acheté Y. Il existe une autre manière d’être
propriétaire d’un objet : le recevoir. Ainsi, notre programme peut être étendu pour
tenir compte de cette seconde possibilité :

7
proprietaire(X,Y):-
acheter(X,Y).
proprietaire(X,Y):-
recevoir(X,Y).

Pour être réellement utile, ce programme doit être complété par certains faits :

acheter(maria,manteau).
recevoir(paulo,auto).

proprietaire(X,Y):-
acheter(X,Y).
proprietaire(X,Y):-
recevoir(X,Y).

Maintenant, nous pouvons soumettre les requêtes suivantes :

?- proprietaire(paulo,auto).
Yes

?- proprietaire(paulo,manteau).
No

?- proprietaire(maria,X).
X = manteau

?- proprietaire(X,Y).
X = maria
Y = manteau ;

X = paulo
Y = auto ;

No

?- proprietaire(X,X).
No

Remarquez que la dernière requête échoue parce que les deux arguments doivent
être instanciés par le même terme.

8
Rien n’empêche de mélanger des faits avec des clauses. Par exemple, dans le
programme, on peut spécifier directement que Ana est propriétaire d’un chien qui
s’appelle Berto :

acheter(maria,manteau).
recevoir(paulo,auto).
chien(berto).

proprietaire(ana,berto).
proprietaire(X,Y):-
acheter(X,Y).
proprietaire(X,Y):-
recevoir(X,Y).

Attention : Il ne faut pas confondre ’prédicat’, qui est associé à une valeur de
vérité, et ’terme composé’, qui désigne une entité du monde.

1.5 Exercices
1.1 Soit le programme suivant :

situe_dans(curitiba,parana).
situe_dans(florianopolis,santa_catarina).
situe_dans(joinville,santa_catarina).
situe_dans(montreal,quebec).
situe_dans(X,bresil):-
situe_dans(X,parana).
situe_dans(X,bresil):-
situe_dans(X,santa_catarina).
situe_dans(X,canada):-
situe_dans(X,quebec).
situe_dans(X,amerique):-
situe_dans(X,bresil).
situe_dans(X,amerique):-
situe_dans(X,canada).

Quelle est la réponse aux requêtes suivantes ? (S’il y en a plus d’une, donnez-
les toutes)

?- situe_dans(florianopolis,santa_catarina).

9
?- situe_dans(florianopolis,parana).

?- situe_dans(X,santa_catarina).

?- situe_dans(curitiba,X).

?- situe_dans(X,Y).

Quelle est la réponse à la requête suivante ?


situe_dans(santa_catarina,bresil) ? Pourquoi ?

1.2 Écrivez les clauses Prolog qui définissent les relations suivantes :

grand_parent(X,Y) (X est la grand-mère ou le grand-père de Y)


cousin(X,Y) (X e Y sont cousins germains)

2 Variables et unification (pour extraires les informations)


2.1 Algorithme d’unification
Pour bien comprendre l’algorithme utilisé par Prolog pour exécuter un pro-
gramme, il faut d’abord comprendre l’unification. Intuitivement, l’unification consiste
en une tentative pour déterminer s’il existe une manière d’instancier deux expres-
sions afin de les rendre égales. Plus formellement, l’unification de deux termes A
et B est une substitution θ telle que θA = θB. On dénote θ l’unificateur de A et
B. Par exemple, la substitution {Y = joao, X = mere_de(joao)} est un unificateur
des énoncés aime(joao,X) et aime(Y,mere_de(Y)) .
En Prolog, l’opérateur "=" désigne l’unification. Voici un exemple :

?- aime(joao,X) = aime(Y,mere_de(Y)).
X = mere_de(joao)
Y = joao

Il y a deux détails importants à connaître sur l’utilisation de l’unification en


Prolog. Premièrement, quand l’interprète Prolog tente d’unifier un but avec un
fait ou la tête d’une clause, il peut s’avérer nécessaire de renommer les variables.
Supposons par exemple le fait aime(X,joao) et le but aime(maria,X). Ils
peuvent êtres unifiés si on renomme les variables. Dans ce cas, après le renommage,
l’unification sera effectuée avec les énoncés aime(X1,joão) et aime(maria,X2),
ce qui donnera le résultat {X1 = maria, X2 = joao}.

10
L’autre détail important est illustré par l’exemple suivant d’unification, qui peut
retourner plus d’un résultat :

aime(joao,X) = aime(Y,Z)

{ Y = joao, X = Z }
ou
{ Y = joao, X = Z, W = francisco }
ou
{ Y = joao, X = joao, Z = joao }
ou
...

Ce que l’interprète tente de faire est une unification qui retourne l’unificateur
le plus général, c’est-à-dire celui qui n’instanciera une variable que si c’est réelle-
ment nécessaire de le faire pour obtenir l’unificateur.
Voyons maintenant un algorithme d’unification qui retourne l’unificateur le
plus général. Soient A et B les deux expressions (prédicatives ou fonctionnelle)
à unifier. Voici l’algorithme qui retourne un unificateur θ, s’il existe, et retourne
ECHEC sinon :
θ=∅
La pile contient l’équation A = B
Tant que pile non vide :
Retirer la première équation X = Y de la pile
Si :
X est une variable qui n’apparaît pas dans Y :
Substituer toutes les occurrences de X par Y dans la pile et dans θ
Ajouter X = Y dans θ
Y est une variable qui n’apparaît pas dans X :
Substituer toutes les occurrences de Y par X dans la pile et dans θ
Ajouter Y = X dans θ
X et Y sont des constantes ou des variables identiques :
Continuer
X est f (X1 , ...Xn ) et Y est f (Y1 , ...Yn ) :
Empiler les n équations Xi = Yi
Aucun de ces cas :
Retourner ECHEC
Retourner θ

11
Intuitivement, deux expressions prédicatives A et B s’unifient si elles sont for-
mées à partir d’un même prédicat, avec le même nombre d’arguments. Chaque
argument du prédicat de A doit s’unifier avec l’argument à la même position dans
B. Rappelons-nous que chaque argument est lui-même un terme, qui peut être une
variable, une constante ou un terme composé. Deux termes s’unifient si une des
trois conditions suivantes est respectée :

— Les deux termes sont des constantes identiques.


— Un des termes est une variable non instanciée. Dans ce cas la variable sera
instanciée par l’autre terme.
— Les deux termes respectent les conditions suivantes :
— Ils ont le même foncteur et le même nombre d’arguments.
— Les arguments s’unifient.

2.2 Extraction de l’information


Voyons maintenant comment l’unification permet d’extraire de l’information.
Soit le même programme simple que nous avons introduit à la section précédente :

homme(paulo).
homme(joao).
homme(francisco).
femme(maria).
femme(roberta).
femme(ana).
pere(paulo,francisco).
pere(paulo,joao).
pere(francisco,ana).
mere(roberta,rita).
mere(roberta,joao).
mere(roberta,francisco).
mere(maria,roberta).

Pour savoir qui est une femme, il suffit de faire la requête suivante :

?- femme(X).
X = maria

Pour retourner la réponse, l’interprète Prolog tente d’unifier le but femme(X)


avec chacun des faits que le programme contient, dans l’ordre de leur apparition.
L’unification avec le fait homme(paulo) échoue, puisque les deux prédicats sont

12
différents. Il en sera de même pour les deux faits qui suivent. C’est seulement
lorsque l’interprète rencontre le fait femme(maria) que l’unification réussit.
Dans ce cas, le résultat de l’unification est une instance de la variable X avec la
constante maria. Voici d’autres exemples :

?- pere(paulo,X).
X = francisco ;
X = joao ;
No

?- pere(X,Y).

X = paulo
Y = francisco ;

X = paulo
Y = joao ;

X = francisco
Y = ana ;

No

2.3 Variables anonymes


Une variable anonyme est une variable dont la valeur instanciée après unifi-
cation nous importe peu. Une variable anonyme est n’importe quel identificateur
commençant par le symbole "_". Par exemple, si on désire seulement savoir si
Paulo a un fils, on peut faire la requête suivante :

?- pere(paulo,_).
Yes

Deux occurrences d’une variable anonyme dans un prédicat sont distinctes. Par
exemple, si nous désirons seulement savoir s’il existe une mère, nous soumettrons
la requête suivante :

?- mere(_,_).
Yes

Il est clair ici qu’il ne s’agit pas de trouver toute personne qui est sa propre
mère. En d’autres mots, cette requête n’est pas équivalente à celle-ci :

13
?- mere(X,X).
No

En fait, ce que l’interpréteur Prolog fait, c’est un renommage de chaque va-


riable anonyme pour la distinguer des autres. Cela signifie que la requête mere(_,_)
est transformée en une requête de la forme mere(_G120,_G304).
Jusqu’à maintenant, aucun des faits que nous avons déclarés ne contenait des
variables. Il n’est pas nécessaire qu’il en soit ainsi. Supposons que nous écrivons
un programme pour spécifier qui aime qui :

aime(paulo,maria).
aime(ana,clara).
aime(claudia,paulo).

Si nous voulons spécifier que tout le monde aime Maria, nous ajouterions le
fait suivant, qui contient une variable anonyme :

aime(_,maria).

Si nous voulons spécifier que tout le monde s’aime soi-même, nous ajouterions
le fait suivant :

aime(X,X).

2.4 Opérateur d’unification


On peut utiliser directement l’opérateur "=" pour unifier deux termes (ou deux
expressions prédicatives) :

?- X = 1.
X = 1

?- joao = roberta.
No

?- suc(X) = suc(suc(suc(0))).
X = suc(suc(0))

?- p(X,X) = p(1,2).
No

14
?- p(X,X) = p(1,1).
X = 1

?- p(X,X) = p(1,W).
X = 1
W = 1

?- p(Y,fun(Y)) = p(toto,Z).
Y = toto
Z = fun(toto)

?- p(X,X,2) = p(1,W,W).
No
Pour s’assurer que deux termes ne sont pas unifiables, on utilise l’opérateur \= :
?- p(X,X,2) \= p(1,W,W).
Yes

2.5 Exercices
2.1 Quel est le résultat des tentatives d’unification suivantes :
UFPR = ’UFPR’
ufpr = ’UFPR’
ufpr = ’ufpr’
4 = 3+1
f(X,a(b,c)) = f(Z,a(Z,c))
f(X,a(b,X)) = f(Z,a(Z,c))
population(ville((curitiba),X) = population(ville((Z),K)
population(ville((curitiba),X) = population(W,ville((montreal))
suc(suc(X)) = suc(suc(suc(suc(Z))))
(habite(X,app(145,edifice(K))),proprietaire(paulo,app(Z))) = (habite(M,A),proprietaire(M,A))
(habite(X,app(145,edifice(K))),proprietaire(paulo,Z)) = (habite(M,A),proprietaire(M,A))
(habite(X,app(145,edifice(K))),proprietaire(paulo,Z)) =
(habite(M,app(W,edifice(b123))),proprietaire(M,A))
(habite(X,app(145,Z)),proprietaire(paulo,app(145,Z))) =
(habite(M,app(W,edifice(b123))),proprietaire(M,A))

2.2 Soit le programme suivant :


aime(paulo,maria).
aime(ana,clara).
aime(claudia,paulo).
Pourquoi ne pouvons-nous pas ajouter le fait aime(_,_) pour spécifier que
tout le monde s’aime soi-même ? Expliquez ce qui se produit lorsqu’on ajoute ce
fait.

15
3 Algorithme d’exécution d’un programme
3.1 Algorithme de résolution
Essentiellement, un programme Prolog est composé de règles, qui sont des
clauses déclarant un fait qui dépend d’autres faits. Ces règles ont la forme suivante :

H :- B1 ,B2 , . . .,Bn

H et B1 ,B2 , . . .,Bn constituent la tête et le corps de la règle, respectivement.


La signification d’une règle est la suivante : pour résoudre un but H, il faut résoudre
la conjonction de buts B1 ,B2 , . . .,Bn . Un fait peut être considéré comme une
clause sans corps.
Pour satisfaire une requête, l’interpréteur Prolog utilise l’algorithme de résolu-
tion, qui utilise une pile, appelée résolvante, pour contenir tous les buts qu’il reste
à résoudre (notez que nous considérons ici qu’un fait est une clause dont le corps
est vide) :

0) Se positionner à la première clause du programme


1) Résolvante ← requête
2) Tant que la résolvante n’est pas vide :
2.1) Obj ← Premier objectif de la résolvante
2.2) Retirer le premier objectif de la résolvante
2.3) Chercher la première clause C = H : −B1 , B2 , . . . Bn telle que H s’unifie avec Obj
Si on réussit
2.4) Ajouter au début de la résolvante tous les buts B1 ,B2 ,. . .Bn
2.5) Si il existe d’autres têtes de clauses unifiables avec Obj
Mémoriser la position de la clause C comme dernier point de choix
2.6) Retourner à 2.1
Sinon
2.7) Si il existe des points de choix :
Retourner au dernier point de choix (Backtrack)
Revenir à l’étape 2.3
Sinon :
Retourner ECHEC
3) Retourner l’instantiation des variables de la requête
Il est important de noter que dans ce processus, le système mémorise les va-
riables instanciées. Quand une variable est instanciée dans une règle, soit dans la
tête, soit dans le corps, la même instanciation s’applique à toutes les occurrences
de la variable dans la règle. Quand l’interpréteur fait un retour arrière à un point de
choix, il élimine le résultat de toutes les unifications qui ont été réalisées entre le
moment où le point de choix a été mémorisé et le moment de l’échec.

16
3.2 Exemple d’exécution
Soit le programme suivant (rappelons-nous qu’on doit interpréter le prédicat
pere de telle manière que le premier argument représente le père de la personne
indiquée par le second argument, et similairement pour le prédicat mere) :

homme(paulo).
homme(joao).
homme(francisco).
femme(maria).
femme(roberta).
femme(ana).
pere(paulo,francisco).
pere(paulo,joao).
pere(francisco,ana).
mere(roberta,rita).
mere(roberta,joao).
mere(roberta,francisco).
mere(maria,roberta).
parents(X,M,P):-
mere(M,X),
pere(P,X).
frere(X,Y):-
homme(Y),
parents(X,M,P),
parents(Y,M,P).

Nous allons maintenant voir en détail comment est résolue la requête suivante :

?- frere(joao,francisco).
Yes

Au début la résolvante ne contient que la requête, qui sera unifiée avec la tête
de la dernière clause du programme. Ceci entraînera l’ajout des trois buts suivants
dans la résolvante :

homme(francisco)
parents(joao,M,P)
parents(francisco,M,P)

Le premier but, qui s’unifie avec un des faits du programme, est alors retiré.
Le prochain but s’unifie avec la tête de l’avant-dernière clause du programme. En

17
renommant M1 et P1 les deux variables de la clause, l’unificateur est le suivant :
{M = M1, P = P1}. Les buts du corps de la clause seront alors ajoutés dans la
résolvante :

mere(M1,joao)
pere(P1,joao)
parents(francisco,M1,P1)

Le premier but est résolu en l’unifiant avec le fait mere(roberta,joao),


qui se trouve dans le programme. Cette unification ajoute l’instanciation M1 =
roberta. Similairement, le second but est résolu avec le fait pere(paulo,joao),
où P1 = paulo. La résolvante n’aura alors qu’un seul but à résoudre :

parents(francisco,roberta,paulo)

Remarquez ici que les variables du but on été instanciées, à cause de la résolu-
tion des deux buts précédents. Rappelons-nous que nous avions déja M = M1. Or,
comme M1 a été instanciée par la constante roberta, la variable M est automati-
quement instanciée par la même constante. Idem pour la variable P.
Pour résoudre ce dernier but, on l’unifie avec l’avant-dernière clause du pro-
gramme, et obtient alors les deux buts suivants :

mere(roberta,francisco)
pere(paulo,francisco)

Finalement, ces deux derniers buts sont résolus de manière triviale, puisqu’il
existe, pour chacun, un fait identique dans le programme. La résolution se termine
donc avec succès, avec la substitution suivante : M = roberta, P = paulo.
Voici la résolution complète de cette requête (nous indiquons, à chaque étape,
la substitution θ) :

[frere(joao,francisco)] {}
[homme(francisco),parents(joao,M,P),parents(francisco,M,P)] {}
[parents(joao,M1,P1),parents(francisco,M1,P1)] {M=M1, P=P1}
[mere(M1,joao),pere(P1,joao),parents(francisco,M1,P1)] {M=M1, P=P1}
[pere(P1,joao),parents(francisco,roberta,P1)] {M=M1=roberta, P=P1}
[parents(francisco,roberta,paulo)] {M=M1=roberta, P=P1=paulo}
[mere(roberta,francisco),pere(paulo,francisco)] {M=M1=roberta, P=P1=paulo}
[pere(paulo,francisco)] {M=M1=roberta, P=P1=paulo}
[]

Considérons maintenant la requête suivante :

18
?- frere(francisco,Z).
X = joao

Comme nous le verrons, la résolution de ce but exigera un retour arrière. Voici


le déroulement de la résolution jusqu’à l’obtention de l’échec (pour simplifier, nous
ne montrons pas les substitutions de M et P) :

[frere(francisco,Z)] {}
*[homme(Y),parents(francisco,M,P),parents(Y,M,P)]} {Z=Y}
[parents(francisco,M,P),parents(paulo,M,P)] {Z=paulo}
[mere(M,francisco),pere(P,francisco),parents(paulo,M,P)] {Z=paulo}
[pere(P,francisco),parents(paulo,roberta,P)] {Z=paulo}
[parents(paulo,roberta,paulo)] {Z=paulo}
[mere(roberta,paulo),pere(paulo,paulo)] {Z=paulo}
ÉCHEC

Quand cet échec se produit, il faut reculer au dernier point de choix, indiqué par
une astérisque. Dans ce cas, il s’agit du moment où était tentée la résolution du but
homme(Y). Reculant à ce point, et en poursuivant la recherche d’une clause s’uni-
fiant à ce but, on obtient une nouvelle instanciation (Y = joao), et la résolution
continue à partir de ce point

[frere(francisco,Z)] {}
*[homme(Y),parents(francisco,M,P),parents(Y,M,P)]} {Z=joao}
[parents(joao,M,P),parents(joao,M,P)] {Z=joao}
[mere(M,francisco),pere(P,francisco),parents(joao,M,P)] {Z=joao}
[pere(P,francisco),parents(joao,roberta,P)] {Z=joao}
[parents(joao,roberta,paulo)] {Z=joao}
[mere(roberta,joao),pere(paulo,joao)] {Z=joao}
[pere(paulo,joao)] {Z=joao}
[] {Z=joao}

Maintenant, nous sommes en mesure de comprendre l’effet du " ;" entré après
une première solution retournée. Il s’agit en fait d’une demande de retour arrière,
tout comme si la résolution s’était butée à un échec. Dans le dernier exemple, le
résultat serait le suivant :

?- frere(francisco,X).
X = joao ;
X = francisco ;
No

Pour fournir cette seconde solution, l’interprète Prolog recule jusqu’au dernier
point de choix, qui correspond toujours au moment où on a traité but homme(Y) :

19
[frere(francisco,Z)] {}
[homme(Y),parents(francisco,M,P),parents(Y,M,P)]} {Z=francisco}
[parents(francisco,M,P),parents(francisco,M,P)] {Z=francisco}
[mere(M,francisco),pere(P,francisco),parents(francisco,M,P)] {Z=francisco}
[pere(P,francisco),parents(francisco,roberta,P)] {Z=francisco}
[parents(francisco,roberta,paulo)] {Z=francisco}
[mere(roberta,francisco),pere(paulo,francisco)] {Z=francisco}
[pere(paulo,francisco)] {Z=francisco}
[] {Z=francisco}

On voit que cette technique est pratique pour connaître, dans cet exemple, tous
les frères d’une personne. Mais on voit aussi que notre programme a le défaut
de nous indiquer que chaque homme est le frère de soi-même. Pour éviter cela,
il faudrait modifier la clause qui définit le prédicat frere, en ajoutant un test
supplémentaire :

frere(X,Y) :-
homme(Y),
parents(X,M,P),
parents(Y,M,P),
X \= Y.

L’ordre des buts dans le corps de la clause est très important dans l’exécution
d’un programme. Supposons, par exemple, que le prédicat frere soit défini de la
manière suivante :

frere(X,Y) :-
parents(X,M,P),
parents(Y,M,P),
homme(Y),
X \= Y.

Dans ce cas, beaucoup d’unifications inutiles seraient tentées. Avec une requête
comme frere(francisco,X), le programme pourrait tenter inutilement de
résoudre le but parents(Y,M,P) en instanciant Y par une valeur qui représente
une femme.

3.3 Retour arrière avec plusieurs points de choix


Quand il y a plus d’un point de choix au moment du retour arrière, il faut
s’assurer d’identifier celui qui sera considéré en premier. On reculera au point de
choix correspondant au dernier but unifié pour lequel il restait d’autres possibilités
d’unification. Pour mieux comprendre, considérons le programme suivant :

20
tres_intelligent(X):-
intelligent(X),
sympathique(X).
tres_intelligent(X):-
professeur_ia(X).

intelligent(X):-
debrouillard(X),
humain(X).
intelligent(X):-
dauphin(X).

humain(X):-
homme(X).
humain(X):-
femme(X).

professeur_ia(michel).
debrouillard(chico).
dauphin(flipper).
femme(george).
homme(chico).
sympathique(flipper).

Supposons maintenant la requête suivante :

?- tres_intelligent(X).

Pour résoudre ce but, l’interpréteur Prolog devra réaliser plusieurs retours ar-
rière. Voici le déroulement de la résolution jusqu’au premier échec :

*[tres_intelligent(X)] {}
*[intelligent(X),sympathique(X)] {}
[debrouillard(X),humain(X),sympathique(X)] {}
*[humain(chico),sympathique(chico)] {X=chico}
[homme(chico),sympathique(chico)] {X=chico}
[sympathique(chico)] {X=chico}
Échec

On note l’existence de trois points de choix. L’ordre d’apparition de ces points


de choix est le suivant :
1. tres_intelligent(X)
2. intelligent(X)
3. humain(X)

21
Par conséquent, le point de choix auquel on reculera, au premier échec, est
celui associé au but humain(X). La résolution repart à partir de ce point, pour se
buter une nouvelle fois à un échec :

*[tres_intelligent(X)] {}
*[intelligent(X),sympathique(X)] {}
[debrouillard(X),humain(X),sympathique(X)] {}
[humain(chico),sympathique(chico)] {X=chico}
[femme(chico),sympathique(chico)] {X=chico}
ÉCHEC

Cette fois, l’interprète Prolog reculera au point de choix associé au but intelligent(X),
puisqu’il n’y a plus de choix possible avec le but humain. Continuant à partir de
ce point, il faudra maintenant résoudre le but dauphin(X). La variable X sera
instanciée par la constante flipper. Le résolution pourra alors continer et termi-
ner avec succès :

*[tres_intelligent(X)] {}
[intelligent(X),sympathique(X)] {}
[dauphin(X),sympathique(X)] {}
[sympathique(flipper)] {X=flipper}
[] {X=flipper}

Remarquez qu’il reste encore un point de choix qui n’a pas été considéré. Ainsi,
si on demande une nouvelle solution, l’interpréteur Prolog reculera à ce point de
choix. Dans ce cas, une nouvelle résolution à partir de ce point tentera de résoudre
le but professeur_ia(X), ce qui se terminera avec succès, avec X = michel :

[tres_intelligent(X)] {}
[professeur_ia(X)] {}
[] {X=michel}

3.4 Exercices
3.1 Traduire en Prolog les faits suivants :
— João aime Maria.
— Paulo aime Antônio.
— Antônio aime Claudia.
— Fernando aime Claudia.
— Claudia aime tous ceux qui l’aiment.
— Personne n’aime Paulo.
— Deux personnes de sexes différents qui s’aiment mutuellement sont aussi
des amants.

22
3.2 Supposons un édifice contenant un ensemble de salles. On utilise une relation
connexe(X,Y) pour indiquer que l’on peut passer directement de la salle X à la
salle Y, et vice versa. Supposant que la constante exterieur désigne l’extérieur
de l’édifice, définissez le prédicat sortir(X), dont la résolution réussit s’il existe
un chemin pour se rendre à l’extérieur à partir de la salle X.
Exemple :

connexe(salle1,salle2).
connexe(salle2,salle5).
connexe(salle3,exterieur).
connexe(salle4,salle7).
connexe(salle5,salle6).
connexe(salle5,salle8).
connexe(salle6,salle9).
connexe(salle9,exterieur).

?- sortir(salle1).
Yes

?- sortir(salle4).
No

4 Opérateurs arithmétiques
4.1 Opérateurs
Prolog permet les opérations logiques suivantes : X < Y, X > Y, X =<
Y, X >= Y. À noter que =< et >= signifient "plus petit ou égal" et "plus grand
ou égal", respectivement.
Pour utiliser ces opérateurs, les variables doivent être instanciées :

?- 4 >= 3.
Yes
?- X >= 3.
No

Prolog fournit aussi les opérateurs arithmétiques +, -, *, /, mod. Pour


récupérer le résultat d’une opération arithmétique en Prolog, on utilise l’opérateur
"is" : X is Exp . La valeur de l’expression (composée en utilisant les opéra-
teurs arithmétiques) est calculée et on instancie la variable X avec le résultat. Si

23
l’expression contient des variables, celles-ci doivent être instanciées avec une va-
leur numérique.
Exemples :

?- X is 3+4.
X = 7

?- 7 is 3 + X.
Error

?- X = 4, Y is 2 * X.
X = 4
Y = 8

Si l’opérateur is est utilisé avec à sa gauche une variable instanciée, l’inter-


préteur Prolog vérifiera tout simplement si les valeurs sont égales :

?- X = 7, X is 3 + 4.
X = 7
Yes

?- X = 6, X is 3 + 4.
No

Il est important de noter que l’expression à la gauche de l’opérateur is ne peut


être qu’une valeur numérique, et non pas une expression quelconque. Par exemple,
la requête suivante échoue, malgré l’égalité :

?- X = 5 + 2, X is 3 + 4.
No

L’utilisation du résultat d’une expression numérique se fait indirectement par


le biais d’une variable.
Exemple : Programme qui convertit en valeur numérique un structure suc(suc(...)) :

Erroné:
convertir(0,0).
convertir(suc(X),Y+1):-
convertir(X,Y).

Correct:

24
convertir(0,0).
convertir(suc(X),Z):-
convertir(X,Y),
Z is Y+1.

Le premier programme est erroné parce qu’il n’évalue pas l’expression à chaque
pas de l’exécution. Il construit un structure en utilisant l’opérateur + :

?- convertir(suc(suc(0)),X).
X = 0 + 1 + 1

Seul le second programme évalue le résultat de l’expression :

?- convertir(suc(suc(0)),X).
X = 2

Parce que l’opérateur is ne permet pas d’utiliser une variable libre (non instan-
ciée) à droite, la requête suivante retournera une erreur d’exécution :

?- convertir(X,2).
Error

4.2 Exercices
4.1 Definissez un programme puissance(X,N,P) qui unifie P avec la va-
leur X N .

?- puissance(2,3,X).
X = 8

5 Listes
5.1 Définitions
Une liste est une structure particulière qui représente une séquence d’objets,
qui est construite de manière récursive avec les deux éléments suivants :
1. la liste vide [ ]
2. le foncteur ”.”, qui ajoute un item en tête de liste
Exemple : la séquence 1, 2 et 3 est représentée ainsi :

.(1, .(2, .(3, [])))

25
Il existe une autre notation plus pratique : [1,2,3]
Une liste peut contenir des éléments de types différents :

[1, 22, X, 54]


[4, plus(3,plus(3,0)), a]

Une liste peut contenir d’autres listes :

[1, [5,6], 3,10]


[X1, roberto, [ [paulo, Y], andre], eliana]

Voici une autre notation qui distingue la tête du reste de la liste (aussi appelé queue
de la liste) :

[1 | [2,3] ] ⇔ .(1, [2,3]) ⇔ [1,2,3]

Il est possible d’isoler plus d’un élément en tête de liste. Par exemple, le terme
suivant s’unifie avec n’importe quelle liste qui commence par les éléments 1 et 2 :

[1,2|X]

5.2 Unification de listes


Voici des exemples d’unification de listes :

?- [1,2|X] = [1,2]
X = []

?- [1,2|X] = [1,2,7,3,4]
X = [7,3,4]

Dans un programme Prolog, on utilise très souvent la décomposition d’une liste


pour isoler la tête. Voici des exemples :

p([1,2,3]).
p([je,mange,[un,gateau,au,chocolat]]).

?- p([X|Y]).
X = 1
Y = [2,3] ;

26
X = je
Y = [mange,[un,gateau,au,chocolat]]

?- p([_,_,[_|Y]]).
Y = [gateau,au,chocolat]

Remarque : La queue d’une liste peut être la liste vide. Dans l’exemple qui
suit, X s’unifie avec le premier élément de la liste [4], qui est la constante 4. Y
doit être unifié avec le reste de la liste. Comme il n’y a aucun élément après 4, il
sera donc unifié avec la liste vide.

?- [X|Y] = [4].
X = 4
Y = []

Voici un autre exemple du même phénomème, mais de manière indirecte par


l’unification de deux prédicats :

?- pred([],[1]) = pred(X,[Y|X]).
X = []
Y = 1

La liste vide ne peut pas s’unifier avec une liste qui contient au moins un élément :

?- [X] = [].
No

?- [X] = [1,2]
No

Voici un exemple subtil d’échec à l’unification des termes suivants :

?- p([X,Y],[X|Y]) = p(Z,Z).

Ici, Z doit s’unifier d’abord avec [X,Y]. Elle sera donc instanciée par cette ex-
pression. Ensuite, les deuxièmes arguments doivent être unifiés entre eux. Comme
Z a déjà été instanciée par le terme [X,Y], il faudra alors unifier celui-ci avec le
terme [X|Y]. Les deux têtes s’unifient sans problème, puisqu’il s’agit de la même
variable non instanciée. Il faudra alors unifier les deux queues, c’est-à-dire [Y]
et Y. Mais cela est impossible puisqu’il faudrait unifier une liste avec une autre
liste dont l’unique élément est la liste elle-même, résultant ainsi en une structure
infinie :

27
Y = [[[[...]]]]
Voici un autre exemple d’unification :
?- [X,a|Z] = [b,a,a|K]
X = b
Z = [a|K]
Ici, nous tentons d’unifier une liste qui contient au moins deux éléments avec
une autre liste qui en contient au moins trois. Les deux premiers éléments doivent
être unifiés : X = b et a = a. Il reste alors à unifier les deux restes, qui sont,
respectivement, Z et [a|K], ce qui se fait trivialement en instanciant la variable
Z.

5.3 Exercices
5.1 Quel est le résultat des tentatives d’unification suivantes :
p(X,[3|X]) = p(Z,[3])
p(X,[3|X]) = p([],[3,[]])
[roberta] = [X|Y]
[[a,b],X|Z] = [Y,Y]
pred([],[1]) = pred(X,[Y|X])
[curitiba,paranagua,pontagrossa|X] = [curitiba|Z]
[X,Y] = [X|Y]
[X,Y] = [X|Z]
[X,a|Z] = [b,a,a|K]
[[]|Y] = [[],[],[]]
[X,Y|Z] = [a,b,c,d]
[X|[Y|Z]] = [a,b,c,d]
[X|[Y|Z]] = [X,Y|Z]
[[X|Y]|Z] = [a,b,c,d]
[[X|Y]|Z] = [[a,b,c,d]]
[[X|Y]|Z] = [[a,b,c],[d,e],[f]]

6 Processus récursif
6.1 Definition
En Prolog, on utilise de manière systématique la récursivité pour résoudre un
problème. Un processus récursif consite en :

28
— un pas récursif qui accomplit une partie du travail (normalement sur un
morceau de la structure manipulée) et qui appelle récursivement la réso-
lution du même prédicat, mais avec le reste des données qui n’ont pas été
traitées ;
— une condition d’arrêt, qui termine la résolution.

Normalement, un processus récursif agit sur une structure elle-même récursive


(c’est-à-dire un terme dont l’un des arguments est un terme du même type). Voici
un exemple de prédicat défini de manière récursive :

factoriel(0,1).
factoriel(X,F):-
Y is X-1,
factoriel(Y,Fy),
F is Fy * X.

?- factoriel(4,X).
X = 24

?- factoriel(X,24).
Error

La seconde requête échoue parce que le programme n’a pas été écrit de manière
à accepter une variable libre comme premier argument. Ce qui se produit dans ce
cas est que la résolution du but Y is X-1, dans le corps de la seconde clause, ne
sera pas possible puisque la variable X doit être instanciée, comme l’exige l’opéra-
teur is.

6.2 Vérification de l’appartenance à une liste


Nous verrons maintenant la définition du prédicat member, déjà pré-défini
en Prolog, qui vérifie si un item est contenu dans une liste. Le raisonnement est
simple : si l’item recherché est le premier de la liste, on retourne avec succès.
Sinon, on continue la recherche dans la queue de la liste :

member(X,[X|_]).
member(X,[_|Y]):-
member(X,Y).

?- member(4,[2,3,4,8]).

29
Yes
Il est intéressant de savoir qu’on peut utiliser le même prédicat pour extraire
séquentiellement les éléments de la liste. Lors de la résolution, le premier élément
de la liste est retourné. Comme il y a un point de choix, on peut obtenir les autres
éléments par une suite de retours arrière :
?- member(X,[2,3,4]).
X = 2 ;
X = 3 ;
X = 4 ;
No
La figure 1 montre les détails de la résolution de cette requête (les points de
choix sont indiqués en italique).

La définition du prédicat member montre que l’unification permet l’utilisation


de techniques très puissantes, qui sont souvent utilisées dans l’élaboration de pro-
grammes Prolog :

— La décomposition de la liste (en isolant la tête) peut être spécifiée dans


la tête de la clause. Dans la première clause de member, on isole le pre-
mier élément et on utilise une variable anonyme pour la queue, étant donné
qu’elle ne nous intéresse pas dans ce cas. Dans la seconde clause, nous
avons le cas contraire : on utilise une variable anonyme pour la tête de liste,
alors que le reste sera unifié avec la variable Y.
— L’identité des éléments peut être spécifiée dans la tête de la clause. Par
exemple, dans la première clause de member, on exige que l’élément re-
cherché s’unifie avec le premier de la liste.

Voici une autre définition de member, équivalente mais moins élégante :


member(X,[Y|_]):-
X = Y.
member(X,[Y|Reste]):-
X \= Y,
member(X,Reste).

Il faut éviter ce type de définition. Il est en effet inutile d’utiliser des variables
différentes dans la tête de la première clause, puisque de toute façon on veut les
unifier.

30
Première solution retournée: Dernière tentative:

member(X,[2,3,4])
member(X,[2,3,4])
X = X1
X=2
member(X1,[3,4])
OK
X1 = X2

Deuxième solution retournée: member(X2,[4])

member(X,[2,3,4]) X2 = X3

X = X1 member(X3,[])

member(X1,[3,4])

X1 = 3 ÉCHEC

OK

Troisième solution retournée:

member(X,[2,3,4])

X = X1

member(X1,[3,4])

X1 = X2

member(X2,[4])

X2 = 4

OK

F IGURE 1 – Résolution de member(X,[2,3,4]).

31
Le problème du test réalisé dans le corps de la seconde clause est plus subtil.
Si la requête ne contient aucune variable libre, le résultat est le même. Par contre
si dans la requête le premier argument est une variable libre, le comportement est
bien différent. Dans ce cas, une seule réponse sera retournée :

?- member(X,[2,3,4]).
X = 2 ;
No

Ce qui se produit est que pour obtenir la seconde solution, il faut retourner au
point de choix qui nous amène a faire une tentative avec la seconde clause. Mais
pour résoudre avec cette clause, on devra éventuellement résoudre le but X \= 2.
Or ce but échouera, puisque la variable X peut être unifiée avec 2, et on demande
que ce ne soit pas le cas.
Pour bien saisir l’avantage de l’unification, on peut comparer avec ce que serait
une implémentation de member en C++. D’abord voici l’équivalent de l’implémen-
tation des listes :
template <typename T>
class ListElem
{
public:
friend class List<T>;
ListElem(T h, ListElem<T>* t) : = head_(h), tail_(t) {}
private:
T head_;
ListElem<T>* tail_;
};

template <typename T>


class List
{
public:
List(ListElem<T>* first = NULL) : first_(first) { }
bool empty() { return first_ == NULL; }
T head() {return first_->head_; }
List<T> tail() { return List(first_->tail_); }
void insert(T elem)
{
if (first_ == NULL)
first_ = new ListElem<T>(elem, NULL );
else
first_ = new ListElem<T>(elem, first_ );
}
private:
ListElem<T>* first_;

32
};

Avec cette implémentation des listes, nous définirions la fonction member de la


manière suivante :

template <class T>


bool member(T x, Liste<T> l)
{
if (l.empty())
return false;
else if (l.head() == x)
return true;
else
return member(x, l.tail());
}

Nous allons maintenant voir un exemple où on utilise le prédicat member pour


extraire les éléments d’une liste. Supposons une situation où on reçoit une liste
de noms de personnes et que nous voulons imprimer tous les éléments de cette
liste qui représentent des femmes. Considérons d’abord un programme récursif qui
solutionne ce problème :

femme(ana).
femme(clara).
femme(maria).

liste_femme([]).
liste_femme([X|Xs]):-
femme(X),
write(X),
nl,
liste_femme(Xs).
liste_femme([_|Xs]):-
liste_femme(Xs).

?- liste_femme([ana,paulo,clara,roberto]).
ana
clara

Yes

33
On peut faire une version non récursive du même programme, en utilisant le
prédicat fail. Ce prédicat échoue toujours lorsqu’on essait de le résoudre, forçant
ainsi un retour arrière. Voici le programme.

liste_femme(L):-
member(X,L),
femme(X),
write(X),
nl,
fail.
liste_femme(_).

À la première tentative de résolution de member, X s’unifie avec le premier


élément de la liste. Ensuite, le programme vérifie si X est une femme. Si c’est le
cas, le nom est affiché et on passe au prédicat suivant, qui est le fail. Un retour
arrière est alors réalisé, ce qui nous ramène au point de choix qui a été introduit
lors de la résolution de member. On obtiendra alors une nouvelle valeur pour X,
soit le prochain item de la liste. Quand le prochain élément de la liste n’est pas une
femme, on échoue à la résolution du but femme(X), ce qui force à nouveau un
retour arrière. Et ainsi de suite jusqu’à ce que toute la liste ait été parcourue.

6.3 Concaténation de listes


Le prochain exemple est la concaténation de listes, représentée par le prédicat
append en Prolog. Le principe est le suivant :
1. Isoler le premier élément de la liste.
2. Résoudre récursivement le prédicat pour concaténer le reste de la liste avec
la seconde liste.
3. Ajouter au début de la liste résultante l’élément qui avait été isolé à la pre-
mière étape.
Traduit en Prolog, le programme obtenu est le suivant :

append([],X,X).
append([X|Xs],Y,[X|Zs]):-
append(Xs,Y,Zs).

Ce qui est intéressant avec ce programme est que l’unification, puisqu’elle ne


fait pas de distinction entre les données et les résultats, permet différents usages du
même programme :

34
?- append([a,b,c],[d,e],R).
R = [a,b,c,d,e]

?- append(X,[d,e],[a,b,c,d,e]).
X = [a,b,c]

?- append(X,Y,[a,b,c]).
X = []
Y = [a,b,c] ;

X = [a]
Y = [b,c] ;

X = [a,b]
Y = [c] ;

X = [a,b,c]
Y = [] ;

No

La figure 2 montre les détails de la résolution de la dernière requête. La figure 3


montre la situation au moment du premier retour arrière. Finalement, la figure 4
montre l’état au second retour arrière.

append(X,Y,[a,b,c])

X=[]
Y = [a,b,c]

append([], [a,b,c],[a,b,c]) ===> X=[] Y=[a,b,c]

F IGURE 2 – Résolution de append(X,Y,[a,b,c])

Aussi étrange que cela puisse paraître, on peut même utiliser le prédicat append
avec des variables non instanciées pour les deux derniers arguments :

35
append(X,Y,[a,b,c]) append(X,Y,[a,b,c])
X=[a|Xs] X=[a|Xs]
Y = Y1 Y = Y1

append(Xs,Y1,[b,c]) append(Xs,Y1,[b,c])
Xs=[]
Y1 = [b,c]

append([],[b,c],[b,c]) ===> X=[a] Y=[b,c]

F IGURE 3 – Résolution de append(X,Y,[a,b,c])

append(X,Y,[a,b,c]) append(X,Y,[a,b,c])
X=[a|Xs] X=[a|Xs]
Y = Y1 Y = Y1

append(Xs,Y1,[b,c]) append(Xs,Y1,[b,c])
Xs=[b|Xs1] Xs=[b|X1s]
Y1 = Y2 Y1 = Y2

append(Xs1,Y2,[c]) append(X1s,Y2,[c])
X1s = []
Y2 = [c]

append([],[c],[c]) ===> X=[a,b] Y=[c]

F IGURE 4 – Résolution de append(X,Y,[a,b,c])

36
?- append([1,2],X,Y).
X = K
Y = [1,2|K]

6.4 Exercices
6.1 Soit les faits suivants :

suivant(a,b).
suivant(b,c).
suivant(c,d).
...
suivant(y,z).
suivant(z,a).

Écrivez un programme Prolog qui utilise ces faits pour encoder un nom en
suivant l’algorithme suivant. Soit n la position d’une lettre dans l’alphabet. Chaque
lettre du nom est substituée par la lettre qui est à la position n + x dans l’alphabet,
où x est spécifié dans l’appel du programme. Par exemple, si x = 3, la lettre d
sera substituée par la lettre g, et la lettre z par la lettre c. Le programme reçoit une
liste de lettres qui représente le nom et retourne une liste de lettres qui représente
le nom encodé :

?- encoder([m,i,c,h,e,l],3,X).
X = [p,l,f,k,h,o]

6.2 Soit le programme suivant :

mystere(N,Res):-
magique(N,[],L),
reverse(L,L2),
append(L,[N|L2],Res).

magique(1,L,L).
magique(N,A,L):-
N2 is N-1,
magique(N2,[N2|A],L).

a) Quel sera le résultat retourné pour la requête suivante :

?- mystere(4,L).

37
b) Quel sera le résultat si on demande une seconde solution ?
c) Modifiez le programme de manière à ce qu’il ne puisse pas retourner plus d’une
solution.

6.3 Soit le programme suivant :

mystere(X,Y,Z):-
myst1(X,Y,Z).

myst1([ ],[ ],[ ]).


myst1([X|Reste],[X|R1],R2):-
myst2(Reste,R1,R2).

myst2([ ],[ ],[ ]).


myst2([X|Reste],R1,[X|R2]):-
myst1(Reste,R1,R2).

a) Quel sera le résultat retourné pour la requête suivante :

?- mystere([t,b,u,e,d,m,o],X,Y).

b) Quel sera le résultat retourné pour la requête suivante :

?- mystere(X,[1],Z),

c) Quel sera le résultat si on demande une seconde solution pour la requête de b) ?

6.4 On représente une matrice n × n par une liste de listes, où chaque liste corres-
pond à une rangée de la matrice. Soit par exemple la matrice suivante :
 
a b c
 d e f 
g h i
Voici la représentation de cette matrice :

[ [a, b, c],
[d, e, f],
[g, h, i] ]

38
La position d’un élément de la matrice est représentée par une paire (x, y),
où (0, 0) désigne le coin supérieur gauche et (n, n) le coin inférieur droit. Dans
l’exemple, les éléments aux positions (0, 0) et (3, 3) sont a e i, respectivement.
Définissez en Prolog le prédicat chercher(X,Y,Matrice,Elem), qui
instancie Elem avec l’élément qui se trouve à la position (X,Y) dans la matrice :

?- chercher(2,2,[[a,b,c],[d,e,f],[g,h,i]],Z).
Z = e

6.5 Soit le programme suivant :

p(X,[],[X]).
p(X,[Y|L],[X,Y|L]):-
X < Y.
p(X,[Y|L],[Y|R]):-
p(X,L,R).

a) Quel sera le résultat retourné pour la requête suivante :

?- p(5,[2,3,9],Z).

b) Quel sera le résultat si on demande une seconde solution ?

6.6 Soit l’arbre généalogique suivant :

Jorge
PÈRE

Pedro Maria
PÈRE MÈRE MÈRE

Ana Leo Luis

MÈRE PÈRE

Rui

Une manière de représenter cet arbre est une liste de termes formés par les
foncteurs pere et mere.
Définissez un prédicat descendant_commun qui reçoit trois arguments. Les
deux premiers sont les noms de deux personnes. Le troisième est un arbre généa-
logique qui est représenté par une liste qui contient toutes les relations de parenté,
en utilisant les foncteurs pere et mere. Par exemple :

39
?- descendant_commun(ana,leo,
[pere(jorge,maria), mere(maria,leo),
mere(maria,luis), pere(pedro,ana),
mere(ana,rui), pere(leo,rui)]).
Yes

?- descendant_commun(pedro,jorge,
[pere(jorge,maria), mere(maria,leo),
mere(maria,luis), pere(pedro,ana),
mere(ana,rui), pere(leo,rui)]).
Yes

?- descendant_commun(pedro,luis,
[pere(jorge,maria), mere(maria,leo),
mere(maria,luis), pere(pedro,ana),
mere(ana,rui), pere(leo,rui)]).
No

6.7 Supposons une représentation en Prolog d’une base de connaissances en lo-


gique propositionnelle, qui utilise les opérateurs ∧, ∨ et ¬. Ces opérateurs sont
représentés par les termes fonctionnels and et or, qui prennent deux arguments,
et neg, qui n’en prend qu’un seul. Un argument peut être une proposition ou une
expression. Par exemple, supposons la base de connaissances suivante :

P
¬S ∧ Q
P ∨ ¬(T ∧ R)
Cette base serait représentée par la liste suivante :

[p, and(neg(s),q), or(p,neg(and(t,r)))]

Définissez un prédicat verif(F,I), où F est la liste qui représente la base de


connaissances et I une interprétation, c’est-à-dire la liste des propositions vraies.
La résolution doit retourner avec succès si la base est vraie selon l’interprétation I.
Si une proposition se trouve dans la liste I, elle est vraie, sinon elle est considérée
fausse. Voici des exemples d’exécution qui montrent comment doit se comporter
le programme :

?- verif([p, and(neg(s),q), or(p,neg(and(t,r)))],[p,q]).

40
Yes
?- verif([p, and(neg(s),q), or(p,neg(and(t,r)))],[p,s,q]).
No

7 Interprétations déclarative et procédurale


En Prolog, il y a deux manières d’analyser un programme. La première est une
interprétation déclarative, où les règles représentent un état du monde. Elles sont
utilisées pour déterminer si une situation est vraie ou fausse. Le prédicat member
défini auparavant est perçu de manière déclarative quand on fait une requête du
type member(2, [1,2,3,4]). L’interprétation déclarative, dans ce cas, est la
suivante : un élément est membre d’une liste s’il est le premier ou s’il est membre
du reste de la liste.
L’autre interprétation, dite procédurale, considère les règles comme des re-
cettes pour atteindre un certain objectif. Une règle du type A:- B,C. est com-
prise de la manière suivante : pour résoudre A, il faut résoudre B et ensuite C. Par
exemple, le prédicat append(L1,L2,L3) suggère naturellement une interpréta-
tion procédurale : pour concaténer L1 e L2, on doit d’abord concaténer la queue de
L1 avec L2, ajouter le premier élément de L1 à la tête du résultat obtenu et unifier
le tout avec L3.
En résumé, on a une interprétation procédurale quand on tient compte de l’al-
gorithme de résolution de Prolog pour comprendre le programme. Entre autres, on
doit alors tenir compte de l’ordre d’apparition des buts durant la résolution. D’un
autre côté, on a une interprétation déclarative quand on peut comprendre un pro-
gramme en faisant complètement abstraction de l’algorithme de résolution.
Pour mieux comprendre ces concepts, nous allons étudier quelques exemples.
Soit le programme suivant :

plus(0,X,X).
plus(suc(X),Y,suc(Z)):-
plus(X,Y,Z).

Dans une interprétation déclarative, il est vu comme un programme qui déter-


mine si la somme des deux premiers arguments est égale au troisième :

?- plus(suc(suc(0)),suc(suc(0)), suc(suc(suc(suc(0))))).
Yes
?- plus(suc(suc(0)),suc(suc(0)), suc(suc(0))).
No

41
Dans une interprétation procédurale, il peut être utilisé pour calculer la somme
de deux nombres :

?- plus(suc(suc(0)),suc(suc(0)), X).
X = suc(suc(suc(suc(0))))

Étant donné qu’un clause en Prolog ne distingue pas les entrées des sorties, elle
peut avoir plusieurs interprétations différentes. Par exemple, le prédicat plus peut
ête utilisé pour réaliser un addition, tout comme une soustraction :

?- plus(suc(suc(0)),X , suc(suc(suc(suc(0))))).
X = suc(suc(0))

Considérons maintenant le programme factoriel présenté antérieurement :

factoriel(0,1).
factoriel(X,F):-
Y is X-1,
factoriel(Y,Fy),
F is Fy * X.

Nous avons déjà utilisé ce programme avec une interprétation procédurale. Ap-
paremment, il permet aussi une interprétation déclarative, puisqu’il peut être utilisé
pour vérifier si 4! = 24 :

?- factoriel(4,24).
Yes

Mais cela est une illusion, puisqu’en fait, dans les cas où il devrait échouer, avec
la requête factoriel(3,24) par exemple, il entrera plutôt dans une boucle
infinie (pourquoi ?). Pour résoudre ce problème, il faut ajouter un autre test dans la
dernière clause :

factoriel(0,1).
factoriel(X,F):-
X > 0,
Y is X-1,
factoriel(Y,Fy),
F is Fy * X.

Remarquez que cette nouvelle version n’accepte pas toutes les possibilités de
requête. Par exemple, on ne peut pas l’utiliser pour identifier le nombre x tel que
x! = 24 :

42
?- factoriel(X,24).

ERROR: Arguments are not sufficiently instantiated


^ Exception: (7) _G311 is _G239-1 ?
Le problème ici est que l’opérateur is exige qu’il n’y ait aucune variable libre
à sa droite. Pour obtenir un programme qui fonctionne correctement, il faut ajou-
ter une clause dans la définition et ajouter des tests pour déterminer si le premier
argument est une variable libre :
factoriel(0,1).
factoriel(X,Y):-
var(X),
proc_fact(0,Y,X).
factoriel(X,F):-
nonvar(X),
X > 0,
Y is X-1,
factoriel(Y,Fy),
F is Fy * X.

proc_fact(X,Y,X):-
factoriel(X,Y).
proc_fact(X,Y,R):-
X2 is X+1,
X2 =< Y,
proc_fact(X2,Y,R).
Remarquons que la seconde clause utilise un prédicat intermédiaire proc_fact,
qui calcule itérativement le factoriel de tous les nombres à partir de 0, jusqu’à ce
que l’on tombe sur une valeur qui correspond à l’entrée Y ou qu’on dépasse cette
valeur (dans ce dernier cas, il aura échec).
Pour terminer cette discussion, voyons un dernier exemple qui permet lui aussi
les deux interprétations. Il s’agit du prédicat next(X,L,Y), qui réussit si Y suit
immédiatement X dans la liste L. Voici le programme :
next(X,[X,N|_],N).
next(X,[_|Reste],N):-
next(X,Reste,N).
On peut facilement vérifier que ce programme permet l’interprétation déclara-
tive pour déterminer si deux éléments sont voisins dans une liste :

43
?- next(2,[1,2,3,4],3).
Yes

?- next(2,[1,2,3,4],4).
No

Il permet aussi toutes les combinaisons possibles d’instances pour les variables
X et Y :

?- next(2,[1,2,3,4],X).
X = 3

?- next(X,[1,2,3,4],4).
X = 3

?- next(X,[1,2,3,4],Y).
X = 1
Y = 2 ;

X = 2
Y = 3 ;

X = 3
Y = 4 ;

No

Notons que les programmes n’ont pas tous un interprétation déclarative. Un


bon exemple est le prédicat liste_femmes, que nous avons présenté antérieu-
rement. L’utilisation des prédicats write et fail forcent une interprétation pro-
cédurale du programme.

8 Construction de nouvelles structures


8.1 Appariement
Un appariement entre une structure donnée et une structure produite permet
la construction de cette dernière parallèlement au parcours de la structure donnée.
L’idée est la suivante : pour chaque élément de la structure donnée en entrée, on
produit un élément qui se retrouvera à la position correspondante dans la structure

44
qui sera retournée comme résultat. On peut éventuellement avoir des cas où un
item traité dans la structure d’entrée n’a aucun correspondant dans la structure de
sortie. Il se peut aussi que l’item ait plusieurs valeurs qui lui correspondent dans
la structure de sortie. L’important ici est qu’il doit y avoir un parallélisme entre les
éléments de la structure d’entrée et ceux de la structure de sortie.
Exemples :
Programme pour extraire d’une liste les nombres inférieurs à une valeur fixée :

filtrer([],_,[]).
filtrer([X|R],Max,[X|R2]):-
X < Max,
filtrer(R,Max,R2).
filtrer([X|R],Max,R2):-
X > Max,
filtrer(R,Max,R2).

?- filtrer([4,10,5,0,7,3], 5, L).
L = [4,5,0,3]

Programme pour éliminer tous les nombre répétés d’une liste :

eliminer_rep([],[]).
eliminer_rep([X,X|R],L):-
eliminer_rep([X|R],L).
eliminer_rep([X|R],[X|R2]):-
eliminer_rep(R,R2).

?- eliminer_rep([1,2,2,2,3,6,7,7,10],L).
L = [1,2,3,6,7,10]

?- eliminer_rep([1,2,2,2,3,6,2,2,2,7,7,10],L).
L = [1,2,3,6,2,7,10]

Attention : voici une autre définition du prédicat eliminer_rep qui apparem-


ment fonctionne bien mais qui est incorrecte si on veut éliminer uniquement les
répétitions consécutives :

eliminer_rep([],[]).
eliminer_rep([X|R],L):-
member(X,R),
eliminer_rep(R,L).

45
eliminer_rep([X|R],[X|L]):-
eliminer_rep(R,L).

Défini ainsi, le programme retirera toutes les répétitions, qu’elles soient consécu-
tives ou non :

?- eliminer_rep([1,2,2,2,3,6,7,7,10],L).
L = [1,2,3,6,7,10]

?- eliminer_rep([1,2,2,2,3,6,2,2,2,7,7,10],L).
L = [1,2,3,6,7,10]

Programme pour construire une expression arithmétique :

const_exp(Liste,Exp):-
const(Liste,Exp,[]).

const([’(’|Liste],exp(A1,Op,A2),Reste):-
chercher_arg(Liste,A1,[Op|R1]),
chercher_arg(R1,A2,[’)’|Reste]).

chercher_arg([X|Reste],X,Reste):-
X \= ’(’.
chercher_arg([’(’|Reste],Exp,R2):-
const([’(’|Reste],Exp,R2).

?- const_exp([’(’,3,*, ’(’,4,-,a,’)’,’)’],Exp).
Exp = exp(3,*,exp(4,-,a))

8.2 Utilisation d’un accumulateur :


On utilise un accumulateur quand on ne peut pas construire une nouvelle struc-
ture parallèlement à la structure fournie en entrée, ou encore pour obtenir une ré-
cursivité plus efficace. À chaque pas, on mémorise une structure intermédiaire que
l’on construit graduellement. Le programme retourne la structure construite quand
il arrive à la fin du processus récursif.
Exemples :
Exemple classique de l’inversion d’une liste :

46
inverser(X,L):-
inv(X,[],L).

inv([],L,L).
inv([X|R],L,L2):-
inv(R,[X|L],L2).

?- inverser([5,4,3,2],X).
X = [2,3,4,5]

Tri par insertion :

inserer([],X,[X]).
inserer([X|R],Y,[Y,X|R]):-
Y < X.
inserer([X|R],Y,[X|R2]):-
Y > X,
inserer(R,Y,R2).

trier(L,Lord):-
tri_ins(L,[],Lord).

tri_ins([],L,L).
tri_ins([X|R],L,Lord):-
inserer(L,X,L2),
tri_ins(R,L2,Lord).

?- trier([3,7,5,9,10,2],X).
X = [2,3,5,7,9,10]

8.3 Autres exemples :


Évaluation d’une expression arithmétique :

eval(X,_,X):-
number(X).
eval(X,Env,Val):-
atom(X),
member([X,Val],Env).
eval(exp(X,Op,Y),Env,Val):-
eval(X,Env,X1),

47
eval(Y,Env,Y1),
apply(Op,X1,Y1,Val).

apply(+,X,Y,Val):- Val is X+Y.


apply(-,X,Y,Val):- Val is X-Y.
apply(*,X,Y,Val):- Val is X*Y.
apply(/,X,Y,Val):- Val is X/Y.

?- eval(exp(3,*,exp(4,-,a)),[[a,2],[b,8]],X).
X = 6

Pour évaluer une expression, le programme reçoit deux structures. Une est
l’expression, qui peut contenir des constantes qui ne sont pas des nombres mais
qui représentent plutôt des variables (ne pas confondre avec des variables Prolog).
L’autre est une liste qui indique la valeur associée à chaque "variable" que contient
l’expression.
Remarque : Dans cet exemple, on utilise les prédicat pré-définis (voir la section
13, qui décrit ces prédicats) number(X) et atom(X).
Voici un programme pour extraire d’une liste les éléments qui suivent immé-
diatement un élément spécifié :

sousliste_apres(X, [X|Reste], Reste).


sousliste_apres(X,[_|Reste], R):-
sousliste_apres(X,Reste,R).

?- sousliste_apres(4,[1,2,3,4,5,3,2,1],L).
L= [5,3,2,1]

Le problème avec ce programme est que si un élément apparaît deux fois, nous
aimerions que la liste retournée soit celle qui suit la dernière occurrence de cet
élément :

?- sousliste_apres(3,[1,2,3,4,5,3,2,1],L).
L= [2,1]

Pour obtenir ce comportement, il faut modifier le programme, en utilisant le


prédicat pré-défini \+, qui représente la négation (voir la section 12.3) :

48
sousliste_apres(X, [X|Reste], Reste):-
\+ member(X,Reste).
sousliste_apres(X,[_|Reste], R):-
sousliste_apres(X,Reste,R).

8.4 Exercices
8.1 Écrivez un programme pour dupliquer les éléments d’une liste :

?- dupliquer([a,c,f],X).
X = [a,a,c,c,f,f]

8.2 Soit un programme pour calculer la somme d’une liste de nombres :

?- somme([3,7,9],X).
X = 19

a) Écrivez une version récursive de ce programme qui n’utilise pas la technique


de l’accumulateur.
b) Écrivez une nouvelle version qui utilise la technique de l’accumulateur.

8.3 Écrivez un programme qui reçoit une valeur et partitionne une liste, retournant
une liste qui contient toutes les items qui sont inférieurs ou égaux a cette valeur, et
une autre liste qui contient les nombres supérieurs à cette valeur :

?- partition([3,24,0,10,12],8,X,Y).
X = [3,0]
Y = [24,10,12]

8.4 Écrivez un programme qui détermine si une liste est une sous-liste d’une autre
liste :

?- sousliste([a,f],[w,z,a,f,u]).
Yes
?- sousliste([a,u],[w,z,a,f,u]).
No

8.5 Écrivez un programme qui détermine si tous les éléments d’une liste sont inclus
dans une autre liste :

49
?- inclus([a,u],[w,z,a,f,u,h]).
Yes
?- inclus([a,k],[w,z,a,f,u,h]).
No

8.6 Écrivez un programme qui traduit en français une liste de mots en anglais et
vice-versa :

?- traduire([i,eat,an,orange],P).
P = [je,mange,une,orange]
?- traduire(E,[je,suis,ici]).
E = [i,am,here]

*8.7 Modifiez le programme précédent pour traiter les cas où un mot correspond à
plus d’un mot dans l’autre langue :

?- traduire2([paul,will,go,to,montréal],F).
P = [paul,ira,à,montréal]
?- traduire2([paul,talked,to,robert],F).
P = [paul,a,parle,à,robert]
?- traduire2(E,[je,cherche,la,salle,de,bain]).
E = [i,am,looking,for,the, bathroom]

8.8 Écrivez une programme qui concatène une liste de listes :

?- concatener([[a,b],[c,d,e],[f,g]],L).
L = [a,b,c,d,e,f,g]
?- concatener([[],[1,2]],L).
L = [1,2]
?- concatener([[],[]],L).
L = []
?- concatener([],L).
L = []

*8.9 Écrivez un programme qui effectue un tri par sélection.

8.10 Écrivez une programme qui reçoit une liste et qui retourne une liste ne conte-
nant que les éléments qui se retrouvent plus d’une fois dans la liste fournie :

?- elem_repetes([a,b],L).
L = []

50
?- elem_repetes([a,a,b],L).
L = [a]
?- elem_repetes([a,b,c,c,d,e,c,d,d,k,l,e],L).
L = [c,d,e]

8.11 Écrivez un programme qui intercale les éléments de deux listes. Si elles sont
de tailles différentes, on complète avec les items restants dans la liste de plus grande
taille.

?- intercaler([],[], L).
L = []
?- intercaler([a],[], L).
L = [a]
?- intercaler([],[1,2], L).
L = [1,2]
?- intercaler([1,2],[], L).
L = [1,2]
?- intercaler([a,b],[1,2,3,4], L).
L = [a,1,b,2,3,4]
?- intercaler([a,b,c,d],[1,2,3,4], L).
L = [a,1,b,2,c,3,d,4]

8.12 Écrivez un programme qui aplatit une liste pouvant contenir un nombre indé-
fini de listes imbriquées. (essayez d’écrire une version efficace qui n’utilise pas le
prédicat append) :

?- aplatir([1,2,[3,4],5],L).
L = [1,2,3,4,5]
?- aplatir([1,2,[],[2,[6,7,[10]]]], L).
L = [1,2,2,6,7,10]
?- aplatir([[[]]], L).
L = []

8.13 Écrivez un programme qui reçoit une liste et qui retourne une liste de paires,
où chaque paire indique le nombre de fois qu’un élément apparaît dans la liste.
(indice : essayez d’utiliser un accumulateur et le prédicat num_occ de la section
9) :

?- num_app([a,b,a,d,c,c,a,d],L).
L = [[c,2],[d,2],[b,1],[a,3]]

51
8.14 Modifiez le programme de l’exercice 3.2 pour obtenir le chemin qui permet
de sortir d’une salle :

connexe(salle1,salle2).
connexe(salle2,salle4).
connexe(salle4,exterieur).
connexe(salle5,salle1).
connexe(salle5,exterieur).
connexe(salle6,salle3).
connexe(salle3,salle7).
connexe(salle7,salle6).

?- sortir(salle1,Chemin).
Chemin = [salle1, salle2, salle4]

?- sortir(salle3,Chemin).
No

8.15 Transformez le programme factoriel de la section 6 en utilisant la tech-


nique de l’accumulateur.

8.16 Soit un programme qui retourne la valeur maximale d’une liste de nombres.
a) Écrivez une version récursive de ce programme qui n’utilise pas la technique
de l’accumulateur.
b) Écrivez une version qui utilise la technique de l’accumulateur.

9 Ordre des clauses


Un aspect imporant à ne pas oublier, quand on programme en Prolog, est l’ordre
des clauses. Cet ordre détermine quelle sera la première solution retournée. Voyons,
par exemple, l’utilisation de member avec une variable libre comme premier ar-
gument :

member(X,[X|_]).
member(X,[_|Y]):-
member(X,Y).

?- member(Z,[a,b,c]).
Z = a ;

52
Z = b ;
Z = c ;
No

Si on inverse l’ordre des clause, l’ordre des solutions retournées est aussi in-
versé :

member(X,[_|Y]):-
member(X,Y).
member(X,[X|_]).

?- member(Z,[a,b,c]).
Z = c ;
Z = b ;
Z = a ;
No

Dans d’autres cas, l’ordre peut faire en sorte que le processus récursif ne s’arrête
jamais :

append([X|Xs],Y,[X|Zs]):-
append(Xs,Y,Zs).
append([],X,X).

?- append(X,[1,2],Y).
... (boucle infinie)

Finalement, si l’ordre est incorrect, on peut obtenir des réponses erronées. Sup-
posons que nous voulons écrire un programme qui calcule le nombre de fois qu’un
élément apparaît dans une liste. Le programme correct est le suivant :

num_occ(_,[],0).
num_occ(X,[X|Reste],Num):-
num_occ(X,Reste,N),
Num is N + 1.
num_occ(X,[_|Reste],Num):-
num_occ(X,Reste,Num).

?- num_occ(b,[a,b,f,b,f,k,b,a],X).
X = 3

53
La première classe traite le cas de la liste vide. La seconde traite le cas où l’élé-
ment recherché est le premier de la liste. Dans ce cas, on résout récursivement le
prédicat avec le reste de la liste et on retourne le résultat obtenu + 1. Finalement, si
on n’a affaire à aucun de ces cas, on résout récursivement le prédicat et on retourne
la valeur obtenue.
Voyons maintenant le même programme, mais avec les deux dernières clauses
inversées :

num_occ(_,[],0).
num_occ(X,[_|Reste],Num):-
num_occ(X,Reste,Num).
num_occ(X,[X|Reste],Num):-
num_occ(X,Reste,N),
Num is N + 1.

Si on se limite à une interprétation déclarative, les deux programmes sont équi-


valents. Par contre, ce n’est pas du tout le cas, si on considère l’interprétation pro-
cédurale, où l’ordre des clauses est pris en compte. On obtient tout simplement une
réponse erronée :

?- num_occ(b,[a,b,f,b,f,k,b,a],X).
X = 0

La raison de ce comportement est que la seconde clause réussit toujours lorsque


la liste n’est pas vide. Alors, le processus récursif se poursuit toujours avec la
deuxième clause, jusqu’à qu’on atteigne la condition d’arrêt représentée par la pre-
mière clause.
Remarque : Le programme, même avec l’ordre correct des clauses, retourne de
nouvelles solutions erronées, si on force le retour arrière :

?- num_occ(b,[a,b,f,b],X).
X = 2 ;

X = 1 ;

X = 1 ;

X = 0 ;

No

54
Essayons de comprendre pourquoi on obtient ces réponses erronées. D’abord,
voici la situation lorsque la première solution est retournée (les points de choix sont
toujours indiqués en italique) :
num_occ(b,[a,b,f,b],X) ===> X = 2

X = Num

num_occ(b,[b,f,b],Num)

Num = Num1

num_occ(b,[f,b],N) Num1 is N+1

N = Num2

num_occ(b,[b],Num2)

Num2 = Num3

num_occ(b,[],N1) Num3 is N1+1

N1 = 0

num_occ(b,[],0)

Pour fournir la seconde solution, l’interpréteur Prolog recule au dernier point


de choix : le but num_occ(b,[b],Num2), qui peut être unifié avec la dernière
clause. La suite de la résolution nous amène donc au résultat suivant :

55
num_occ(b,[a,b,f,b],X) ===> X = 1

X = Num

num_occ(b,[b,f,b],Num)

Num = Num1

num_occ(b,[f,b],N) Num1 is N+1

N = Num2

num_occ(b,[b],Num2)

Num2 = Num3

num_occ(b,[],Num3)

Num3 = 0

num_occ(b,[],0)

Pour obtenir la troisième solution, on recule au dernier point de choix, soit le but
num_occ(b,[b,f,b],Num). En poursuivant la résolution à partir de ce point,
en utilisant cette fois-ci la dernière clause, on obtient la solution, avec en prime un
nouveau point de choix :

56
num_occ(b,[a,b,f,b],X) ===> X = 1

X = Num

num_occ(b,[b,f,b],Num)

Num = Num1

num_occ(b,[f,b],Num1)

Num1 = Num2

num_occ(b,[b],Num2)

Num2 = Num3

num_occ(b,[],N) Num3 is N + 1

N = 0

num_occ(b,[],0)

Finalement, si on reprend la résolution à partir du dernier point de choix qui s’est


ajouté, on obtient la dernière solution :

num_occ(b,[a,b,f,b],X) ===> X = 0

X = Num

num_occ(b,[b,f,b],Num)

Num = Num1

num_occ(b,[f,b],Num1)

Num1 = Num2

num_occ(b,[b],Num2)

Num2 = Num3

num_occ(b,[],Num3)

Num3 = 0

num_occ(b,[],0)

57
Pour éviter ces réponses additionnelles erronées, il faut mettre une condition sup-
plémentaire dans la dernière clause :

num_occ(_,[],0).
num_occ(X,[X|Reste],Num):-
num_occ(X,Reste,N),
Num is N + 1.
num_occ(X,[Y|Reste],Num):-
X \= Y,
num_occ(X,Reste,Num).

?- num_occ(b,[a,b,f,b],Z).
Z = 2 ;
No

Un autre exemple qui montre bien l’importance de l’ordre des clauses est la
seconde version du programme eliminer_rep, que nous avons vu à la section
8.1. Rappelons-nous que ce programme élimine les répétitions dans un liste :

eliminer_rep([],[]).
eliminer_rep([X|R],L):-
member(X,R),
eliminer_rep(R,L).
eliminer_rep([X|R],[X|L]):-
eliminer_rep(R,L).

Avant d’étudier les détails de l’exécution de ce programme, nous allons d’abord


considérer une version où les deux buts de la deuxième clause sont inversés :

eliminer_rep([],[]).
eliminer_rep([X|R],L):-
eliminer_rep(R,L),
member(X,L).
eliminer_rep([X|R],[X|L]):-
eliminer_rep(R,L).

Soit la requête suivante :

?- eliminer_rep([1,2,3,2],Z).
Z = [1,3,2]

58
Voici l’état de la résolution la première fois que l’interpréteur Prolog rencontre un
échec :
eliminer_rep([1,2,3,2],Z)

Z = L

eliminer_rep([2,3,2],L) member(1,L)

L = L1

eliminer_rep([3,2],L1) member(2,L1)

L1 = L2

eliminer_rep([2],L2) member(3,L2)

L2 = L3

eliminer_rep([],L3) member(2,L3)

L3 = []

ECHEC
eliminer_rep([],[])

Il tentera alors de retourner au dernier point de choix, c’est-à-dire le but eliminer_rep([2],L2)


qu’il reussira à unifier avec la dernière clause. La suite de la résolution entraînera
à nouveau un échec :

59
eliminer_rep([1,2,3,2],Z)

Z = L

eliminer_rep([2,3,2],L) member(1,L)

L = L1

eliminer_rep([3,2],L1) member(2,L1)

L1 = L2

eliminer_rep([2],L2) member(3,L2)

L2 = [2|L3]

eliminer_rep([],L3)
ECHEC

L3 = []

eliminer_rep([],[])

On recule donc au dernier point de choix, soit le but eliminer_rep([3,2],L1).


Encore une fois, on se butera à un échec :
eliminer_rep([1,2,3,2],Z)

Z = L

eliminer_rep([2,3,2],L) member(1,L)

L = L1

eliminer_rep([3,2],L1) member(2,L1)

L1 = [3|L2]

eliminer_rep([2],L2)

L2 = L3

eliminer_rep([],L3) member(2,L3)

L3 = []

ECHEC
eliminer_rep([],[])

60
Remarquez qu’un nouveau point de choix est apparu avec le but eliminer_rep([2],L2).
C’est à ce point de choix qu’on poursuivra la résolution, avec encore une fois un
échec, mais plus haut dans l’arbre de résolution :
eliminer_rep([1,2,3,2],Z)

Z = L

eliminer_rep([2,3,2],L) member(1,L)

L = L1

eliminer_rep([3,2],L1) member(2,L1)
ECHEC
L1 = [3|L2]

eliminer_rep([2],L2) member(2,[3,2])

L2 = [2|L3]

eliminer_rep([],L3)

L3 = []

eliminer_rep([],[])

Cette fois, on reculera au but eliminer_rep([2,3,2],L), que l’on unifiera


avec la dernière clause. Avec encore trois retours arrière, on finira par obtenir la
solution L = [2,3,2] pour ce but. Mais on aura encore un échec à la résolution
du but suivant, member(1,[2,3,2]). Alors, pour obtenir la solution finale, on
devra retourner au dernier point de choix disponible. Pour obtenir la solution finale
à partir de là, beaucoup de retours arrière seront encore nécessaires.
Comparons maintenant avec l’autre version (bien meilleure !) du même pro-
gramme :

eliminer_rep([],[]).
eliminer_rep([X|R],L):-
member(X,R),
eliminer_rep(R,L).
eliminer_rep([X|R],[X|L]):-
eliminer_rep(R,L).

Voici l’état de la résolution au moment du premier échec :

61
eliminer_rep([1,2,3,2],Z)

Z = L

member(1,[2,3,2]) eliminer_rep([2,3,2],L)

member(1,[3,2])

member(1,[2])

member(1,[])

ECHEC

On remarquera qu’il ne sera pas nécessaire de tenter de résoudre le but


eliminer_rep([2,3,2],L), puisque nous savons déjà que member(1,[2,3,2])
a échoué. On évite ainsi tous les retours arrières qui ont été effectués avec l’autre
version du programme.
Ainsi, l’interprète Prolog sait déjà qu’il doit tenter de nouveau à partir du point
choix eliminer_rep([1,2,3,2],Z). En unifiant avec la seconde clause, la
suite de la résolution nous amène à un nouvel échec :

62
eliminer_rep([1,2,3,2],Z)

Z = [1|L]

eliminer_rep([2,3,2],L)

L = L1

eliminer_rep([3,2],L1)
member(2,[3,2])

L1 = L2

OK
member(3,[2]) eliminer_rep([2],L2)

ECHEC

Maintenant, la résolution reprend au point de choix eliminer_rep([3,2],L1),


et tombe sur un nouvel échec :

63
eliminer_rep([1,2,3,2],Z)

Z = [1|L]

eliminer_rep([2,3,2],L)

L = L1

eliminer_rep([3,2],L1)
member(2,[3,2])

L1 = [3|L2]

eliminer_rep([2],L2)
OK

L2 = L3

member(2,[]) eliminer_rep([],L3)

ECHEC

Le prochain point de choix considéré est le but eliminer_rep([2],L2).


Cette fois, la résolution se termine avec succès :

64
eliminer_rep([1,2,3,2],Z) ===> Z = [1,3,2]

Z = [1|L]

eliminer_rep([2,3,2],L)

L = L1

eliminer_rep([3,2],L1)
member(2,[3,2])

L1 = [3|L2]

eliminer_rep([2],L2)
OK
L2 = [2|L3]

eliminer_rep([],L3)

L = []

eliminer_rep([],[])

Remarquez qu’il reste un point de choix. Par conséquent, si nous demandons une
nouvelle solution, l’interpréteur reculera à ce point de choix et retournera une nou-
velle solution (erronée).

10 Manipulation dynamique de programmes


En Prolog, il est possible qu’un programme soit modifié dynamiquement. Plus
précisément, un programme peut se modifier lui-même en ajoutant ou en retirant
des clauses. Il n’est pas facile de fournir un exemple simple de programme qui se
modifie lui-même, mais nous verrons les prédicats qui permettent ce procédé :
assert, qui ajoute une nouvelle clause, et retract, que retire la première
clause s’unifiant avec l’argument.
Supposons un programme pour calculer la somme d’une liste de produits :

prix(pomme, 0.50).
prix(riz, 1.40).
prix(pain, 2.70).

65
cout_total([],0).
cout_total([X|Xs],N):-
cout_total(Xs,N2),
prix(X,P),
N is N2 + P.

?- cout_total([pomme,pain],X).
X = 3.2

Si nous voulons ajouter le lait dans la liste des produits qui ont un prix, nous
exécuterions la requête suivante :

assert(prix(lait,1.10)).

Maintenant, nous pouvons faire une requête qui aurait échoué auparavant :

?- cout_total([pomme,pain,lait],X).
X = 4.3

Pour modifier le prix d’un produit, il est nécessaire de retirer une clause et d’en
ajouter un nouvelle qui indique le nouveau prix :

?- retract(prix(pomme,_)), assert(prix(pomme,0.60)).
Yes

Jusqu’à maintenant, tous les exemples concernent des faits. Mais il est possible de
manipuler aussi des clauses. Voyez par exemple comment nous pourrions modifier
le programme pour que la prix des oranges soit toujours identique au prix des
pommes :

?- assert(prix(orange,P):- prix(pomme,P)).
Yes

Remarque : Par défaut, SWI-Prolog ne permet pas d’utiliser assert et retract


pour des prédicats qui sont déjà définis dans un programme. Pour pouvoir le faire, il
faut que ces prédicats soient déclarés dynamiques, au début du programme. Pour ce
faire, on utilise l’expression :-dynamic suivie de tous les prédicats dynamiques
avec leur arité (c’est-à-dire leur nombre d’arguments). Dans notre exemple, nous
avons un prédicat dynamique d’arité 2 :

:- dynamic prix/2.

66
11 Recupération de solutions multiples
11.1 Utilisation du retour arrière
Pour obtenir toutes les solutions d’une résolution, la seule manière que nous
connaissons jusqu’à maintenant est l’utilisation répétitive du point-virgule après
chaque solution retournée. Soit le programme suivant :

homme(roberto).
homme(paulo).
homme(joao).
homme(armando).
femme(lucia).
femme(clara).
pere(pauo,roberto).
pere(paulo,joao).
pere(armando,lucia).
pere(armando,clara).

Pour identifier tous les enfants de Paulo, nous utiliserions la méthode suivante :

?- pere(paulo,X).
X = roberto ;
X = joao ;
No

Mais cela ne fonctionne pas si nous désirons récupérer toutes les solutions pour
en faire un traitement. Par exemple, si nous voulons calculer le nombre d’enfants
que Paulo a, il nous faut une manière de mettre tous les enfants dans une liste et
calculer le nombre d’items que cette liste contient. On pourrait toujours résoudre
le problème en utilisant des prédicats dynamiques :

tous_les_enfants(X,L):-
assert_enfants(X),
retract_enfants(L).

assert_enfants(X):-
pere(X,F),
assert(enfant(F)),
fail.
assert_enfants(_).

67
retract_enfants([F|Fs]):-
retract(enfant(F)),
retract_enfants(Fs).
retract_enfants([]):-
\+ enfant(_).
Ce programme résout le problème en deux étapes. Dans une première étape, il
ajoute un fait pour chaque enfant de X. Remarquez l’usage de fail, pour forcer le
retour arrière, afin d’identifer tous les enfants de X (le prédicat assert n’a jamais
de point de choix). À la seconde étape, il retire tous les faits qui ont été ajoutés, et
les met dans une liste.

11.2 Prédicat findall


Le problème avec la solution précédente est qu’elle est complexe. Pour ré-
cupérer la même information, il est beaucoup plus simple d’utiliser le prédicat
findall. Ainsi, pour obtenir la liste de tous les enfants de Paulo, nous exécute-
rions la requête suivante :
?- findall(X,pere(paulo,X),L).
L = [roberto,joao]
Le fonctionnement est le suivant. L’interprète Prolog tente de toutes les ma-
nières possibles de résoudre le but qui se retrouve comme second argument de
findall. Plus précisément, il cherchera une première solution, puis fera des re-
tours arrière jusqu’à ce qu’il n’y ait plus de solution. Pour chaque tentative réussie,
il ajoute dans une liste le résultat des instanciations du premier argument. Fina-
lement, le dernier argument, qui doit toujours être une variable, sera unifié avec
la liste de solutions. L’ordre des solutions dans la liste correspondra à l’ordre des
solutions trouvées lors de la résolution. S’il n’y a aucune solution, on obtiendra
la liste vide. Pour revenir à notre exemple, voici le programme pour calculer le
nombre d’enfants :
nombre_enfants(Personne,Num):-
findall(F,pere(Personne,F),Enfants),
length(Enfants,Num).

?- nombre_enfants(paulo,N).
N = 2
?- nombre_enfants(joao,N).
N = 0

68
Remarque : le prédicat length retourne 0 pour une liste vide.
Il n’est pas nécessaire que le premier argument soit une variable. Il peut être une
structure plus complexe. Supposons par exemple que nous voulions savoir qui sont
toutes les personnes impliquées dans une relation père-enfant. Dans ce cas, on peut
utiliser findall, en utilisant une paire de variables comme premier argument :

?- findall([X,Y],pere(X,Y),L).
L = [[paulo,roberto],[paulo,joao],
[armando,lucia],[armando,clara]]

Le second argument peut être une conjonction de plusieurs buts. Par exemple,
si nous désirons la liste de toutes les personnes qui ont une fille, nous exécuterions
la requête suivante :

?- findall(X,(pere(X,F),femme(F)),L).
L = [armando,armando]

La liste obtenue contient deux occurrences de Armando parce qu’il y a deux so-
lutions possibles qui impliquent Armando (il a deux filles). Pour éviter cette redon-
dance, on peut substituer la conjonction de buts par un prédicat a_une_fille
défini de la manière suivante :

a_une_fille(Personne):-
findall(X,pere(Personne,X),Enfants),
findall(X,(member(X,Enfants),femme(X)),Filles),
Filles \= [].

?- a_une_fille(paulo)
No
?- a_une_fille(armando)
Yes

Ce programme fonctionne ainsi : il extrait la liste de tous les enfants, puis


exrait de cette liste tous ceux qui sont des femmes, pour finalement vérifier que
cette dernière liste n’est pas vide. Le problème avec ce programme est qu’il ne
fonctionnera pas correctement si on l’appelle avec une variable libre. Il retournera
avec succès, mais sans instancier la variable :

?- a_une_fille(X)
X = _G109

69
Nous n’obtenons pas le résultat espéré (X = armando) à cause de la séman-
tique du prédicat findall. Par définition, les variables contenues dans les deux
premiers arguments du prédicat demeurent libres au retour de la résolution. Pour
solutionner notre problème, nous pouvons modifier le programme, en ajoutant un
prédicat qui vérifie si X est un hommme :

a_une_fille(Personne):-
homme(Personne),
findall(X,pere(Personne,X),Fils),
findall(X,(member(X,Fils),femme(X)),Filles),
Filles \= [].

?- a_une_fille(X)
X = armando ;
No

Maintenant, nous pouvons obtenir la liste non redondante des hommes qui ont une
fille :

?- findall(X,a_une_fille(X),L).
L = [armando]

Nous verrons, plus loin, un mécanisme de contrôle, le coupe-choix, qui offre


une solution plus directe pour éliminer les solutions redondantes.

11.3 Prédicats bagof et setof


Tout comme le prédicat findall, les prédiats bagof et setof retournent
toutes les instanciations d’un terme telles que les instanciations des variables qu’il
contient satisfont aussi le but passé en second argumant. Deux choses distinguent
ces prédicat de findall :
— S’il n’y a aucune manière de trouver une telle instanciation, le prédicat
échoue, au lieu de retourner la liste vide.
— Si le but contient des variables qui ne se retrouvent pas dans le premier
terme, on ne retourne que les solutions liées à une instanciation spécifique
de ces variables.
Ce qui distingue setof de bagof est que le premier élimine les solutions
redondantes.
Voici un exemple qui illustre l’usage de bagof :

70
p(1).
p(2).
p(3).

q(1,a).
q(1,b).
q(2,c).
q(2,d).

?- bagof(Y,(p(X),q(X,Y)),L).

Y = _G173
X = 1
L = [a, b] ;

Y = _G173
X = 2
L = [c, d] ;
No

Revenons maintenant au prédicat a_une_fille que nous avons défini aupa-


ravant en utilisant findall :

a_une_fille(Personne):-
findall(X,pere(Personne,X),Enfants),
findall(X,(member(X,Enfants),femme(X)),Filles),
Filles \= [].

Il est important de noter, ici, que nous voulons un échec si la personne n’a pas
d’enfant, ou si elle a des enfants mais qu’aucun n’est une fille. Ceci suggère une
meilleure solution qui utlise le prédicat bagof :

a_une_fille3(Personne):-
bagof(X,(pere(Personne,X),femme(X)),_).

Remarquez que ce prédicat échoue si la personne n’a pas de fille. Si elle a une
fille, la résolution se termine avec succès, et on ne s’interesse pas à la liste retournée
par le prédicat bagof.

71
11.4 Exercices
11.1 Nous voulons utiliser le prédicat sortir de l’exercice 3.2 pour écrire un
programme qui retourne la liste de toutes les salles connectées avec l’extérieur.
Voici un programme qui ne fonctionne pas :

salles_connectees_avec_exterieur(Salles):-
findall(S,sortir(S),Salles).

Identifiez le problème et écrivez une version qui fonctionne.

11.2 En utilisant le prédicat findall, écrivez un programme qui reçoit deux listes
et retourne une liste de tous les éléments qui apparaissent dans les deux listes :

?- dans_les_deux_listes([a,b,f,g,z],[c,b,k,l,g,r],L).
L = [b,g]
?- dans_les_deux_listes([a,z],[c,b,k,l,g,r],L).
L = []

11.3 Que fait le programme suivant ?

mystere(L1,L2,Res):-
findall(X*Y,(member(X,L1),member(Y,L2)),L3),
traiter(L3,Res).

traiter([],0).
traiter([X|Reste],N):-
traiter(Reste,N1),
N is X+N1.

11.4 Utilisez assert et retract pour définir le prédicat nombre_enfants.

12 Mécanismes de contrôle
12.1 Coupe-choix :
Le coupe-choix (cut) est le prédicat " !", qui est utilisé pour couper l’arbre de
recherche quand un retour arrière est effectué. Principalement, on l’utilise pour
empêcher qu’un programme ne tente de retourner une nouvelle solution. L’effet du
coupe-choix est le suivant :

72
Quand l’interpréteur Prolog rencontre un coupe-choix, il oublie
tous les points de choix qui ont été ajoutés lors de l’unification de la
tête de la clause et le but résolu, ainsi que lors de la résolution de tous
les buts qui se trouvent avant le coupe-choix dans le corps de la clause.
Pour mieux comprendre le principe du coupe-choix, voyons quelques exemples.
Voici un programme qui calcule la somme de tous les nombres de 1 à n :

somme_jusque(1,1).
somme_jusque(N,S):-
M is N-1,
somme_jusque(M,P),
S is P + N.

Le problème avec ce programme est qu’il entrera dans une boucle infinie si on
effectue un retour arrière :

?- somme_jusque(3,X).
X = 6 ;
boucle infinie...

Ceci se produit parce qu’au moment de la résolution du but somme_jusque(1,S1),


l’unification s’est faite avec la première clause, ce qui a entraîné la mémorisation
d’un point de choix, puisqu’il est aussi possible d’unifier avec la seconde clause.
Lors du retour arrière, l’interpréteur retournera à ce point de choix et poursuivra la
résolution avec la seconde clause, ce qui causera la boucle infinie. Pour éviter ce
problème, il suffit d’ajouter un coupe-choix à la première clause :

somme_jusque(1,1):-!.
somme_jusque(N,S):-
M is N-1,
somme_jusque(M,P),
S is P + N.

Ainsi, quand nous demanderons un retour arrière, l’interpréteur ne tentera pas


l’unification avec la seconde clause.
Un autre exemple est un programme qui extrait d’une liste de personnes celles
qui sont de jeunes pères :

pere(roberto,ana).
pere(paulo,romeu).

73
pere(michel,tomas).
pere(roberto,francisco).
age(roberto,23).
age(michel,35).
age(paulo,53).
age(tomas,7).

peres_jeunes([],[]).
peres_jeunes([X|Reste],[X|R]):-
pere(X,_),
age(X,I),
I < 36,
peres_jeunes(Reste,R).
peres_jeunes([_|Reste],R):-
peres_jeunes(Reste,R).

Le problème avec ce programme est qu’il peut retourner plus d’une solution :

?- peres_jeunes([roberto,paulo,michel],X).
X = [roberto, michel] ;
X = [roberto] ;
X = [roberto, michel] ;
X = [roberto] ;
X = [michel] ;
X = [] ;
No

La situation qui prévaut quand l’interprète Prolog retourne la première solution


est telle qu’illustré à la figure 5 (les buts qui possèdent un point de choix sont
indiqués en italique).

Le premier point de choix qui sera considéré, lors du retour arrière, est le but
parents_jeunes([michel],R), qui pourra être réunifié avec la seconde
clause. La poursuite de la résolution à partir de ce point amènera l’ajout du but
peres_jeunes([],R) dans la résolvante, sans ajouter la constante michel
dans la liste de résultats.
Au second retour arrière, on recule au but pere(roberto,_), qui réussira à
nouveau, en unifiant avec le fait pere(roberto,francisco). On obtiendra
à nouveau la première solution, avec encore une fois un point de choix au but
peres_jeunes([michel],R).

74
peres_jeunes([roberto,paulo,michel],X)

X = [roberto | R]

pere(roberto,_) age(roberto,23) 23<36 peres_jeunes([paulo,michel],R)

R = R1
pere(roberto,ana)

peres_jeunes([michel],R1)

R=[michel | R2]

pere(michel,_) age(michel,35) 35<36 peres_jeunes([ ],R2)

R2=[ ]

peres_jeunes([ ],[ ])

F IGURE 5 – Arbre de résolution

75
Finalement, il restera le point de choix associé au but initial, qui peut être unifié
avec la seconde clause. En redémarrant à partir de ce point, on obtient une nouvelle
solution : [michel]. Pour arriver à cette solution, un nouveau point de choix a
été ajouté, comme l’illustre la figure 6.

peres_jeunes([roberto,paulo,michel],X)

peres_jeunes([paulo,michel],X)

peres_jeunes([michel],R)

R=[michel | R2]

pere(michel,_) age(michel,35) 35<36 peres_jeunes([ ],R2)

R2=[ ]

peres_jeunes([ ],[ ])

F IGURE 6 – Arbre de résolution

Lors du dernier retour arrière, on repartira donc de ce dernier point de choix


pour redémarrer la résolution, ce qui amènera à unifier la variable R avec la liste
vide.
La solution pour éviter toutes ces réponses erronées est l’utilisation du coupe-
choix avant l’appel récursif de la seconde clause (on peut aussi le mettre après
le dernier but de la clause et obtenir le même effet). Ceci signifie que quand on
trouvera un jeune père, il ne sera jamais possible de rechercher d’autres solutions
pour ce cas :

peres_jeunes([],[]).
peres_jeunes([X|Reste],[X|R]):-
pere(X,_),

76
age(X,I),
I < 36,!,
peres_jeunes(Reste,R).
peres_jeunes([_|Reste],R):-
peres_jeunes(Reste,R).

Il faut bien comprendre que dans cet exemple, il y a deux endroits dans le pro-
gramme qui sont des points de choix : la seconde clause et le but pere(X,_) dans
la seconde clause. Le coupe-choix qui a été ajouté résout une partie du problème.
Mais le programme est encore inefficace. Si on a un père qui a plus d’un fils et dont
l’âge est supérieur à 36 ans, on résoudra plusieurs fois le but pere(X,_) avec ce
père, pour échouer à chaque fois lors du test I < 36. On ne peut pas résoudre
ce problème en mettant le coupe-choix immédiatement après le but pere(X,_)
(pourquoi ?). Pour résoudre le problème, il faut plutôt définir un nouveau prédi-
cat est_un_pere et mettre le coupe-choix dans la définition de ce prédicat. Le
nouveau programme aurait alors la forme suivante :

peres_jeunes([],[]).
peres_jeunes([X|Reste],[X|R]):-
est_un_pere(X),
age(X,I),
I < 36,!,
peres_jeunes(Reste,R).
peres_jeunes([_|Reste],R):-
peres_jeunes(Reste,R).

est_un_pere(X):-
pere(X,_),!.

Pour éliminer le point de choix qui apparaît au moment de l’unification avec la


seconde clause sans utiliser de coupe-choix, on peut modifier le programme de la
manière suivante :

pere_jeune(X):-
pere(X,_),
age(X,I),
I < 36.

peres_jeunes([],[]).
peres_jeunes([X|Reste],[X|R]):-

77
pere_jeune(X),
peres_jeunes(Reste,R).
peres_jeunes([X|Reste],R):-
\+ pere_jeune(X),
peres_jeunes(Reste,R).

Malheureusement, cette solution n’est pas optimale. Nous avons éliminés les
réponses erronées, mais pas les réponses redondantes, qui sont dues au fait qu’un
père peut avoir plusieurs enfants. En d’autres mots, on a toujours un point de choix,
au but pere(X,_), qui se trouve dans la définition du prédicat pere_jeune.
Pour éliminer ce point de choix, nous pourrions redéfinir le prédicat pere_jeune,
en utilisant findall pour extraire tous les fils et en vérifiant que la liste résultante
ne soit pas vide. Cela fonctionne parce que le prédicat findall n’ajoute jamais
de point de choix :

pere_jeune(X):-
findall(F,pere(X,F),Fils),
Fils \= [],
age(X,I),
I < 36.

peres_jeunes([],[]).
peres_jeunes([X|Reste],[X|R]):-
pere_jeune(X),
peres_jeunes(Reste,R).
peres_jeunes([X|Reste],R):-
\+ pere_jeune(X),
peres_jeunes(Reste,R).

Nous avons finalement obtenu une version du programme sans coupe-choix,


mais il s’agit d’un programme qui est moins efficace (à cause de l’utilisation du
prédicat findall). En effet, il est inutile d’identifier la liste de tous les fils. Il
suffit de vérifier qu’il en existe un.
On voit qu’il a fallu modifier sensiblement le programme pour éviter l’utilisa-
tion du coupe-choix, et le résultat n’est pas un programme plus facile à comprendre.
En plus, le prédicat pere_jeune est appelé une deuxième fois, soit dans la troi-
sième clause de la définition de peres_jeunes, si la résolution échoue avec la
deuxième clause. En conclusion, il est préférable d’utiliser le coupe-choix dans cet
exemple. D’autant plus qu’il ne modifie en rien la sémantique du programme. Il ne
fait qu’éliminer des solutions erronées et/ou redondantes.

78
Il est très important de comprendre que l’effet du coupe-choix est d’éliminer
tous les points de choix qui ont été ajoutées depuis le moment où la tête de la clause
a été unifiée. Pour mieux comprendre, considérons le programme suivant :

p(1).
p(2).
d(2).
f(1).
c(2).
b(1).
b(2).

r(X):- p(X).
r(X):- f(X).

q(X):- r(X),!,c(X).
q(X):- b(X).

pred(X):- q(X),f(X).
pred(X):- d(X).

Soit la requête suivante :

?- pred(1).
No

La figure 7 illustre l’arbre de résolution au moment où l’on arrive au coupe-


choix. À ce moment-là, il y a trois points de choix. Une fois le coupe-choix traité,
le point de choix associé à son noeud parent et tous les points de choix dans les
sous arbres à sa gauche sont éliminés. Ceci signifie que les points de choix r(1)
et q(1) seront éliminés, mais pas le point de choix pred(1).
Après le traitement du coupe-choix, la résolution du but c(1) échoue. On
effectue alors un retour arrière à l’unique point de choix qui subsiste. La résolution
démarre à partir de ce point, en tentant d’unifier le but avec la seconde clause du
prédicat pred. Ceci mènera à un nouvel échec, puisqu’il ne sera pas possible de
résoudre le but d(1).
Considérons maintenant une version un peu différente du programme, où le
coupe-choix apparaît dans la définition du prédicat pred :

79
pred(1)

q(1) f(1)

r(1) ! c(1)

p(1)

OK

F IGURE 7 – Exemple d’utilisation de coupe-choix

p(1).
p(2).
d(2).
f(1).
c(2).
b(1).
b(2).

r(X):- p(X).
r(X):- f(X).

q(X):- r(X),c(X).
q(X):- b(X).

pred(X):- q(X),!,f(X).
pred(X):- d(X).
Soit la requête suivante :
?- pred(2).
La figure 8 montre la situation lorsqu’on arrive au coupe-choix. Dans ce cas,
tous les points de choix seront retirés. Quand le but suivant, soit f(2), sera traité,
on se butera à un échec, sans aucune possibilité de retour arrière.

80
pred(2)

q(2) ! f(2)

r(2) c(2)

p(2) OK

OK

F IGURE 8 – Exemple d’utilisation de coupe-choix

12.2 Dangers avec le coupe-choix


Le coupe-choix est un outil qui peut s’avérer dangereux, spécialement quand
on développe un programme en ayant en tête une interprétation procédurale. Le
danger est d’obtenir un programme dont la sémantique diffère de celle que nous
voulions obtenir. Plus précisément, le programme peut ne pas fonctionner dans
certaines situations qui n’ont pas été considérées lors du développement.
Considérons, par exemple, un programme pour identifier le plus petit de deux
nombres :

min(X,Y,X):-
X =< Y, !.
min(X,Y,Y).

L’auteur de ce programme a réalisé que si la résolution réussit avec la première


clause, il faut mettre un coupe-choix pour éviter une seconde réponse erronée en
cas de retour arrière. Si la résolution échoue avec la première clause, cela signifie
que Y est plus petit que X. Il n’est donc pas nécessaire d’ajouter un test dans la
seconde clause :

?- min(2,3,X).
X = 2 ;
No

81
?- min(5,2,X).
X = 2
Ce type de coupe-choix est un "coupe-choix rouge", c’est-à-dire un coupe-
choix dangereux qu’il vaudrait mieux éviter. Le problème est que le programmeur
n’a pensé qu’à une utilisation où le dernier argument est toujours une variable libre.
Il n’a pas pensé à la possibilité d’utiliser de prédicat pour vérifier qu’un nombre
donné est le plus petit de deux nombres :
?- min(2,5,5).
Yes
On voit que dans ce cas, la résolution retourne avec succès, alors qu’elle aurait
du échouer. Pour éviter cela, nous pourrions ajouter un test :
min(X,Y,Z):-
X =< Y,
!,
Z=X.
min(X,Y,Y).
Mais cette solution rend le programme moins compréhensible. Il est important
de remarquer que ce problème ne se serait pas produit si le programmeur avait
utilisé une version plus intuitive, sans coupe-choix :
min(X,Y,X):-
X =< Y.
min(X,Y,Y):-
X > Y.
Remarquez qu’on peut toujours utiliser un coupe-choix pour rendre le pro-
gramme plus efficace, sans en changer la sémantique :
min(X,Y,X):-
X =< Y,!.
min(X,Y,Y):-
X > Y.
Ici, le coupe-choix ne fait qu’éviter une réunification avec la tête de la seconde
clause qui, de toute façon, entraînerait un échec lors de la résolution du but qui se
trouve dans le corps de cette clause.
Conclusion : Quand on introduit un coupe-choix dans la définition d’un pré-
dicat, il est très important de considérer l’effet de celui-ci dans les différentes ma-
nières d’appeler ce prédicat.

82
12.3 Négation :
En Prolog, il n’y a pas à proprement parler de négation. Il n’y a que l’opérateur
\+ qui représente la négation par échec. Ceci signifie que la résolution de
\+ But réussira seulement s’il n’existe aucune manière de résoudre But. Le but
ne peut pas être une variable libre :

?- \+ X .
Error
?- \+ member(3,[2,3,4])).
No
?- \+ member(3,[4,5,6])).
Yes

Il est très facile de définir la négation par échec en Prolog, en utilisant le prédi-
cat prédéfini fail :

neg_echec(X):- X,!,fail.
neg_echec(X).

Pour se convaincre que la négation par échec implémentée en Prolog n’est pas
équivalente à la négation en logique, il suffit de considérer le programme suivant :

etudiant_ancien(X):-
\+ nouveau(X),
etudiant(X).

nouveau(paulo).
etudiant(jorge).

Si on exécute la requête etudiant_ancien(X), nous n’obtenons aucune


solution, malgré le fait qu’il en existe une (X = jorge). Remarquez qu’en interchan-
geant l’ordre des deux buts dans le corps de la clause, nous obtiendrions la réponse
correcte.

12.4 Exercices
12.1 En utilisant le coupe-choix, modifiez le programme de l’exercice 8.14 pour
éliminer les solutions multiples.

12.2 Soit le programme suivant :

83
m(a).
m(b).
m(k).
p(a,b).
p(a,c).
p(b,d).
p(b,e).
p(k,e).
q(c).
q(d).
q(e).

pred1(X,Y):-
m(X),
pred2(X,Y).

pred2(X,Y):-
p(X,Y),
!,
q(Y).
Quel sera le résultat de la requête suivante :

?- pred1(X,Z).

Et quel sera le résultat si on demande une seconde solution ?

12.3 Soit le programme suivant :

m(a).
m(c).
q(a).
r(a).
r(b).
r(c).

p(X):-
q(X),
!,
fail.

84
p(X):-
r(X).

s(X):-
m(X),
p(X).

a) Quel sera le résultat de la requête suivante :

?- s(X).

b) Quel sera le résultat si on demande une seconde solution ?

12.4 Pourquoi l’ajout d’un coupe-choix après le but pere(X,_) ne résout pas le
problème d’efficacité dans la définition de peres_jeunes ?

13 Quelques prédicats prédéfinis


consult(+File)

Ouvre un fichier File et interprète les clauses qu’il contient.

13.1 Vérification du type d’un terme


var(+Terme)

Réussit si Terme est une variable libre (non instanciée).

?- var(X).
Yes
?- var(23).
No
?- X=Y, Y=23, var(X).
No

nonvar(+Terme)

Réussit si Terme est une variable instanciée.


Exemple : Définition d’un prédicat plus(X,Y,Z), qui peut aussi être utilisé pour
faire la soustraction :

85
plus(X,Y,Z):-
nonvar(X),
nonvar(Y),
Z is X+Y.
plus(X,Y,Z):-
nonvar(X),
nonvar(Z),
Y is Z-X.
plus(X,Y,Z):-
nonvar(Y),
nonvar(Z),
X is Z-Y.

?- plus(3,4,R).
R = 7
?- plus(R,5,11).
R = 6
?- plus(X,Y,11).
No

integer(+Terme)

Réussit si Terme est un nombre entier.

float(+Terme)

Réussit si Terme est un nombre réel.

number(+Terme)

Réussit si Terme est un nombre.

atom(+Terme)

Réussit si Terme est une constante qui n’est pas un nombre.

?- atom(23).
No
?- atom(rodrigo).
Yes

86
?- atom(’UFPR’).
Yes
?- atom(X).
No
?- atom(suc(suc(0))).
No
?- atom("Estou chegando").
No

atomic(+Terme)

Réussit si Terme est une constante, qui peut éventuellement être un nombre.

?- atomic(23).
Yes
?- atomic(rodrigo).
Yes
?- atomic(’UFPR’).
Yes
?- atomic(X).
No
?- atomic(suc(suc(0))).
No

13.2 Comparaison et unification de termes


+Terme1 = +Terme2

Réussit si les deux termes peuvent être unifiés.

+Terme1 \= +Terme2

Réussit si les deux termes ne peuvent pas être unifiés.

14 Conseils pour bien programmer en Prolog


14.1 Erreurs fréquentes
Voici quelques erreurs fréquentes que l’on rencontre dans des programmes Pro-
log :

87
— Fautes de frappes, dans les noms de prédicat, par exemple. Le programme
compile, mais ne s’exécute pas correctement. Soit l’exemple suivant :
nombre_elme([],0).
nombre_elem([X|Reste],Z):-
nombre_elem(Reste,N),
Z is N+1.
La résolution échouera toujours, puique le prédicat nombre_elem n’a
pas de clause pour la liste vide.

14.2 Trace d’exécution


Voici un exemple de programme erroné :

pred(X,Y):-
mystere1(X,Z),
mystere2(Z,Y).

mystere1([],[]).
mystere1([X|Reste],Z):-
X < 0,
mystere1(Reste,Z).
mystere1([X|Reste],[X|Z]):-
mystere1(Reste,Z).

mystere2([],[]).
mystere2([X|Reste],[X|ResteFiltre]):-
filtre(X),
mystere2(Reste,ResteFiltre).

filtre(X):-
W is X mod 2,
W = 0.

La requête suivante devrait retourner un résultat, mais elle échoue :

?- pred1([5,4,-3],X).
No

88
Pour déboguer le programme, on peut demander une trace de la résolution,
en appelant auparavant le prédicat prédéfini trace. L’interpréteur exécutera alors
la résolution pas à pas. À chaque pas, il montre le prochain but à résoudre, de la
manière suivante :
Call: (niveau de profondeur dans l’arbre de résolution)
But
Il attend alors une action de notre part. Si on appuie sur la touche <enter>, il tente
l’unification, met à jour la résolvante, et montre le prochain but à résoudre.
Quand l’interpréteur sort de la résolution d’un but, il y a trois cas possibles.
L’échec, indiqué par la forme suivante :
Fail:(niveau de profondeur) But
Le succès, indiqué par la forme suivante :
Exit:(niveau de profondeur) But avec variables instanciées.
S’il y a échec, l’interprète recule au dernier point de choix, et affiche la ligne
suivante :
Redo:(niveau de profondeur) But qui correspond au dernier
point de choix .
Si on appuie à nouveau sur la touche <enter>, il tentera de redémarrer la ré-
solution à partir de ce point de choix. Voici la trace d’exéxution du programme
ci-dessus :

?- trace.

Yes

?- pred([5,4,-3],X).
Call: ( 8) pred1([5, 4, -3], _G200) ?
Call: ( 9) mystere1([5, 4, -3], _L168) ?
Call: ( 10) 5<0 ?
Fail: ( 10) 5<0 ?
Redo: ( 9) mystere1([5, 4, -3], _L168) ?
Call: ( 10) mystere1([4, -3], _G304) ?
Call: ( 11) 4<0 ?
Fail: ( 11) 4<0 ?
Redo: ( 10) mystere1([4, -3], _G304) ?
Call: ( 11) mystere1([-3], _G307) ?
Call: ( 12) -3<0 ?
Exit: ( 12) -3<0 ?
Call: ( 12) mystere1([], _G307) ?
Exit: ( 12) mystere1([], []) ?
Exit: ( 11) mystere1([-3], []) ?
Exit: ( 10) mystere1([4, -3], [4]) ?
Exit: ( 9) mystere1([5, 4, -3], [5, 4]) ?
Call: ( 9) mystere2([5, 4], _G200) ?

89
Call: ( 10) filtre(5) ?
^ Call: ( 11) _L234 is 5 mod 2 ?
^ Exit: ( 11) 1 is 5 mod 2 ?
Call: ( 11) 1=0 ?
Fail: ( 11) 1=0 ?
Fail: ( 10) filtre(5) ?
Fail: ( 9) mystere2([5, 4], _G200) ?
Redo: ( 11) mystere1([-3], _G307) ?
Call: ( 12) mystere1([], _G310) ?
Exit: ( 12) mystere1([], []) ?
Exit: ( 11) mystere1([-3], [-3]) ?
Exit: ( 10) mystere1([4, -3], [4, -3]) ?
Exit: ( 9) mystere1([5, 4, -3], [5, 4, -3]) ?
Call: ( 9) mystere2([5, 4, -3], _G200) ?
Call: ( 10) filtre(5) ?
^ Call: ( 11) _L193 is 5 mod 2 ?
^ Exit: ( 11) 1 is 5 mod 2 ?
Call: ( 11) 1=0 ?
Fail: ( 11) 1=0 ?
Fail: ( 10) filtre(5) ?
Fail: ( 9) mystere2([5, 4, -3], _G200) ?
Fail: ( 8) pred1([5, 4, -3], _G200)

Avec cet exemple, on peut voir que la résolution du but mystere1 réussit,
après deux retours arrière. Le premier but que l’on tente de résoudre est mystere2,
sans succès. L’interprète recule alors au dernier point de choix, qui est le but
mystere1([-3], _G307). Ce but peut s’unifier avec la tête de la dernière
clause du prédicat mystere1, retournant plus d’un résultat, mais la nouvelle
tentative de résolution de mystere2 échoue. Donc, la résolution de la requête
échoue, puisqu’il n’y a plus de point de choix.
Il est possible d’obtenir une trace plus courte. En effet, pour chaque but, on
peut demander qu’il soit résolu sans fournir les détails de la résolution, en entrant
la commande s (skip), au lieu d’appuyer sur la touche <enter>. Voici une trace du
même programme, mais sans les détails des première tentatives de résolution des
buts mystere1([5,4,-3],_L168]) et mystere2([5, 4], _G200) :

?- pred1([5,4,-3],X).
Call: ( 8) pred1([5, 4, -3], _G200) ?
Call: ( 9) mystere1([5, 4, -3], _L168) ? s
Exit: ( 9) mystere1([5, 4, -3], [5, 4]) ?
Call: ( 9) mystere2([5, 4], _G200) ? s
Fail: ( 9) mystere2([5, 4], _G200) ?
Redo: ( 11) mystere1([-3], _G313) ?
Call: ( 12) mystere1([], _G316) ?
Exit: ( 12) mystere1([], []) ?
Exit: ( 11) mystere1([-3], [-3]) ?

90
Exit: ( 10) mystere1([4, -3], [4, -3]) ?
Exit: ( 9) mystere1([5, 4, -3], [5, 4, -3]) ?
Call: ( 9) mystere2([5, 4, -3], _G200) ?
Call: ( 10) filtre(5) ?
^ Call: ( 11) _L193 is 5 mod 2 ?
^ Exit: ( 11) 1 is 5 mod 2 ?
Call: ( 11) 1=0 ?
Fail: ( 11) 1=0 ?
Fail: ( 10) filtre(5) ?
Fail: ( 9) mystere2([5, 4, -3], _G200) ?
Fail: ( 8) pred1([5, 4, -3], _G200) ?

No

Pour bien comprendre cette trace, comparez-la à l’arbre de résolution. La si-


tuation au moment du premier échec est illustrée à la figure 9 (point de choix en
italique).

pred([5,4,−3],X)

mystere2(Z,X)
mystere1([5,4,−3],Z)
X = [5|ResteFiltre]
Z = [5|Z1]

filtre(5) mystere2([4],ResteFiltre)
mystere1([4,−3],Z1)

Z1 = [4|Z2]

ECHEC
mystere1([−3],Z2)

−3 < 0 mystere1([],Z2)

Z = []

mystere1([],[])

F IGURE 9 – Arbre de résolution

On voit qu’il n’y a qu’un seul point de choix : le but mystere1([-3], Z2)
(qui correspond au but mystere1([-3], _G313) dans la trace). L’interprète
effectue un retour arrière à ce point de choix, et ajoute le but mystere1([],

91
Z3) dans la résolvante. On obtiendra alors la liste Z3, tel qu’illustré à la figure 10.

pred([5,4,−3],X)

mystere2(Z,X)
mystere1([5,4,−3],Z)

Z = [5|Z1]

mystere1([4,−3],Z1)

Z1 = [4|Z2]

mystere1([−3],Z2)

Z2 = [4|Z3]

mystere1([],Z3)

Z3 = []

mystere1([],[])

F IGURE 10 – Arbre de résolution

Remarquez qu’il n’y a plus de point de choix. La résolution du second but de la


clause pred sera tentée de nouveau. Le but à résoudre aura la forme mystere2([5,4,-3],X),
qui n’aura pas plus de succès.

92