Aller au contenu

Maîtrisez la coupure

Le retour arrière est la grande force de Prolog : il explore toutes les pistes à votre place. Mais c’est parfois une faiblesse. Quand vous savez qu’une piste est la bonne, inutile de garder les autres en réserve ; et quand un choix exclut les suivants (« si l’enfant a moins de 14 ans, tarif enfant, sinon… »), il faut pouvoir le dire. C’est le rôle de la coupure (cut), notée !.

Dans ce chapitre, vous allez apprendre ce que fait exactement la coupure, la placer sans casser vos programmes, et distinguer les coupures inoffensives des coupures qui changent le sens du programme. Fil rouge : la grille tarifaire d’un cinéma rennais.

La coupure est un prédicat prédéfini sans argument, qui s’écrit ! dans le corps d’une clause. Sur le plan logique, elle ne dit rien : elle réussit toujours. Mais elle a un effet de bord sur l’exécution : elle supprime des points de choix, c’est-à-dire des alternatives que Prolog gardait en réserve.

Précisément, quand une clause de la forme p :- b1, b2, !, b3. exécute son !, deux sortes d’alternatives sont abandonnées :

  • les autres clauses de p qui n’ont pas encore été essayées ;
  • les autres solutions des buts situés à gauche du ! dans la clause (b1 et b2).

En revanche, les buts à droite du ! (b3) ne sont pas touchés : s’ils ont plusieurs solutions, le retour arrière les explore normalement. Et si b3 échoue, p tout entier échoue, puisqu’il ne reste plus aucune alternative.

Pensez à la coupure comme à un engagement : « arrivé ici, je choisis cette clause et ces valeurs, définitivement ». Dans l’arbre de recherche, elle élague toutes les branches en attente entre le nœud où p a été appelé et le nœud où l’on se trouve.

La position de la coupure change tout. Retenez la règle : tout ce qui est avant le ! est définitif une fois qu’on l’a franchi, tout ce qui est après reste soumis au retour arrière.

suspect1(X)en_cuisineleamarcen_salleinesyvesaucune coupuremarc, puis inessuspect2(X)en_cuisineleamarcen_salleinesyvesen_cuisine(X), !, gourmand(X)aucune réponsesuspect3(X)en_cuisineleamarcen_salleinesyvesen_cuisine(X), gourmand(X), !marc seulementsolutionessayé, échouetirets : coupé, jamais essayé! : coupure franchieclause 1clause 2clause 1clause 2clause 1clause 2!!
Sans coupure, tout l'arbre est exploré. Dans suspect2, la coupure franchie avec lea supprime marc et la clause 2 ; dans suspect3, elle n'est franchie qu'avec marc.

Au chapitre 5, nous avions écrit :

correction(Auj, Anniv, 0) :- Auj >= Anniv.
correction(Auj, Anniv, 1) :- Auj < Anniv.

Les deux clauses s’excluent mutuellement : si la première réussit, la seconde échouera forcément. Pourtant, Prolog l’ignore. Il garde un point de choix après la première clause et, au moindre retour arrière, réévalue la comparaison de la seconde pour rien. Une coupure lui évite ce travail :

correction(Auj, Anniv, 0) :- Auj >= Anniv, !.
correction(Auj, Anniv, 1) :- Auj < Anniv.

Le programme donne exactement les mêmes solutions avec ou sans cette coupure : elle ne sert qu’à l’efficacité, en temps (on n’essaie pas une clause inutile) et en mémoire (on ne garde pas le point de choix). On dit que c’est une coupure verte. Elle est typique des clauses mutuellement exclusives, mais pas seulement : si une personne peut avoir une réduction pour plusieurs raisons et qu’une seule suffit, couper après la première raison trouvée évite d’accorder la même réduction plusieurs fois.

correction(918, 312, C)918 >= 312 : C = 0918 < 312 : échecsans coupureau retour arrière, la clause 2est testée pour riencorrection(918, 312, C)918 >= 312 : C = 0918 < 312 : échecavec coupure vertela clause 2 n’est jamais testée :même réponse, C = 0clause 1clause 2clause 1clause 2!
Une coupure verte ne supprime qu'une branche qui aurait échoué de toute façon : la réponse C = 0 est la même, le travail en moins.

Tentation naturelle : puisque la coupure garantit qu’on n’arrive à la seconde clause que si Auj >= Anniv a échoué, pourquoi refaire le test ? Passons au cinéma, qui applique un tarif enfant, puis un tarif étudiant, puis le plein tarif :

age(lucas, 10). age(ines, 21). age(marc, 45).
etudiant(ines).
tarif(P, 5) :- age(P, A), A < 14, !.
tarif(P, 7) :- etudiant(P), !.
tarif(_, 10).

La dernière clause n’a plus aucune condition : elle suppose que les précédentes ont échoué. Retirez les coupures, et Lucas aurait deux tarifs, 5 puis 10 (il n’est pas étudiant), tout comme Ines, 7 puis 10. Ces coupures changent l’ensemble des solutions : ce sont des coupures rouges. Elles ne sont pas interdites, mais elles rendent le sens du programme dépendant de l’ordre d’exécution, et c’est là que naissent les pièges.

?- tarif(lucas, T).
T = 5
?- tarif(lucas, 10).
Yes

La première réponse est juste, la seconde absurde. Pourquoi ? Dans tarif(lucas, 10), la tête de la première clause, tarif(P, 5), ne s’unifie pas avec tarif(lucas, 10) : la clause est écartée avant d’atteindre la coupure. La deuxième échoue (Lucas n’est pas étudiant), et la troisième, sans condition, réussit.

Le remède consiste à ne lier le résultat qu’après la coupure : la tête accepte n’importe quel tarif, et c’est le corps qui le fixe, une fois la décision prise.

tarif(P, T) :- age(P, A), A < 14, !, T = 5.
tarif(P, T) :- etudiant(P), !, T = 7.
tarif(_, 10).

Maintenant, tarif(lucas, 10) entre dans la première clause, franchit la coupure, puis échoue sur 10 = 5 ; comme la coupure a supprimé les autres clauses, la réponse est « No ». Correct.

tarif(lucas, 10)tête tarif(P, 5) :10 ≠ 5, écartéeavant la coupureetudiant(lucas)échectarif(_, 10)Yestarif(lucas, 10)10 < 14, !,10 = 5 : échecclause 2clause 3tarif(P, 5) :- age(P, A), A < 14, !.tarif(P, T) :- age(P, A), A < 14, !, T = 5.réponse fausse : Lucas paierait 10réponse « No » : correcte123123!
Avec la sortie dans la tête, la question saute la clause 1 avant sa coupure et tombe sur le tarif par défaut. Liée après la coupure, elle est rejetée.

Que donne ?- tarif(P, T)., pour connaître tous les tarifs ? Une seule réponse : P = lucas, T = 5. La coupure a figé P = lucas, puisque age(P, A) est à sa gauche, et supprimé toutes les autres personnes. Le prédicat, correct en mode (+, ?), est faux en mode (-, -).

tarif(P, T)age(P, A), A < 14, !, T = 5P = lucas, A = 10T = 5P = inesP = marcclause 2clause 3la coupure fige P = lucas :Ines et Marc ne sont jamais essayés,une seule réponse en mode (−, −)clause 1!
En mode (−, −), la coupure fige la première personne trouvée par age(P, A) : Lucas. Les autres ne sont jamais essayées.

C’est la même chose pour ce prédicat, qui semble anodin :

a_des_enfants(X) :- parent(X, _), !.

En mode (+), la coupure est utile : on s’arrête au premier enfant trouvé, au lieu de répondre « Yes » autant de fois qu’il y a d’enfants. Mais en mode (-), ?- a_des_enfants(X). ne donne qu’un seul parent de la base. Une coupure rouge change donc la sémantique selon le mode d’utilisation : documentez le mode prévu, et testez les autres.

CoupureEffet sur les solutionsSi on la retireUsage
Verteaucun, elle ne fait que gagner du tempsmême programme, plus lentclauses exclusives, une seule preuve suffit
Rougeen supprime certainesle programme devient fauxsinon implicite, « premier trouvé »

La coupure permet d’écrire la structure conditionnelle qui manque à Prolog :

si_alors_sinon(P, Q, _) :- P, !, Q.
si_alors_sinon(_, _, R) :- R.

Si P réussit, on s’engage dans la première clause et on exécute Q ; sinon, on exécute R. Remarquez qu’il n’y a pas besoin d’écrire la condition « non P » dans la seconde clause : on n’y arrive que si P a échoué. Logiquement, si_alors_sinon(P, Q, R) est vrai si P et Q sont vrais, ou si P est faux et R vrai.

Ce prédicat utilise des méta-variables : P, Q et R sont des variables placées là où l’on attend un but. Au moment de l’exécution, elles doivent être liées à des termes appelables, comme age(lucas, A) ; Prolog les exécute alors comme des buts (c’est ce que fait aussi le prédicat call/1). Un programme peut donc manipuler des buts comme des données.

En pratique, on utilise la construction prédéfinie ( Condition -> Alors ; Sinon ), qui a le même sens et coupe seulement dans la condition. Le tarif s’écrit alors en une seule clause, très lisible :

tarif(P, T) :-
( age(P, A), A < 14 -> T = 5
; etudiant(P) -> T = 7
; T = 10
).
  • ! réussit toujours et, une fois franchi, supprime les autres clauses du prédicat et les autres solutions des buts à sa gauche ; les buts à sa droite restent soumis au retour arrière.
  • La position du ! dans la clause change radicalement les solutions (voir suspect2 et suspect3).
  • Une coupure verte ne change pas les solutions, seulement l’efficacité ; une coupure rouge les change.
  • Avec une coupure rouge, liez les résultats après le !, sinon une question qui fournit le résultat peut obtenir une réponse fausse.
  • Une coupure rouge peut casser le mode génération : a_des_enfants(X) ne donne plus qu’une réponse.
  • Le si-alors-sinon s’écrit avec une coupure, ou avec ( C -> A ; S ), qui garde la première solution de la condition.

Exercice · Un maximum trop rapide

facile

Un camarade écrit :

maxi(X, Y, X) :- X >= Y, !.
maxi(_, Y, Y).
  1. La coupure est-elle verte ou rouge ?
  2. Que répondent ?- maxi(5, 2, M). et ?- maxi(5, 2, 2). ? Corrigez le prédicat.
  3. Écrivez une version avec une coupure verte.
Voir la solution
  1. Elle est rouge : sans elle, maxi(5, 2, M) donnerait M = 5 puis M = 2, car la seconde clause n’a pas de condition.

  2. maxi(5, 2, M) répond M = 5, ce qui est juste. Mais maxi(5, 2, 2) répond Yes : la tête de la première clause, maxi(X, Y, X), demanderait que 5 et 2 soient égaux, elle est donc écartée avant la coupure, et la seconde clause réussit sans condition. Correction : lier le résultat après la coupure (onglet Prolog ci-dessous).

  3. Avec des conditions exclusives explicites, la coupure devient verte.

Ici, retirer la coupure ne change aucune réponse, et maxi(5, 2, 2) échoue bien.

En code

% 2. version corrigée : le résultat est lié après la coupure
maxi(X, Y, Z) :- X >= Y, !, Z = X.
maxi(_, Y, Y).
% 3. version avec une coupure verte (à charger à la place de la précédente)
maxi(X, Y, X) :- X >= Y, !.
maxi(X, Y, Y) :- X < Y.
% avec l'une ou l'autre version :
% ?- maxi(5, 2, M). M = 5
% ?- maxi(5, 2, 2). No

Exercice · Déplacer la coupure

moyen

Avec la base des suspects de ce chapitre, donnez toutes les réponses aux questions ?- suspect4(X). et ?- suspect5(X)., et justifiez à l’aide de l’arbre de recherche.

suspect4(X) :- !, en_cuisine(X), gourmand(X).
suspect4(X) :- en_salle(X), gourmand(X).
suspect5(X) :- en_cuisine(X), gourmand(X).
suspect5(X) :- en_salle(X), !, gourmand(X).
Voir la solution

suspect4 : la coupure est le premier but. Elle supprime immédiatement la seconde clause, mais il n’y a encore aucun but à sa gauche. Les buts à sa droite gardent donc leur retour arrière : en_cuisine(X) propose Léa (échec sur gourmand), puis Marc (succès). Réponse : X = marc seulement. La seconde clause n’est jamais essayée, Ines n’apparaît pas.

suspect5 : la première clause, sans coupure, donne X = marc (Léa échoue). Au retour arrière, on passe à la seconde clause : en_salle(X) propose Ines, la coupure fige ce choix (Yves ne sera jamais essayé), puis gourmand(ines) réussit. Réponses : X = marc, puis X = ines, puis « No ». La coupure de la seconde clause n’a aucun effet sur la première, déjà entièrement explorée.

En code

en_cuisine(lea). en_cuisine(marc).
en_salle(ines). en_salle(yves).
gourmand(marc). gourmand(ines).
% suspect4/1 et suspect5/1 : ceux de l'énoncé
% ?- findall(X, suspect4(X), L4), findall(X, suspect5(X), L5).
% L4 = [marc], L5 = [marc, ines]

Exercice · Ajouter à un ensemble

difficile

On représente un ensemble par une liste sans doublons. Écrivez ajouter(+P, +E1, -E2), qui ajoute P en tête de l’ensemble E1 s’il n’y est pas encore, et laisse E1 inchangé sinon :

  1. d’abord sans coupure, en utilisant membre/2 et hors/2 (du chapitre 6) ;
  2. puis avec une coupure, sans appeler hors/2.

Vérifiez votre version avec coupure sur la question ?- ajouter(a, [a, b], [a, a, b]).

Voir la solution
  1. Sans coupure, les deux cas doivent être exclusifs explicitement (onglet Prolog ci-dessous).

Petit défaut : si P est présent, membre peut réussir plusieurs fois (s’il y avait des doublons) et chaque succès redonnerait la même réponse.

  1. Avec une coupure rouge, le test hors devient inutile, mais il faut lier le résultat après la coupure.

Sur ajouter(a, [a, b], [a, a, b]) : membre(a, [a, b]) réussit, la coupure supprime la seconde clause, puis [a, a, b] = [a, b] échoue. Réponse : « No », ce qui est juste. La version naïve ajouter(P, E, E) :- membre(P, E), !. aurait écarté la première clause dès la tête ([a, b] ne s’unifie pas avec [a, a, b]) et la seconde aurait répondu Yes, à tort.

En code

% 1. sans coupure, avec membre/2 et hors/2 du chapitre 6
ajouter(P, E, E) :- membre(P, E).
ajouter(P, E, [P|E]) :- hors(P, E).
% 2. avec une coupure rouge (à charger à la place de la version 1)
ajouter(P, E, R) :- membre(P, E), !, R = E.
ajouter(P, E, [P|E]).
% ?- ajouter(c, [a, b], R). R = [c, a, b]
% ?- ajouter(a, [a, b], [a, a, b]). No