• Aucun résultat trouvé

Que trouve-t-on dans ce chapitre ?

Le domaine de Prolog III est celui des arbres (finis et infinis). Un nouvel opérateur universel a été ajouté par rapport aux Prolog standard : le constructeur général d'arbres. La notion de liste a également été étendue à celle de tuple, et ce domaine muni d'une véritable opération de concaténation. Les chaînes de caractères sont, d'autre part, un sous-ensemble des tuples. Enfin, pour des raisons de compatibilité et d'efficacité, les listes classiques de Prolog ont été conservées. Ce chapitre présente les opérations et les relations définies sur les arbres et les tuples, ainsi qu'un certain nombre d'exemples utilisant des contraintes sur ces domaines.

1 . Introduction

Prolog III améliore sensiblement le traitement des arbres, en introduisant un opérateur qui autorise la représentation d'un arbre de manière totalement générique, indépendamment de son arité. En ce qui concerne les listes, structures de données de base des programmes Prolog, l’amélioration apportée par Prolog III est fondamentale et introduit un nouvel objet : le tuple. Cette évolution répond à un double constat. Tout d’abord, la structure même de liste, un arbre construit à partir d’un symbole fonctionnel binaire, rend l'opération de concaténation très onéreuse. Ensuite, tout accès au n-ième élément d’une liste (y compris lors d'une concaténation programmée en Prolog) ne peut être effectué qu’au travers d’un parcours séquentiel. Ces deux inconvénients sont supprimés grâce à l’introduction de contraintes sur les tuples, munis d’une concaténation dont les propriétés répondent à ce que l’on est en droit d’attendre.

2 . Les arbres

Les arbres forment la classe la plus générale d’objets définis en Prolog III. Tous les objets définis dans la suite de ce document (tuples, listes, chaînes, valeurs numériques ou booléennes, …) sont des éléments de l’ensemble des arbres et toute variable Prolog III représente une valeur prise dans cet en- semble.

Ces arbres sont composés de nœuds étiquetés par : • des identificateurs,

• des caractères,

• les valeurs booléennes 0' et 1',

• des valeurs numériques , • le signe spécial <>

{

On prendra bien garde à ne pas confondre arbres et termes. Les

arbres sont des éléments du domaine de Prolog III ; les termes sont des constructions syntaxiques.

Voici un exemple d’arbre : érable <> `a` `c` `e` `r` feuilles lobes caduques 7 1'

Les arbres dont l’étiquette initiale est un identificateur sont appelés arbres

factuels, et ceux dont l’étiquette initiale est le signe <> sont appelés tuples. Les

tuples dont tous les éléments sont des caractères sont appelés chaînes.

{

NOTATION : En Prolog III, les chaînes sont plutôt représentéesentre guillemets (par exemple : "Ralliez vous à mon panache blanc") et les tuples entre chevrons (par exemple <Marignan, 1515, 1'>).

Opérations sur les arbres

Les opérations définies sur les arbres sont les opérations de construction. On trouve ainsi, comme en Prolog II le constructeur d’arbres, mais également deux nouveaux opérateurs, le constructeur de tuple, et le constructeur général

d’arbres, qui permet de désigner tout arbre de manière totalement

générique, indépendamment de son nombre de fils. Voici les schémas illustrant ces opérations.

xn x0 = x1 x0 (x1 …xn ) Le constructeur d’arbres xn <> = x0 xn <x0 > Le constructeur de tuples x0 = x n x 1 x0 [ <> ] xn x1

Le constructeur général d’arbres

Le constructeur général d'arbres permet de représenter tout arbre à l'aide seulement de son étiquette initiale et du tuple formé de ses fils immédiats.

{

ATTENTION : on ne peut pas insérer d'espaces entre le terme qui représente l'étiquette de l'arbre et la parenthèse ouvrante du constructeur. C'est là l'un des rares cas en Prolog III où l'écriture d'un blanc est interdite.

Voici quelques exemples de l'utilisation de ces divers constructeurs. On donne ici plusieurs formes syntaxiques possibles qui utilisent les différents constructeurs, et les arbres correspondants :

exemple 1 2 3 exemple(1,2,3) exemple[<1,2,3>] <1,2,3> <>(1,2,3) <>[<1,2,3>] <> 2 3 1 exemple 1 2 3 exemple(<1,2,3>) exemple[<<1,2,3>>] <>

Voici enfin un exemple du même type portant sur une chaîne de caractères :

"123" <>(`1`,`2`,`3`) <>["123"] <>[<`1`,`2`,`3`>] <> `2` `3` `1`

Contraintes sur les arbres

Les contraintes sont construites à l’aide de relations. Les relations utilisées sur les arbres sont :

• l’égalité (=),

• l’inégalité (#),

• la relation unaire qui impose à un arbre d'avoir un nombre déterminé de fils immédiats. Dans le cas où cet arbre est représenté par la variable A et son nombre de fils par N, cette relation se note A::N.

Exemples

Voici quelques exemples de contraintes sur les arbres.

{X = Erable("acer",feuilles(lobes(7),caduques(1')))}

Cette contrainte impose à la variable X de représenter l'arbre donné en

exemple précédemment.

{X = E[U],Y = E[V]}

Cet ensemble de contraintes impose aux variables X et Y de représenter deux

arbres dont les étiquettes initiales sont égales

{X = E[U], E # <>}

Cette contrainte impose à la variable X de représenter un arbre qui ne soit

pas un tuple.

Examinons enfin quelques exécutions qui illustrent l'utilisation de ce type de contraintes :

> {E[U] = exemple(1,2,3)}; {E = exemple, U = <1,2,3>} > {E[U] = <1,2,3>}; {E = <>, U = <1,2,3>} > {E[U] = exemple(<1,2,3>)}; {E = exemple, U = <<1,2,3>>} > {E[U] = "123"}; {E = <>, U = "123"} > {E[U] = 121/3}; {E = 121/3, U = <>}

Les exemples suivants montrent comment sont simplifiées certaines équations sur les arbres :

> {X(Y) = Y(X) }; {Y = X, X::0 } > {<X> = X[Y]}; {X = <>, Y = <<>>} > {Y[X] = X[Y] }; {Y = <>, X = <>}

Voici enfin des exemples qui montrent des contraintes pour lesquelles les solutions sont des arbres infinis :

> {X(Y) = X[Y]};

{Y = <Y>, X::0 }

> {X(Y) = <<X,Y>>};

Restrictions sur la contrainte de taille

En Prolog III on ne peut imposer à un arbre d'avoir un nombre déterminé de fils immédiats qu'à la condition que ce nombre soit une constante, c'est-à- dire un entier positif. Toutefois, pour augmenter la facilité de programmation, si ce nombre est représenté par un terme contenant des variables un mécanisme de retardement est mis en place par Prolog III. Nous reviendrons sur ce mécanisme dans le chapitre spécialement consacré aux retardements.

{E[<X, ff(Y)>. Z] :: 13};

Dans une contrainte de taille, le nombre de fils immédiats doit être explicitement connu

3 . Les tuples

On appelle tuple tout arbre dont l’étiquette initiale est <>. Cette première définition permet de considérer les tuples comme des suites finies d’arbres. On notera au passage que l’aspect fini se rapporte au nombre d’éléments (et non pas à la profondeur de ceux-ci), et que le caractère de suite induit un ordre sur ces éléments (les tuples ne sont pas des ensembles). On appellera souvent taille le nombre de fils d’un tuple.

Opérations

La seule opération définie sur les tuples est l’opération de concaténation, notée par un point (“.”). Cette opération construit, à partir de deux tuples T1 et T2, respectivement de tailles n et m, le tuple T1.T2, de taille m+n,

Ceci peut s’illustrer par le schéma suivant : <> x1 xn <> y1 ym • = <> x1 …xn y1 …ym

Concaténation de deux tuples

Relations

Les relations qui permettent de construire des contraintes sur les tuples sont les mêmes que celles utilisées pour les arbres, à savoir l'égalité, l'inégalité, et la relation unaire permettant d'associer à un tuple le nombre de ses éléments. Dans ce dernier cas, on dispose de la relation de taille sur les arbres, mais également d'une relation unaire qui impose à un terme T de

représenter un tuple (T !tuple).

Restrictions concernant les tuples

Pour réaliser un bon compromis entre la complexité des contraintes traitées et les considérations d'efficacité liées à la réalisation d'un langage de programmation, Prolog III impose un certain nombre de restrictions sur ces contraintes. En ce qui concerne les tuples, cette restriction porte principalement sur le fait que tout tuple figurant en membre gauche d'une

opération de concaténation doit être de taille explicitement connue. La seconde

restriction découle naturellement de celle imposée aux arbres, à savoir que pour toute contrainte de taille portant sur un tuple, le nombre représentant celle-ci doit être explicitement connu. Dans le cas où ces restrictions ne sont pas respectées, un mécanisme de retardement, que nous détaillerons par la suite, est mis en place (voir le chapitre entièrement consacré aux retardements)

{ U.V.W = <E>.Z, U :: 4, V :: 2 }

Tout tuple figurant en membre gauche d'une opération de concaténation doit être de taille explicitement connue

Exemples de contraintes sur les tuples

Voici un certain nombre de systèmes de contraintes corrects portant sur des tuples : {<1,2,3>.V = Z} {<E>.U = V} {U.V = W,U :: 100} {U.<E,F>.V = W, U :: 4} {(U.V).W = T,U :: 2,V :: 4}

On notera dans ces exemples que pour toute concaténation, la taille des opérandes gauches est connue.

Voici, cette fois quelques exécutions de ce types de contraintes, qui montrent l'ensemble des solutions correspondantes :

> {<0>.U = U.<0>, U :: 10};

{U = <0,0,0,0,0,0,0,0,0,0>}

> {Z :: 10, <1,2,3>.Z = Z.<2,3,1>};

Récapitulatif des opérations et relations

Pour récapituler, voici toutes les opérations et relations utilisables sur les arbres et les tuples :

Opérations et relations définies sur les arbres

opérations

constructeur d'arbre x0(x1, ..., xn)

constructeur général d'arbre x0[<x1, ..., xn> ]

relations

égalité x = y

inégalité x # y

taille x :: n

Opérations et relations définies sur les tuples

opérations constructeur de tuple <x0, x1, ..., xn> concaténation u . v relations égalité u = v inégalité u # v taille u :: n typage u !tuple

4 . Règles prédéfinies sur les tuples

Les règles prédéfinies qui concernent la taille des tuples se divisent, on aura l'occasion de le revoir souvent, en deux classes : les règles occasionnant un retardement et celles pour lesquelles ce n'est pas le cas. On retrouvera une description détaillée et des exemples dans le chapitre "Règles prédéfinies et procédures externes".

Tailles et concaténations

bound_size(T,N) : Pose la contrainte {T :: N} lorsque N est un entier

positif connu, ou {N = k} si k est la taille de T. Echoue si T n'est pas de

taille connue et si N est inconnu ou ne représente pas une valeur entière

positive.

size(U,N) : Pose la contrainte {U :: N}, avec les retardements nécessaires

si N et la taille de U ne sont pas connus. Echoue si N est connu mais ne

représente pas une valeur entière positive.

bound_conc(U1,U2,U3) : Pose la contrainte {U3 = U1.U2}. Echoue si la

taille du tuple U1 n'est pas connue.

c o n c 3 ( U 1 , U 2 , U 3 ) : Pose l'ensemble de contraintes suivant :

{ U3 = U1.U2, U1 :: N1, U2 :: N2, U3 :: N3, N3 = N1+N2, N1 >= 0, N2 >= 0}, avec les éventuels retardements lorsque N1, N2 ou N3 ne sont pas connus.

arg3(N,T1,T2) : Cette primitive pose la contrainte {T2 = N'},N'

est le Nième argument du tuple T1. Si N est nul, T2 est égal à l'étiquette

initiale de T1 c'est-à-dire <>. Echoue si N n'est pas un entier positif connu ou

Divers

arg(N,T1,T2) : Cette primitive pose la contrainte {T2 = N'},N' est

le Nième argument du terme T1. Si N est nul, T2 est égal à l'étiquette initiale

de T1. En particulier, si T1 est un tuple T2 représente le Nième élément de ce

tuple. Echoue si N n'est pas un entier positif connu.

known_part(U1,U2,U3) : Pose la contrainte {U1 = U2.U3},U2 est un

tuple formé de la plus grande suite de taille connue des premiers éléments de U1.

split(U,L) : Pose la contrainte {L = L'},L' est la liste composée des

éléments qui forment le tuple U (voir la suite de ce chapitre concernant les

listes). Echoue lorsque le tuple U n'est pas connu à l'exécution.

tuple(U) : S'exécute avec succès si U représente un tuple entièrement connu

(voir le prédicat bound). Echoue dans tous les autres cas.

is_tuple(U) : Vérifie que le terme U représente un tuple c'est-à-dire que la

contrainte {U !tuple} appartient au système courant. Echoue dans le cas

contraire.

Un exemple de programme sur les tuples

Comme premier exemple de programme, nous nous proposons de considérer le retournement (ou inversion) d'un tuple. Nous examinerons successivement la manière habituellement utilisée en Prolog, puis la programmation en Prolog III, et nous terminerons par l'examen d'un programme utilisant des contraintes retardées.

Voici tout d'abord le programme classique, à ceci près que l'on utilise des tuples au lieu de listes. On pourra en profiter pour noter la manière d'exprimer un tuple à la mode “Prolog standard” (un couple tête-de-liste, queue-de-liste) à l'aide de la concaténation. Le rapport, dans ce cas, entre le

E.L de Prolog II et le <E>.L de Prolog III n'est, on l'aura compris que

talement différente. On retrouvera également dans ce programme, peut être avec une certaine émotion, le toujours fameux prédicat conc.

naive_reverse(<>,<>) -> ; naive_reverse(<E>.X,X') -> naive_reverse(X,X'') conc(X'',<E>,X') ; conc(<>,X,X) -> ; conc(<E>.X,Y,<E>.Z) -> conc(X,Y,Z) ;

Voici maintenant le même programme, qui utilise cette fois-ci la concaténation de Prolog III. Le prédicat bound_conc installe la contrainte {X' = X''.<E>} après l'exécution de naive_reverse. On est alors certain

de connaître la taille du tuple X''. Il n'y a donc aucun retard mis en place.

Cette manière de faire est bien plus efficace que la première, dans la mesure où la concaténation de Prolog III est bien supérieure au prédicat conc décrit

dans le programme précédent.

naive_reverse(<>, <>) -> ; naive_reverse(<E>.X,X') ->

naive_reverse(X,X'')

bound_conc(X'',<E>,X') ;

Voici enfin un troisième programme, qui met en place les concaténations avant l'exécution de naive_reverse. Des contraintes retardées sont alors mises en place (voir chapitre sur les retardements). Si le programme fournit les résultats attendus, les temps d'exécutions sont moins bons que dans le cas précédent, en raison de la gestion des retards.

naive_reverse(<>,<>) -> ; naive_reverse(<E>.X,X'.<E>) ->

5 . Les listes

Pour des raisons d'efficacité, et bien que l'opérateur binaire utilisé pour les listes en Prolog classique puisse être aisément remplacé par une conca- ténation, Prolog III permet l'utilisation de listes “classiques” construites sur la notion de paire pointée, à l'aide d'un symbole fonctionnel binaire représenté par [].

Syntaxiquement les formes suivantes sont permises :

[ U | L ] qui représente une liste de tête U et de

queue L

[U1,U2,...,Un] qui représente une liste formée des

éléments U1,U2,...,Un

[ U 1 , U 2 , . . . , U n | L ] qui représente une liste dont les premiers

éléments sont U1,U2,...,Un et la queue

de liste L

{

Bien que la syntaxe ne soit pas ambiguë, vous devez veiller à ne pas confondre l'opérateur binaire utilisé pour les listes et représenté par des crochets (par exemple [U], pour une liste réduite à un élément),

et l'opérateur n-aire de construction générale d'arbres, représenté par le même symbole (par exemple E[U]).

{

ATTENTION : il y a une véritable ambiguïté dans le fait suivant : comment distinguer une liste ayant A pour tête et B pour queue,

qu'on doit noter [A|B], d'une liste réduite à l'unique booléen,

résultat de la disjonction des booléens A et B, qu'on devrait noter

encore [A|B] ? Cette ambiguïté est levée en imposant convention-

nellement, dans le second cas, le parenthésage de l'expression booléenne en question : [(A|B)]

Il n'y a aucune opération définie sur les listes en Prolog III autre que celles concernant les arbres, et donc pas de contraintes particulières. On pourra quelquefois privilégier ce type de structures dans des programmes sans concaténations pour en améliorer les performances.

{

On peut se demander à juste titre quel est le statut exact du double symbole [], qui désigne la liste vide. La réponse est qu'il s'agit

d'un identificateur, qui devrait en toute rigueur être représenté entre quotes, mais pour lequel il est toléré d'omettre ces quotes. Les notations [] et ' [ ] ' (et même s y s : [ ]) sont donc

équivalentes.

Voici quelques exécutions faisant intervenir des listes :

> {E[U] = [A,B,C|L]}; {E = '[]', U = <A,[B,C | L]>} > {E(X,Y) = [A,B,C|L]}; {E = '[]', Y = [B,C | L], A = X} > {[A,B,C|L] = [A'|L']}; {A' = A, L' = [B,C | L]} > i s _ i d e n t ( [ ] ) ; {}

Primitives sur les listes

arg2(N,L,T) : Si N est nul, pose la contrainte {T = N'},N '

représente le nombre d'élément de la liste L. Si N n'est pas nul, pose la

contrainte {T = N'},N' est le Nième argument de la liste L. Echoue si N

n'est pas un entier positif connu ou si la liste L n'est pas suffisamment

connue.

list_tuple(L,U) : Pose la contrainte {U = U'},U' est le tuple

composé des éléments qui forment la liste L (voir la suite de ce chapitre

concernant les listes). Echoue lorsque la liste L n'est pas entièrement connue.

6 . Les chaînes

Comme nous l'avons déjà mentionné, les chaînes en Prolog III sont des tuples dont les éléments sont des caractères. On peut donc utiliser sur les

chaînes l'opération de concaténation ainsi que les relations unaires et binaires utilisables sur les tuples.

Voici quelques exemples de contraintes sur des chaînes :

> {X."a" = "a".X, X :: 10};

{X = "aaaaaaaaaa"}

> {X = Y." une concatenation ".<`d`,`e`,` `>.Z, Y = <>(`V`,`o`,`i`,`c`,`i`), Z = U["chaine"]};

{X = "Voici une concatenation de chaine", Y = "Voici",

Z = "chaine", U = <>}

Primitives sur les chaînes

Il nous faut également noter un certain nombre de règles prédéfinies et prédicats évaluables concernant les chaînes de caractères. On se reportera pour plus de détails au chapitre Règles prédéfinies et procédures externes.

Manipulations de chaînes

conc_string(S1,S2,S3) : Cette règle prédéfinie a le même effet que son homologue de Prolog II. Elle énumère tous les triplets S1,S2,S3 de chaînes

telles que S3 est la concaténation de S1 et S2. Ce prédicat ne réussit que si

les chaînes S1 et S2 ou bien la chaîne S3 sont entièrement connues.

find_pattern(S1,S2,N) : Pose la contrainte {N = D} D est la position

du début de la chaîne S 2 dans la chaîne S 1. Si la chaîne S 2 n'est pas

trouvée, ou si S 1 et S 2 ne sont pas entièrement connues, alors find_pattern échoue.

substring(S1,N1,N2,S2) : Pose la contrainte {S2 = S1'},S1' est

la sous-chaîne de S1 commençant à la position N1, de longueur N2. Echoue

Conversions de chaînes

list_string(L,S) : Pose la contrainte {S = L'},L' est une chaîne

composée des caractères qui forment la liste L. Echoue si la liste L n'est pas

entièrement connue.

string_ident(P,S,I) :

• Si I est connu : pose l'ensemble de contraintes {S = S',P = P'}S '

est une chaîne composée des caractères de la représentation abrégée de l'identificateur I, et P' une chaîne composée des caractères formant le

préfixe de l'identificateur I.

• Si S et P sont connus : pose l'ensemble de contraintes {I = I'}, où I' est

un identificateur composé des caractères de la chaîne représentant son préfixe, P, et de celle représentant sa notation abrégée, S.

string_integer(S,N) :

• Si S est connu : pose l'ensemble de contraintes {N = S'}S' est un

entier formé à partir des caractères de la chaîne S.

• Si N est connu : pose l'ensemble de contraintes {S = N'},N' est une

chaîne formée des caractères qui composent l'entier N.

string_real(S,F) :

• Si S est connu : pose l'ensemble de contraintes {F = S'}S' est un

flottant formé à partir des caractères de la chaîne S.

• Si F est connu : pose l'ensemble de contraintes {S = F'},F' est une

chaîne formée des caractères qui composent le flottant F.

string_bool(S,B) :

• Si S est connu : pose l'ensemble de contraintes {B = S'}S' est un

booléen formé à partir des caractères de la chaîne S.

• Si B est connu : pose l'ensemble de contraintes {S = B'},B' est une

chaîne formée des caractères qui composent le booléen B.

Divers

string(S) : S'exécute avec succès si S représente une chaîne entièrement