• Aucun résultat trouvé

qui permettent de séparer les composants des aspects ont égale- ment cette caractéristique : recomposer le ǥot d’exécution dyna- miquement rend le programme moins prévisible, et moins compré- hensible. sous ces mécanismes présentent donc des avantages et des inconvénients, et le choix du bon compromis est dans les mains du concepteur du système.

.

Le problème de l’expression

ke problème de l’expression est un problème proche du notre. ke but est de pouvoir étendre un interpréteur sans avoir à modiǤer les déǤnitions existantes. soute solution à ce problème sera donc potentiellement pertinente pour l’extension de marcissus.

Définition du problème

vadler déǤnit le problème ainsi zvad98] :

she dxpression oroblem is a new name for an old prob- lem. she goal is to deǤne a datatype by cases, where one can add new cases to the datatype and new func- tions over the datatype, without recompiling existing code, and while retaining static type safety (e.g., no casts).

cans un langage fonctionnel, une façon courante d’implémen- ter un interpréteur est en utilisant un type algébrique de données pour représenter les termes. dn gaskell par exemple, on déǤnit un simple langage arithmétique par le type algébrique suivant :

d6t6 Term = Const6nt Int | Plus Term Term | Mult Term Term

Termest un type qui a trois variantes : un terme est soit une constante (Const6nt, un entier), une addition (Plus), ou une mul- tiplication (Mult).

ka syntaxe du type algébrique correspond à la façon usuelle de déǤnir des grammaires : la forme de aackus-maur (ame). tne ame pour le langage arithmétique pourrait être :

<term> ::= <int> | <term> + <term> | <term> * <term> <int> ::= | | ...

hci encore, un terme a trois variantes. k’addition et la multipli- cation contiennent d’autres termes, et la varianteintne contient qu’un nombre terminal. nn voit que le type algébrique est suǦsam- ment proche de la ame pour rendre la traduction en code gaskell triviale. kes grammaires pour les langages de programmation étant souvent déǤnies par une ame, le type algébrique est la solution la plus évidente pour les représenter.

charabia Le détournement à travers l’histoire des langages de programmation

Const6nt, Plus et Mult sont les constructeurs du type Term, qui permettent de créer des expressions du langage. k’expression 2 + 5 * 3 sera construite par :

const

mult

plus

const const

Plus (Const6nt ) (Mult (Const6nt ) (Const6nt ))

bette valeur représente l’expression 2 + 5 * 3, et nous donne en même temps un arbre syntaxique abstrait de cette expression.

oour évaluer l’expression arithmétique, il faut évaluer l’arbre syntaxique et produire un nombre en retour. b’est le rôle de l’inter- préteur du langage, qui sera donc une fonction du type algébrique. cans la terminologie de vadler, ces fonctions sont appelées des opérations.

k’opération principale est l’opération d’évaluation qui donne un sens à chaque terme du langage. oar exemple, on souhaite évaluer les expressions de notre langage arithmétique en suivant le sens usuel de l’addition et de la multiplication d’entiers. b’est à dire, pour évaluerPlus t t , on évaluet ett pour obtenir des en- tiers, puis on les additionne. dn gaskell, on écrira l’évaluation en traitant les trois variantes séparément, par pattern matching :

1 ev6l :: Term -> Int 2 ev6l (Const6nt n) = n

3 ev6l (Plus t t ) = ev6l t + ev6l t 4 ev6l (Mult t t ) = ev6l t * ev6l t

ka première ligne donne le type de l’opération : elle consomme un terme pour produire un entier. bhaque variante de terme est traitée explicitement sur chaque ligne. ri le terme est une constante, c’est la ligne 2 qui sera exécutée, si c’est unPlus, la ligne 3, et ainsi de suite.

nn remarque que l’évaluation est récursive pourPlusetMult. ka déǤnition du type algébrique l’était déjà :Termest utilisé dans sa propre déǤnition. kes grammaires de langage sont souvent ré- cursives, pour permettre la construction de grandes expressions à partir de simples éléments. lême un langage aussi simple que celui-ci exhibe cette récursion.

nn peut maintenant évaluer l’expression 2 + 5 * 3 :

> ev6l (Plus (Const6nt )

(Mult (Const6nt ) (Const6nt ))) nn peut voir l’aǦcheur comme la réci-

proque de l’analyseur syntaxique. rauf qu’ici on ne déǤnit pas cet l’analyseur, puisqu’on construit directement les arbres syntaxiques.

tn interpréteur peut déǤnir plusieurs opérations. vadler prend l’exemple d’un aǦcheur (pretty-printer), une opération d’aǦchage des arbres syntaxiques dont la sortie est plus amicale pour l’œil humain. sypiquement, l’aǦcheur d’un langage transforme l’arbre syntaxique en code source que le programmeur a l’habitude de lire. Appliqué aux expressions arithmétiques, il faut transformer le Plus, qui est préǤxe, en + inǤxe, et faire disparaître le mot

. . Le problème de l’expression chinqua

pp :: Term -> String pp (Const6nt n) = show n

pp (Plus t t ) = pp t ++ " + " ++ pp t pp (Mult t t ) = pp t ++ " * " ++ pp t

ri on aǦche ainsi l’arbre syntaxique de l’expression 2 + 5 * 3, on obtient :

> pp (Plus (Const6nt )

(Mult (Const6nt ) (Const6nt ))) + *

k’implémentation de l’aǦchage a la même structure que de l’im- plémentation de l’évaluation. nn traite les trois variantes du type algébrique dans trois lignes diǣérentes. kes deux variantesPluset

Multsont déǤnies récursivement, mais pas la varianteConst6nt, qui est un terminal dans la grammaire. k’opération traite l’arbre syntaxique en partant de la racine, puis s’applique récursivement sur les nœuds de l’arbre jusqu’aux feuilles. be processus, commun aux interpréteurs, est parfois appelé une descente récursive.

cans le problème de l’expression, on cherche à étendre le lan- gage et son interpréteur de deux façons : en rajoutant des nou- veaux termes au langage, et en rajoutant de nouvelles opérations. Avec pp, on a déjà vu comment ajouter une nouvelle opéra- tion à l’interpréteur : il suǦt de déǤnir son comportement pour toutes les variantes des termes du langage. Term a 3 variantes, donc pp déǤnit 3 cas. motons aussi que pour ajouter l’opération il suǦt d’ajouter du code à l’interpréteur. Aucune modiǤcation au code existant n’est requise. b’est une distinction importante, car il est souvent plus facile d’ajouter du code que de modiǤer du code existant.

nn peut caractériser la complexité d’un changement de l’in- terpréteur comme la complexité d’un algorithme. k’opérationpp

comporte autant de lignes qu’il y a de variantes deTerm, plus une pour sa signature. r’il y a v variantes, ajouter une opération aura une complexité de l’ordre de v lignes de code (n(v)). nn note que ces lignes sont des additions par un ’+’ : +n(v).

nn notera les lignes qui modiǤent une déǤnition existante par ’~’, et les lignes supprimées par un ’-’.

Étendre le langage demande de modiǤer le code existant. ri on veut rajouter un terme pour la soustraction, il faut commencer par étendre le type algébriqueTerm:

d6t6 Term = Const6nt Int | Plus Term Term | Mult Term Term ~ | Minus Term Term

b’est une ligne de code ajoutée. lais c’est surtout une redé ni- tion du type algébrique. sypiquement, ajouter une variante induit une recompilation. nn compte donc cette ligne comme une modi- Ǥcation, et non un simple ajout.

chourin Le détournement à travers l’histoire des langages de programmation

lais il faut aussi traiter cette nouvelle variante dans les opéra- tions existantes :

const

mult

plus

eval

pp

cans une décomposition fonctionnelle classique, les opérations implémentent du code pour chaque variante du lan- gage (sens des ǥêches). Ajouter une opération (à gauche) n’aǣecte pas les unités existantes, mais ajouter un terme (à droite) nécessite de modiǤer toutes les opérations.

ev6l :: Term -> Int

~ ev6l (Minus t t ) = ev6l t - ev6l t pp :: Term -> String

~ pp (Minus t t ) = pp t ++ " - " ++ pp t

Au total, si on apopérations dans l’interpréteur, le changement a une complexité de ~n(p).

kes deux changements sont linéaires en lignes de code ajoutées. lais ajouter une opération et ajouter un terme ne sont pas des changements similaires pour autant. Ajouter un terme demande de modi er des déǤnitions existantes dans l’interpréteur, alors que pour ajouter une opération il suǦt d’adjoindre une nouvelle fonc- tion.

ke problème de l’expression touche à la fois à la modularité et à l’extensibilité. À l’extensibilité, parce qu’on cherche à étendre l’interpréteur. dt à la modularité, parce qu’on cherche à décou- per les termes et les opérations en modules, à réduire leurs dé- pendances. bhaque extension devrait être restreinte à un module. cans l’exemple en gaskell, ajouter une opération revient à ajouter un module sans aǣecter les autres, mais ajouter un terme requiert de modiǤer plusieurs modules.

Le problème inverse en programmation par objets

ke problème de l’expression se pose aussi en programmation par objets.

tne façon classique d’implémenter un interpréteur en program- mation par objets est de suivre le patron hnterpréteur zfam+94]. cans ce patron, chaque terme du langage est représenté par une classe, et chaque classe possède une méthode par opération.

6bstr6ct cl6ss Term { 6bstr6ct int ev6l(); } cl6ss Const6nt extends Term {

int n;

Const6nt(int n) { this.n = n; } int ev6l() { return n; }

}

cl6ss Plus extends Term { Term t , t ;

Plus(Term t , Term t ) { this.t = t ;

this.t = t ; }

. . Le problème de l’expression ché

int ev6l() {

return t .ev6l() + t .ev6l(); }

}

cl6ss Test {

public st6tic void m6in(String[] 6rgs) { Term e = new Plus(new Const6nt( ),

new Const6nt( )); System.out.println(e .ev6l()); //:

} }

hci on a deux classesConst6ntetPlusqui implémentent l’opé- ration ev6l. sous les termes héritent de la classe abstraite Term

qui déǤnit les opérations supportées. bomme précédemment, l’éva- luation dePlus.ev6lest récursive. bette fois en revanche, la dé- Ǥnition de l’opérationev6l n’est pas localisée en un seul endroit dans le code source, mais répartie dans les objets. bhaque classe de terme déǤnit sa propre évaluation.

dn conséquence, ajouter un terme au langage se fait simple- ment en ajoutant une nouvel classe :

cl6ss Mult extends Term { Term t , t ; Mult(Term t , Term t ) { this.t = t ; this.t = t ; } int ev6l() {

return t .ev6l() * t .ev6l(); }

}

nn n’a pas à modiǤer le code des autres classes. Ajouter un terme au langage revient à ajouter +n(p) lignes, s’il y a p opéra- tions.

dn revanche, pour ajouter une opération comme l’aǦchage il faut modiǤer toutes les classes existantes :

6bstr6ct cl6ss Term { ~ 6bstr6ct String pp(); cl6ss Const6nt extends Term { ~ String pp() { ... }

cl6ss Plus extends Term { ~ String pp() { ... } cl6ss Mult extends Term { ~ String pp() { ... }

cicerole Le détournement à travers l’histoire des langages de programmation

const