• Aucun résultat trouvé

Un cas typique est celui de la transitivité Quand on cherche à effacer

Dans le document Apprendre et enseigner Prolog pdf (Page 59-68)

plus_grand(Jo,x) en utilisant le programme suivant, on retrouve une instance de ce même but à effacer. Le programme se met alors à boucler et se termine par un débordement1 de pile (ou par une interruption utilisateur!). La manière correcte

d'écrire ce programme consiste à enlever la récursivité à gauche.

plus_grand(Jo, Max) -> ; plus_grand(Max, Fred) -> ;

plus_grand(x, y) -> plus_grand(x, z) plus_grand(z, y);

> plus_grand(Jo, x);

{x=Max} {x=Fred} DEBORDEMENT

"Une bonne solution" plus_grand'(Jo, Max) -> ; plus_grand'(Max, Fred) -> ;

plus_grand(x, z) -> plus_grand'(x, y) plus_grand_ou_egal(y, z);

plus_grand_ou_egal(x, x) ->;

plus_grand_ou_egal(x, y) -> plus_grand(x, y);

Exemple 2 : Cet exemple énumère toutes les listes construites avec 1. Avec le (mauvais) programme ci-dessous, c'est d'abord la liste infinie qui devrait être produite; bien entendu, la bonne solution s'obtient en permutant l'ordre des deux règles. liste_de_un(1.x) -> liste_de_un(x) ; liste_de_un(nil) -> ; >liste_de_un(x); DEBORDEMENT1 La coupure “!”

Normalement Prolog essaye d'effacer une suite de buts de toutes les manières possibles. Mais si on utilise une règle contenant un «!» (ou coupure) pour effacer un but q, l'effacement de ce «!» supprimera tous les choix de règles restant à faire pour effacer ce but q. Cela restreint la taille de l'espace de recherche : on peut dire que «!» fait «oublier» les autres manières possibles d'effacer q.

Le «!» ne peut apparaître que parmi les termes qui constituent le membre droit d'une règle. Les choix qui restent à examiner et que l'effacement du «!» fait «oublier» sont :

- les autres règles ayant la même tête que celle où le «!» figure

- les autres règles qui auraient pu être utilisées pour effacer les termes compris entre le début de la queue et le «!»

Cette question est illustrée par les exemples suivants :

couleur(rouge) ->; couleur(bleu) ->; taille(grand) ->; taille(petit) ->;

choix1("c'est tout") ->;

choix2(x.y) -> ! couleur(x) taille(y); choix2("c'est tout") ->;

choix3(x.y) -> couleur(x) ! taille(y); choix3("c'est tout") ->;

choix4(x.y) -> couleur(x) taille(y) !; choix4("c'est tout") ->; >choix1(u); {u=rouge.grand} {u=rouge.petit} {u=bleu.grand} {u=bleu.petit} {u="c'est tout"} >choix2(u); {u=rouge.grand} {u=rouge.petit} {u=bleu.grand} {u=bleu.petit} >choix3(u); {u=rouge.grand} {u=rouge.petit} >choix4(u); {u=rouge.grand} >choix1(u) !; {u=rouge.grand}

On peut considérer le «!» comme une annotation que l'on fait à un programme pour le rendre plus efficace; bien entendu, cela ne se justifie que si on ne s'intéresse qu'à la première solution fournie par ce programme.

Des utilisations classiques du «!» sont :

" Première solution uniquement "

premiere_solution_uniquement(b) -> b !; " Si Alors Sinon " si_alors_sinon(p,a,b) -> p ! a; si_alors_sinon(p,a,b) -> b; " non " non(p) -> p ! fail; non(p) -> ;

Dans le cas de non montré ci-dessus, il faut remarquer que l'on peut avoir des résultats inattendus si p contient des variables libres. C'est ce que montre le petit exemple suivant :

homme(Abélard) -> ;

femme(x) -> non(homme(x));

>femme(Eloïse); {}

>femme(Abélard);

>femme(x) eq(x, Eloïse); >

^(X,Y)

X doit être une variable et Y un terme quelconque.

^(X, Y) signifie: il existe X tel que Y soit vrai, et est équivalent à un appel de Y. L'utilisation de ce prédicat (qui est aussi un opérateur) n'a de sens que dans les prédicats bagof/3 et setof/3 , pour indiquer les variables existentielles et les retirer de l'ensemble des variables libres.

bagof(x, p, l)

Pour chaque instantiation différente de l'ensemble des variables libres du but p, non existentielles et n'apparaissant pas dans le terme x, unifie l avec la liste de toutes les solutions x lorsqu'on efface p. Chaque liste l est construite suivant l'ordre des solutions trouvées. Exemple:

aa(2,1) -> ; aa(1,2) -> ; aa(1,1) -> ; aa(2,2) -> ; aa(2,1) -> ; >bagof(X, aa(X,Y),L); {Y=1, L= 2.1.2.nil} {Y=2, L= 1.2.nil} > bagof(X,Y^aa(X,Y),L); {L=2.1.1.2.2.nil} > block(e, b), block_exit(e)

block est une règle prédéfinie qui permet de terminer brutalement l'effacement d'un but b. Cette primitive est faite essentiellement pour la récupération des erreurs. On peut considérer que :

- Pour effacer block(e, b) on efface b en ayant auparavant créé une paire de parenthèses fictives, étiquetées par e, autour du but b.

- block_exit(e) provoque l'abandon immédiat de l'effacement de tous les buts inclus entre les parenthèses étiquetées par e et supprime tous les choix éventuels en attente pour ces buts. L'effacement continue ensuite normalement, après les parenthèses étiquetées par e.

De plus :

- Une étiquette est un terme Prolog quelconque, et on considère que deux parenthèses étiquetées sont identiques si leurs étiquettes respectives sont unifiables.

- S'il y a plusieurs parenthèses étiquetées par e, block_exit(e) s'arrête au couple de parenthèses le plus interne.

- Si block_exit(e) ne rencontre pas de parenthèses e, alors on revient au niveau de commande avec le message d'erreur correspondant à e, si e est

C'est ce même mécanisme qui est utilisé pour la gestion des erreurs dans Prolog. Une erreur rencontrée dans l'exécution de Prolog provoque l'effacement du but block_exit(i) où i est un entier correspondant au numéro de l'erreur. De cette manière l'utilisateur peut récupérer toutes les erreurs de Prolog, pour pouvoir les traiter dans son application.

Lorsque les optimisations de compilation sont actives (option -f o1 au lancement de Prolog), la compilation de block est optimisée et la décompilation d'un tel but ne sera pas identique mais équivalente au but d'origine. Le but block(e,b) apparaissant dans une queue de règles, sera décompilé en block(e',_,b').

La règle prédéfinie quit génère un block_exit(14). Elle est définie ainsi :

quit -> block_exit(14); quit(i) -> block_exit(14,i);

A titre d'illustration voici un programme qui lit une suite de commandes :

executer_commande ->

block(fin_commande, toujours(lire_et_exec)); lire_et_exec -> outm("?") in_char'(k) exec(k); exec("q") -> block_exit(fin_commande) ;

exec("f") -> block_exit(16) ; /* Interruption */ exec("n") -> ;

toujours(p) -> repeter p fail ; repeter -> ;

repeter -> repeter ;

Dans cet exemple, la commande "q" utilise block_exit pour retourner au niveau supérieur de executer_commande. La commande "f" simule une erreur Prolog et rend le contrôle au block qui traite les erreurs de Prolog (puisque 16 ne peut s'unifier avec fin_commande). S'il n'y a pas de block englobant, on revient au niveau supérieur de Prolog.

block(e, c, b), block_exit(e, c)

Fonctionnement analogue aux formes précédentes, avec deux «étiquettes» e et c à la place d'une seule. b est le but à effacer. block(e,c,b) lance l'effacement de b; l'effacement ultérieur de block_exit à deux arguments produira la recherche d'un block dont les deux premiers arguments s'unifient avec les deux arguments de block_exit.

Lorsque les optimisations de compilation sont actives (option -f o1 au lancement de Prolog), la compilation de block est optimisée et dans certains cas la décompilation d'un tel but ne sera pas identique mais équivalente au but d'origine. Le but block(e,c,b) apparaissant dans une queue de règles, sera décompilé en block(e',c',b').

Ensemble, ces deux règles prédéfinies constituent une sorte de raffinement du mécanisme de récupération des erreurs; en pratique, e correspond au type d'erreur guetté; c correspond à un «complément d'information» rendu par le mécanisme de détection.

Un grand nombre de règles prédéfinies utilisent cette deuxième étiquette comme complément d'erreur. C'est souvent l'argument, cause de l'erreur, qui y est transmis. En phase de mise au point de programmes, cela peut paraître encore insuffisant. Dans le kit, des modules objets : dbgbase.mo, dbggraph.mo et dbgedin.mo, sont fournis. Ils permettent, après rechargement, en cas d'erreur, d'avoir des messages encore plus complets. Le deuxième argument de block sera unifié avec un terme de la forme :

prédicat d'appel ou < prédicat d'appel, complément d'erreur>

Lorsque block_exit(e,c) est effacé dans un environnement «parenthésé» par block(e',b), alors block_exit(e,c) se comporte comme block_exit(e). Inversement, lorsque block_exit(e) est effacé dans un environnement «parenthésé» par block(e',c',b) alors block_exit(e) se comporte comme block_exit(e,nil)

Erreurs et sous-sessions Prolog

Tout ce qui est dit ci-dessus concernant le mécanisme d'erreur block / block_exit, est vrai à l'intérieur d'une même session Prolog. Lorsqu'il s'agit de transmettre une erreur à travers une ou plusieurs sous-sessions (par exemple lorsqu'un but est lancé par menu), deux restrictions sont à mentionner :

- le deuxième argument de block_exit (souvent utilisé comme complé- ment d'erreur) n'est pas transmis. Il vaudra toujours nil.

- le premier argument de block_exit est transmis uniquement s'il est de type entier. Dans le cas contraire c'est l'entier 317, pour l'erreur : 'block_exit' SANS 'block' CORRESPONDANT, qui est propagé. Interruption

A tout instant un programme Prolog peut être interrompu au moyen d'une touche déterminée, dépendant du système utilisé (par exemple : <Ctrl-C>). Cette interruption est prise en compte par le mécanisme général des erreurs de Prolog et correspond à l'erreur 16. Cette erreur peut donc être récupérée par l'utilisateur. Sinon on revient au niveau de commande avec le message d'erreur suivant : INTERRUPTION UTILISATEUR.

bound(x)

bound(x) s'efface si x est lié. Une variable est considérée comme liée si elle a été unifiée contre :

- une constante (entier, réel, identificateur, chaîne), - un terme de la forme t1.t2,

dif(t1, t2)

La règle prédéfinie dif(t1, t2) est utilisée pour contraindre t1 et t2 à représenter toujours des objets différents. Dès que cette contrainte ne peut plus être satisfaite, on revient en arrière (backtracking). L'implantation de dif utilise un mécanisme similaire à geler.

Voici deux primitives intéressantes que l'on peut écrire avec dif : hors_de(x, l) qui vérifie que x n'appartiendra jamais à la liste l; et tous_differents(l) qui vérifie que les composants de la liste l seront toujours différents deux à deux.

hors_de(x, nil) -> ;

hors_de(x, y.l) -> dif(x, y) hors_de(x, l); tous_differents(nil) -> ;

tous_differents(x.l) -> hors_de(x, l) tous_differents(l);

Avec ces deux primitives on peut par exemple écrire un programme qui calcule des permutations : il suffit de dire que l'on a une liste d'entiers et que tous les éléments de cette liste sont différents deux à deux :

permutation(x1,x2,x3,x4) -> tous_differents(x1.x2.x3.x4.nil)

chiffre(x1) chiffre(x2) chiffre(x3) chiffre(x4); chiffre(1) -> ;

chiffre(2) -> ; chiffre(3) -> ; chiffre(4) -> ;

Remarquons que dans cet exemple, Prolog met d'abord en place toutes les inégalités avant d'exécuter le programme non-déterministe classique. Ceci donne une programmation plus claire et plus efficace.

L'utilisation de dif permet également l' écriture de programmes qui auraient un comportement incorrect si l'on utilisait une définition utilisant la coupure. Par exemple la définition de la relation "x est élément de l à la valeur v" peut s'écrire :

élément(x,nil,false) ->; élément(x,x.l,true) ->;

élément(x,y.l,v) -> dif(x,y) élément(x,l,v); > élément(x,1.2.nil,v);

{x=1, v=true} {x=2, v=true}

{x#1, x#2, v=false}

Si l'on définissait une telle relation en utilisant la primitive !, on introduirait de nombreux comportements anormaux :

élément(x,nil,false) ->; élément(x,x.l,true) -> ! ;

élément(x,y.l,v) -> élément(x,l,v); > élément(x,1.2.nil,v);

{x=1, v=true} % une seule solution !!

default(t1, t2)

La règle prédéfinie default permet de réaliser le contrôle suivant: Si on peut effacer t1, alors on l'efface de toutes les manières possibles, sinon on efface t2. Il faut remarquer que contrairement à ce que l'on pourrait penser à première vue, cette primitive ne peut pas être réalisée avec '!'. Voyons sur un exemple une utilisation de cette règle:

répondre(p) -> default(p,outml("personne")); homme(jean) ->; homme(pierre) ->; > répondre(homme(x)); {x=jean} {x=pierre} > répondre(femme(x)); personne {} eq(t1, t2)

Unification de t1 et t2 : correspond simplement à la règle : eq(x,x) -> ; .

fail

Règle prédéfinie provoquant toujours un échec (backtracking). findall(x, p, l)

Unifie l avec la liste de toutes les solutions x lorsqu'on efface p. free(x)

S'efface uniquement si x n'est pas lié. list_of(x, y, p, l)

Fournit la liste triée, sans répétition, de tous les individus qui satisfont une certaine propriété.

x est une variable.

y est une liste de variables.

p est un terme Prolog contenant au moins toutes ces variables.

Pour chaque ensemble de valeurs de y, unifie l avec la liste des valeurs de x pour que p soit vraie (c'est à dire pour que p s'efface).

L'ordre de la liste l est identique à celui défini sur les termes (cf. term_cmp/3) Exemple : Prolog contenant la base de règles suivante :

homme(grand, michel, 184) ->; homme(grand, alain, 183) ->; homme(grand, henry, 192) ->; homme(petit, nicolas, 175) ->; homme(petit, julien, 176) ->; homme(petit, gilles, 120) ->; > list_of(x,t.nil,homme(t,x,h),l);

not(X)

Est décrit par les règles:

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

repeat

Est décrit par les règles :

repeat ->;

repeat -> repeat; setof(x, p, l)

Pour chaque instantiation différente de l'ensemble des variables libres du but p, non existentielles et n'apparaissant pas dans le terme x, unifie l avec la liste de toutes les solutions x lorsqu'on efface p. Chaque liste l est triée et sans répétition. L'ordre de la liste l est identique à celui défini sur les termes (cf. term_cmp/3).

Exemple : Prolog contenant la base de règles suivante :

aa(2,1) -> ; aa(1,2) -> ; aa(1,1) -> ; aa(2,2) -> ; aa(2,1) -> ; > setof(X, aa(X,Y),L); {Y=1, L= 1.2.nil} {Y=2, L= 1.2.nil} > setof(X,Y^aa(X,Y),L); {L=1.2.nil} > or(x,y)

Définit un ou logique transparent à la coupure. Exemple:

> op(900,xfy,or); {}

> enum(i,4) (eq(i,1) or eq(i,2)); {i=1}

{i=2}

> enum(i,4) ([eq(i,3),'!'] or [outl(no),fail]); no

no {i=3} >

2.2. Geler

Il s'agit de résoudre de manière efficace un problème qui se pose souvent en Prolog : retarder le plus possible certaines décisions. Ce qui revient à dire qu'il faut avoir suffisamment d'information avant de poursuivre l'exécution d'un programme, ou que certaines variables doivent avoir reçu une valeur pour pouvoir continuer. On profite alors des avantages de la programmation déclarative, tout en gardant une exécution efficace.

freeze(x, q)

Le but de cette règle prédéfinie est de retarder l'effacement de q tant que x est inconnu. Plus précisément :

(1) Si x est libre, alors freeze(x, q) s'efface et l'effacement de q est mis en attente (on dit que q est gelé). L'effacement effectif de q sera déclenché au moment où x deviendra liée.

(2) Si x est liée alors freeze lance l'effacement de q normalement.

C'est au programmeur de s'assurer qu'une variable gelée sera toujours dégelée dans le futur, pour que le but associé ne reste pas indéfiniment en attente. Ceci n'est pas vérifié par Prolog, et peut conduire à des solutions trop générales. Remarque : Les impressions des variables d'une question sur lesquelles il reste des buts gelés se fait d'une manière spéciale :

> freeze(x,foo);

{x~foo.nil}

Voyons maintenant quelques exemples d'utilisation de freeze.

Exemple 1 : Evaluation de somme(x, y, z). On veut résoudre l'équation

z = x + y seulement si les deux variables x et y sont connues.

somme(x, y, z) -> freeze(x, freeze(y, somme1(x, y, z))) ; somme1(x, y, z) -> val(x+y, z);

Exemple 2 : Mais on peut faire mieux : On veut résoudre la même équation si deux au moins des variables x, y, z sont connues :

somme (x, y, z) ->

freeze(x, freeze(y, ou_bien(u, somme1(x, y, z)))) freeze(y, freeze(z, ou_bien(u, somme2(x, y, z)))) freeze(x, freeze(z, ou_bien(u, somme3(x, y, z)))); ou_bien(u, p) -> free(u) ! eq(u, Cst) p;

ou_bien(u, p) ->;

somme1(x, y, z) -> val(x+y, z); somme2(x, y, z) -> val(z-y, x); somme3(x, y, z) -> val(z-x, y);

Ci-dessus, la variable u, qui devient liée dès que l'une des additions est effectuée, sert à empêcher que les autres additions, redondantes, soient calculées.

Exemple 3 : Cet exemple montre comment utiliser une variable pour

Dans le document Apprendre et enseigner Prolog pdf (Page 59-68)