Dans cet exercice il s’agit de définir quelques touches supplémentaires et de
les insérer dans la représentation graphiques. Commençons par la definition
des touches.
La première touche, la touche point, n’a aucun calcul associé: elle ne sert
que pour l’entrée d’un nombre réel. Comme les touches des chiffres,
son activation doit donc lancer la méthode activeNumber: qui,
rappelons-le, doit, premièrement, distinguer entre le cas où le chiffre
entrée est le début d’un nouveau nombre et celui où il est la suite
d’un nombre déjà sur l’écran et, deuxièmememnt, elle doit éliminer,
pour rendre plus aisé la lecture, l’éventuel chiffre 0 en-tête et donc
non-significatif.
L’élimination d’un 0 en-tête n’a pas de sens pour l’activation de la
touche point: le 0 devient significatif. Créons alors une méthode
d’activation supplémentaire, responsable juste de l’insertion du caractère
point:
Calculatrice>>activeNumberPoint: label
firstTime := false.
bufferEcran := bufferEcran , label.
self changed: #printString
|
|
Ceci permettra d’entrer des nombres comme 0.06598803584 ou encore le fameux nombre
22.459157738.LamthodeasNumbersaura,commepourleschanesdecaractrescorrespondantdesentiers,transformerdetelleschanesendesnombres.39 La
deuxème touche, la touche x
y est une touche d’opérateur comme
nos autres touches +, -, × et /. Pas tout à fait: pendant que les
touches opérateurs que nous avons implémentées jusqu’à maintenant
correspondent à des opérateurs binaires dont le sélecteur est le symbole que
nous affichons sur la touche, la méthode SMALLTALK calculant xy
s’appelle raisedTo: et ce n’est sûrement pas cette chaîne que nous
désirons afficher. Nous voulons que la touche affiche x
y et calcule
raisedTo:.
Cela implique que pour l’implémentation de cet opérateur les méthodes
creeTouches:with:..., comme dans l’expression (cf 736):
self
creeTouches: #(#(’+’ ’-’ ’*’ ’/’) )
with: {AlignmentMorph newColumn}
and: window
function: #activeFonction:
frame: 3 / 5 @ (1 / 5)
extent: 1 / 5 @ (4 / 5).
|
|
doivent avoir en argument supplémentaire la chaîne de caractères affichée
sur la touche. Nous aurons alors une transmission correspondant à la
structure ci-dessous:
self
creeTouches: #(#(’+’ ’-’ ’*’ ’/’)
#(’x^y’ ’sqrt’ ’1/x’ ’.’))
with: #(#(#+ #- #* #/)
#(#raisedTo: #sqrt #inv ’.’))
with: {AlignmentMorph newColumn.
AlignmentMorph newColumn}
and: window
function: #(#(#activeFonction: .... ))
frame: {(3 / 5 @ (1 / 5)
extent: 1 / 5 @ (4 / 5). ...}
|
|
qui donne en argument, pour la création des touches, à la fois l’étiquette à
afficher dans la représentation graphique et le sélecteur de la méthode
permettant l’évaluation de l’opération associée à la touche activée. Il n’aura
plus une équivalence entre ces deux aspects.
Elle permet aussi d’initialiser plusieurs rangées de touches à la fois.
Définissons alors cette méthodes d’initialisation (au nombre prohibitif
d’arguments):
1 Calculatrice>>creeTouches: tabEtiquette
2 with: tabSelecteur
3 and: tabMorphs
4 function: tabActivF
5 frame: rects
6 window: window
7 | view a1 r |
8 tabEtiquette
9 with: tabSelecteur
10 and: tabMorphs
11 and: tabActivF
12 and: rects
13 do: [:touches :funs :m :foncs :rect |
14 touches with: foncs
15 and: funs
16 do: [:touche :fonc :fun |
17 r:= rect.
18 view := (self
19 newTouche: fun
20 avec: fonc)
21 with: window color
22 and: touche.
23 m addMorph: view. a1 := m].
24 window
25 addMorph:a1
26 frame: r].
|
|
Commentaires:
- Lignes 1 à 6: la méthode attend 6 arguments,
-
- tabEtiquettes est un tableau contenant des sous-tableaux avec
les étiquettes d’une ligne ou d’une colonne de touches.
-
- tabSelecteur est le tableau des sélecteurs correspondant aux
touches.
-
- tabMorph contient les AlignmentMorph pour chacune de ces
rangées de touches.
-
- tabActivF contient, pour chaque touche, le sélecteur de la
méthode à transmettre à la claculatrice pour qu’elle puisse
lancer le calcul associé à une touche.
-
- rects contient le rectangle où il faut positionner chacun des
AlignmentMorph.
-
- window est l’instance de SystemWindow de notre calculatrice.
- Ligne 7: déclare trois variables temporaires, a1 et m servent à
transmettre le morph et le rectangle calculés dans la boucle inférieure
vers la boucle extérieure.
- Lignes 8 à 13: initialise le parcours en parallèle du tableau receveur,
le tableau des étiquettes, et des quatre tableaux donnés en
argument: les sélecteurs arithmétiques, les morphs, les sélecteurs de
lancement et les rectangles de positionnement. Les arguments
du bloc sont donc, dans l’ordre: les étiquettes d’une rangée
de touches, les sélecteurs correspondants à ces étiquettes, le
morph pour cette rangée, les sélecteurs de lancement pour
ces touches et le rectangle de placement pour cette rangée de
touches.
- Ligne 14 à 23: pour chacune des étiquettes nous créons une instance
de la classe Touche ayant le sélecteur correspondant comme
label et le sélecteur de lancement comme fonction. Chacune
des étiquettes est ajoutée au PluggableButtonMorph de la
touche, et chacune des touches est ajoutée à l’AlignmentMorph
courant.
- Ligne 24 à 26: chaque AlignmentMorph de la boucle intérieure est
ajouté à la représentation graphique de la calculatrice à l’endroit
indiqué par le rectangle de positionnement.
Cette méthode de création de rangées de touches utilisent deux méthodes
auxiliaires with:and:do: et with:and:and:and:do: pour le parcours en
parallèle de respectivement trois et cinq collection séquenceable que
voici:
SequenceableCollection>>with: otherCollection
and: yetOtherCollection
do: threeArgBlock
1 to: self size do:
[:index |
threeArgBlock value: (self at: index)
value: (otherCollection at: index)
value: (yetOtherCollection at: index)]
|
|
SequenceableCollection>>with: otherCollection
and: yetOtherCollection
and: fourthCol
and: fifthCol
do: fiveArgBlock
1 to: self size do:
[:index |
fiveArgBlock value: (self at: index)
value: (otherCollection at: index)
value: (yetOtherCollection at: index)
value: (fourthCol at: index)
value: (fifthCol at: index)]
|
|
Cette dernière méthode nécessite utilise un bloc à cinq arguments. SQUEAK
n’a pas prévu des utilisateurs ayant besoin d’un tel bloc. Ajoutons donc
aussi le message value:value:value:value:value: à la classe
BlockContext:
BlockContext>>value: arg1 value: arg2 value: arg3
value: arg4 value: arg5
^self valueWithArguments:
(Array
with: arg1
with: arg2
with: arg3
with: arg4
with: arg5)
|
|
Avant de mettre à jour notre méthode openInWorld pour y intégrer ces
changements, examinons les deux touches supplémentaires que nous
devons encore définir: les touches sqrt et 1/x. À la différence avec
toutes les autres touches d’opérateurs que nous avons implémentées
jusqu’à maintenant, ces deux opérateurs sont unaires: ils n’ont
besopin que d’un seul argument. Nous ne pouvons donc pas les activer
avec la méthode activeFonction:, puisque celle-ci attend deux
arguments.
Définissons alors une méthode responsable pour l’activation des opérateurs
uniaires. Plustôt: définissons en deux versions: une pour la calculatrice
postfixe et une pour la calculatrice infixe.
Commençons par la calculatrice postfixe. Fondamentalement, l’activation
d’un opérateur unaire ne sera pas très différent d’un opérateur binaire. Pour
une activation d’un opérateur binaire, le premier argument est dans
l’accumulateur accu et le deuxième est, sous forme d’une chaîne de
caractères sur l’écran, donc dans la variable bufferEcran. Pour une
activation d’un opérateur unaire, le seul et unique argument doit être le
nombre qui se trouve sur l’écran, le dernier nombre entré. Il suffit
de transmettre le sélecteur associé à la touche à ce nombre, et,
pour le reste, faire exactement la même chose que nous faisons pour
les opérateurs binaires. Rappelons la méthode activeFonction:
responsable de l’activation des opérateurs binaires (repris de la page
515):
CalculatricePolonaise>>activeFonction: label
self changed: #hist
with: ’Enter > ’ , bufferEcran.
accu := accu perform: label asSymbol
with: bufferEcran asNumber.
bufferEcran := accu printString.
self changed: #hist
with: label , ’ > ’ , bufferEcran.
self changed: #printString.
firstTime := true
|
|
Sa simple adaptation à un argument unique nous livre la méthode
suivante:
CalculatricePolonaise>>activeFonctionUnaire: label
self changed: #hist with: ’Enter > ’ , bufferEcran.
accu := bufferEcran asNumber perform: label asSymbol.
bufferEcran := accu printString.
self changed: #hist
with: label , ’ > ’ , bufferEcran.
self changed: #printString.
firstTime := true
|
|
Ces deux méthodes ont tellement des transmissions en commun qu’une
refactorisation s’impose. Extrayons d’abord les parties commune vers la
nouvelle méthode activer::
CalculatricePostfixe>>activer: label
self changed: #hist with: ’Enter > ’ , bufferEcran.
bufferEcran := accu printString.
self changed: #hist with: label , ’ > ’ , bufferEcran.
self changed: #printString.
firstTime := true
|
|
pour ensuite simplifier les deux méthodes d’activation:
CalculatricePostfixe>>activeFonction: label
accu := accu perform: label
with: bufferEcran asNumber.
self activer: label
|
|
et
CalculatricePostfixe>>activeFonctionUnaire: label
accu := bufferEcran asNumber perform: label.
self activer: label
|
|
Pour terminer l’adaptation de notre calculatrice postfixe aux changements
induits par l’ajout de ces quelques touches, il nous faut encore modifier la
méthode openInWorld. Par rapport à la version donnée page 735 ce ne sont
que les parties d’initialisation des rangées de touches contenant des touches
d’opérations que nous devons modifier:
Calculatrice>>openInWorld
| window view c |
window := SystemWindow labelled: self class name.
view := PluggableTextMorph
on: self
text: #printString
accept: nil.
view
font: (StrikeFont familyName: ’Atlanta’ size: 22).
window
addMorph: view
frame: (0 @ 0 extent: 1 @ (1 / 5)).
view := PluggableTextMorph
on: (c := Ecran on: self)
text: #hist
accept: nil
readSelection: #selectTextInterval
menu: #myMenu:.
self addDependent: c.
view setTextColor: Color blue.
window
addMorph: view
frame: (4 / 5 @ (1 / 5) extent: 1 / 5 @ (4 / 5)).
self
creeTouches: #(#(9 8 7) #(6 5 4) #(3 2 1) )
with: {AlignmentMorph newRow.
AlignmentMorph newRow.
AlignmentMorph newRow}
and: window
function: #activeNumber:
frame: 0 @ (1 / 5)
extent: 12 / 25 @ (1 / 5).
self
creeTouches: #(#(’=’ ’0’ ’C’)
#(’+’ ’-’ ’*’ ’/’)
#(’.’ ’sqrt’ ’1/x’ ’x^y’ ))
with: #(#(’=’ ’0’ ’C’)
#(#+ #- #* #div:)
#(’.’ #sqrt #inv #raisedTo: ))
|
|
and: {AlignmentMorph newRow.
AlignmentMorph newColumn.
AlignmentMorph newColumn}
function:
#(#(#activeEgal: #activeNumber: #activeClear:)
#(#activeFonction: #activeFonction:
#activeFonction: #activeFonction:)
#(#activeNumber: #activeFonctionUnaire:
#activeFonctionUnaire: #activeFonction:))
frame: {0 @ (4 / 5) extent: 12 / 25 @ (1 / 5).
12 / 25 @ (1 / 5) extent: 4 / 25 @ (4 / 5).
16 / 25 @ (1 / 5) extent: 4 / 25 @(4 / 5).}
window: window.
window openInWorld
|
|
Pour ne pas obtenir des résultats fractionnaires, nous avons également
ajouté la méthode inv à la classe Number:
Number>>inv
^self reciprocal asFloat
|
|
Enfin, nous avons une calculatrice polonaise permettant des opérations
unaires et binaires sur des nombres entiers et réels. Ci dessous alors la
méthode activeFonctionUnaire: pour la claculatrice infixe. Après les
modifications que nous avons apportées ci-dessus à la classe Calculatrice,
c’est la seule modification nécessaires pour permettre ces touches aussi dans
la classe CalulatriceInfixe.
CalculatriceInfixe>>activeFonctionUnaire: label
self changed: #hist with: ’ > ’ , bufferEcran.
memFonc notNil
ifTrue: [accu := accu perform: memFonc
with: bufferEcran asNumber.
bufferEcran := accu asString].
accu := bufferEcran asNumber perform: label.
self changed: #hist with: label , ’ > ’ , bufferEcran.
bufferEcran := accu printString.
self changed: #printString.
memFonc := nil.
|
|
Même si nous arrêtons ici temporairement le développement de notre
calculatrice postfixe, vous pouvez toujours continuer à la faire évoluer.
Un bon but intermédiaire est d’y inclure aussi des possibilités de
pouvoir calculer des sous-expressions. Par exemple, l’expression
postfixée:
30 3 4 5 + ×-
devrait afficher le résultat 3, résultat de l’expression infixée 30 - (3 × (4 + 5)),
et l’expression:
30 3 4 + 5 ×-
devrait livrer -5, le résultat de l’expression infixée 30 - ((3 + 4) × 5).
Pour la calulatrice infixe, le calcul de sous-expressions demande l’ajout
des touches de parenthésage, les touches ( et ). Pouvez-vous les
programmer?
Vérifions si vous avez choisi les mêmes solutions que celles proposées
ci-dessous. Commençons, comme toujours, par la calculatrice postfixe.
Pour pouvoir calculer l’expression 30 3 4 5 + ×- avec notre calculatrice,
nous devrons entrer d’abord les quatres nombres, donc les mémoriser, pour
ensuite appliquer l’opérateur + aux deux dernier nombres mémorisés. Ce
calcul doit remplacer les deux opérandes, 4 et 5, par le résultat de leur
addition, 9. Maintenant la situation se présente alors comme si l’on avait
mémorisé les trois nombres 30, 3 et 9, et qu’il faudrait appliquer l’opératuer
× aux deux dernier nombres mémorisés. Ce qui résulte dans la
mémorisation de juste deux nombres, 30 et 27 (le résultat de la
multiplication de 3 par 9. Finalement, l’opération de soustraction enlève les
deux nombres et les remplace par le résultat de la soustraction, donc
3.
Notre accumulateur accu, ne doit donc plus seulement garder une valeur
unique, mais, éventuellement, tout une collection de valeurs. Bref: il doit se
comporter comme une pile, comme nos L-systèmes avec crochets (cf. pages
429 et suivante). Et comme pour nos L-système nous allons utiliser pour
l’implémentation de la pile une OrderedCollection et l’opération
d’empilage sera réalisée par la méthode addFirst, celle de dépilage par la
méthode removeFirst, et celle de consultation du sommet de la pile par la
méthode first, des méthodes bien comprises par toute collection
ordonnée.
Débutons la programmation par l’ajout d’une méthode initialize à notre
calculatrice postfixe:
CalculatricePostfixe>>initialize
super initialize.
accu := OrderedCollection new
|
|
et, pour continuer à pouvoir utiliser les calculatrices infixes, adaptons
immédiatement aussi leur méthode d’initialisation:
CalculatricesInfixe>>initialize
super initialize.
memFonc := nil.
accu := 0
|
|
Ces deux modifications impliquent que nous modifions également la
méthode initialize de la super-classe Calculatrice, en y éliminant
l’initialisation de la variable accu:
Calculatrice>>initialize
bufferEcran := ’0’.
firstTime := true.
self changed: #printString
|
|
Ensuite, cherchons, dans la classe CalculatricePolonaise, ces méthodes où la
variable accu est utilisée, afin de l’adapter à son nouveau comportement de
pile.40
Il en existent cinq: la méthode initialize, que nous venons d’écrire, et les
méthodes activeFonction:, qui active un opérateur à deux arguments,
activeFonctionUnaire:, qui active un opérateur à un argument,
activeEgal:, qui termine l’entrée d’un nombre et activer: qui factorise les
parties communes des traitement des opérateurs. Nous devons modifier
chacune d’elles.
Dans la méthode activeEgal: nous devons juste changer l’affectation
simple de la variable d’instance accu à la valeur numérique du nombre écrit
sur l’écran en un empilement de ce même nombre dans la collection
ordonnée accu, ce qui donne la nouvelle version suivante:
CalculatricePostfixe>>activeEgal: label
self changed: #hist with: ’Enter > ’ , bufferEcran.
accu addFirst: bufferEcran asNumber.
firstTime := true
|
|
La méthode activer: utilise l’accumulateur pour afficher son contenu sur
l’écran de la calculatrice. Bien évidemment, nous afficherons maintenant le
sommet de la pile accu, donc son premier élément:
CalculatricePostfixe>>activer: label
self changed: #hist with: ’Enter > ’ , bufferEcran.
bufferEcran := accu first printString.
self changed: #hist with: label , ’ > ’ , bufferEcran.
self changed: #printString.
firstTime := true
|
|
La méthode activeFonction: doit toujours utiliser au moins un des
éléments de la pile comme opérande. Si la pile ne possède qu’un seul
élément,41
cet élement sera le premier opérande et la valeur numérique de l’ecran sera
le deuxième opérande. Si la pile possède au moins deux éléments, le
deuxième élément sera le premier opérande et le premier le deuxième
opérande. La méthode doit également empiler le résultat de l’opération.
C’est exactement cet algorithme qu’implémente la nouvelle version de
activeFonction ci-dessous:
CalculatricePostfixe>>activeFonction: label
| x y |
x := accu removeFirst.
accu isEmpty
ifTrue: [y := x.
x := bufferEcran asNumber]
ifFalse: [y := accu removeFirst].
accu
addFirst: (y perform: label with: x).
self activer: label
|
|
La méthode activeFonctionUnaire: est équivalent à la méthode ci-dessus,
sauf que l’opération associée ne nécessite qu’un unique argument. Cet
argument sera soit le sommet de la pile accu, soit l’ecran si la pile est vide.
Bien entendu, le résultat de l’opération doit à nouveau être empilé. Voici
alors sa nouvelle implémentation:
CalculatricePostfixe>>activeFonctionUnaire: label
accu
addFirst:
((accu isEmpty
ifTrue: [bufferEcran asNumber]
ifFalse: [accu removeFirst])
perform: label).
self activer: label
|
|
> |
| FIG. A.45: | Une
calculatrice
postfixe après
calcul de
30 3 4 5 + ×- |
|
|
La figure A.45 montre une calculatrice postfixe où l’on peut reconnaître
dans la fenêtre historique le calcul de 30 3 4 5 + ×-, donc de
30 - (3 × (4 + 5)). Cette calculatrice permet bien de calculer des
sous-expression sans l’utilisation de parenthèses, puisque l’ordre de
l’écriture des opérandes et opérateurs est suffisant pour déterminer les
sous-expressions.
Par contre, pour pouvoir grouper des sous-expressions dans une calculatrice
infixe nous avons besoin des opérateurs parenthèse ouvrante, correspondant
à début d’une sous-expression, et parenthèse fermante, correspondant à fin
d’une sous-expression.
Pour faire évoluer notre calculatrice infixe vers une qui sache traîter des
parenthèses, la première chose à faire est de déterminer l’emplacement pour
ces deux touches. Si nous réduisant l’écran historique un peu en hauteur,
nous pouvons insérer les deux petites touches ( et ) en bas à droite de la
calculatrice, comme montré sur la figure A.46.

| FIG. A.46: | Une
calculatrice infixe
après calcul de 30-
(3 × (4 + 5)) |
| |
Après les modification apportées dans la section précédente à la méthode
Calculatrice»openInWorld, l’insertion de ces deux touches ne pose pas
de problèmes. Nous devons réduire la taille verticale de la fenêtre
historique de un cinquième, il faut modifier les lignes ou nous plaçons le
PluggableTextMorph sur l’écran: l’argument ’extent: 1
/ 5@4
/ 5 doit devenir
1
/ 5@3
/ 5, ce qui donne les lignes:
window
addMorph: view
frame: (4 / 5 @ (1 / 5) extent: 1 / 5 @ (3 / 5)).
|
|
Pour inserer les deux touches supplémentaires, il suffit d’insérer leurs
spécifications dans les tableaux arguments de la dernière transmission de
creeTouches:with:..., ce qui donne la nouvelle version de cette
transmission que voici (nous avons marqué les lignes nouvellement insérées
avec le commentaire "–>"):
self
creeTouches: #(#(’=’ ’0’ ’C’)
#(’+’ ’-’ ’*’ ’/’)
#(’.’ ’sqrt’ ’1/x’ ’x^y’ )
" --> " #(’)’ ’(’))
with: #(#(’=’ ’0’ ’C’)
#(#+ #- #* #div:)
#(’.’ #sqrt #inv #raisedTo: )
" --> " #(’)’ ’(’))
|
|
and: {AlignmentMorph newRow.
AlignmentMorph newColumn.
AlignmentMorph newColumn.
" --> " AlignmentMorph newRow}
function:
#(#(#activeEgal: #activeNumber: #activeClear:)
#(#activeFonction: #activeFonction:
#activeFonction: #activeFonction:)
#(#activeNumber: #activeFonctionUnaire:
#activeFonctionUnaire: #activeFonction:)
" --> " #(#activeFermante: #activeOuvrante:))
frame: {0 @ (4 / 5) extent: 12 / 25 @ (1 / 5).
12 / 25 @ (1 / 5) extent: 4 / 25 @ (4 / 5).
16 / 25 @ (1 / 5) extent: 4 / 25 @(4 / 5).
" --> " 4 / 5 @ (4 / 5) extent: 1 / 5 @ (1 / 5)}
window: window.
|
|
Puisque la méthode openInWorld est une méthode de la sur-classe de nos
deux calculatrices, postfixe et infixe, nous avons maintenant les touches des
parenthèses dans les deux sous-classes.
Commençons par rendre ces touches ineffectives pour la calculatrice
postfixe, simplement en y définissant les méthodes activeOuvrante
et activeFermante: de manière à ne rien faire du tout. Voici ces
deux méthodes qui nous garantissent que toute instance de la classe
CalculatricePostfixe sait traîter les parenthèses:
CalculatricePostfixe>>activeOuvrante: label
|
|
CalculatricePostfixe>>activeFermante: label
|
|
Pour ne rien faire, il ne font rien: leur corps est vide! Cela règle le
comportement des touches parenthèses pour la calculatrice postfixe.
Pour la calculatrice infixe nous pouvons nous laisser inspirer par les
modifications ayant permis de rendre la calculatrice postfixe capable de
traiter des sous-expressions. Nous avons transformé notre accumulateur
accu en une pile. Probablement nous avons besoin de faire la même chose
ici: à l’entrée d’une sous-expression, donc à la rencontre d’une parenthèse
ouvrante, il faut sauvegarder la valeur actuelle de l’accumulateur pour
recommencer comme si l’on avait à calculer une expression indépendante. À
la sortie d’une sous-expression, donc à la rencontre d’une parenthèse
fermante, il faut 1) calculer la valeur de la sous-expression et 2)
prendre cette valeur comme deuxième opérande de ...En effet, de
quoi? ...de l’opérateur duquel la sous-expression était le deuxième
opérande.42
Mais nous n’avons plus cet opérateur, il était dans la variable d’instance
memFonc avant d’entrer dans la sous-expression. Puisque cette entrée dans
une sous expression faisait recommencer un calcul, la variable memFonc
changeait de valeur durant ce nouveau calcul. Nous avons donc aussi besoin
de transformer la variable d’instance memFonc en une pile, pour pouvoir
retrouver l’opérateur qui avait besoin de la valeur de la sous-expression
comme deuxième opérande.
Commençons par changer la méthode initalize de la sur-classe de nos
calculatrices pour qu’elle initalise la variable accu toujours à une nouvelle
collection ordonnée – et enlevons cette même initialisation de la méthode
initialize de la classe CalculatricePostfixe, ce qui revient au même
que d’éliminer complètement cette méthode:
Calculatrice>>initialize
bufferEcran := ’0’.
firstTime := true.
accu := OrderedCollection new.
self changed: #printString.
|
|
Ensuite, créons deux variables d’instance supplémentaire pour les
calculatrices infixe, des sortes d’indicateurs qui nous permettent de savoir si
une des deux touches de parenthèses vient d’être activé. Nous en aurons
surement besoin. Ce qui donne la nouvelle définition:
Calculatrice subclass: #CalculatriceInfixe
instanceVariableNames: ’memFonc openPar closePar’
classVariableNames: ’’
poolDictionaries: ’’
category: ’Cours-Smalltalk’
|
|
Modifions aussi la méthode initialize pour qu’elle donne des valeurs
initiales correctes à ces trois variables d’instance:
CalculatriceInfixe>>initialize
super initialize.
closePar := openPar := true.
memFonc := OrderedCollection new.
accu addFirst: 0
|
|
Nous savons déjà ce qu’il faut faire à la rencontre d’une parenthèse
ouverte: sauver l’état de calcul et faire comme si l’on était au début
d’un nouveau calcul. Puisque nous avons une variable d’instance
indicateur d’une rencontre d’une parenthèse ouverte, changeons
également sa valeur. ce qui nous donne la méthode activeOuvrante:
suivante:
CalculatriceInfixe>>activeOuvrante: label
memFonc isEmpty
ifTrue:
[^ self changed: #hist
with: ’( > ’ , accu first printString]
ifFalse: [accu addFirst: 0.
openPar:=false]
|
|
Le test regardant si un opérateur a déjà été entré est notre verification que
nous avons bien affaire à une sous-expression se trouvant en position de
deuxième opérande d’un opérateur. Si ce n’est pas le cas, une petite
indication est affichée sur l’écran historique.
Mais supposons que nous avons déjà entré le nombre 30 et l’opérateur “−”.
L’accumulateur contient alors la collection #(30) et change sa valeur après
l’activation de la touche ( en la collection #(0 30). Cette activation fait
aussi changer la valeur de la variable openPar en la valeur false. Puisque
l’activation de la touche parenthèse avait lieu après l’activation de la
touche opérateur −, nous savons que la méthode activeFonction
positionne la variable memFonc. Cette variable a maintenant acqui le
comportement d’une pile, nous devons donc changer son affectation actuelle,
memFonc := label en une expression d’empilage de l’opérateur, ce qui
donne:
CalculatriceInfixe>>activeFonction: label
(firstTime
and: [accu first = 0])
ifTrue: [^ self].
self active: label.
memFonc addFirst: label
|
|
Nous avons en même temps changé l’expression accu = 0, qui marchait
tant que accu était une variable simple, en accu first = 0, pour prendre
en compte sa transformation en une pile.
Continuons le suivi du calcul de l’expression “30 − (“ et supposons que
l’utilisateur active la touche 3 suivi de l’activation de la touche × ce qui a
comme effet l’activation de la méthode activeFonction, qui, elle, lançe la
méthode active. Nous l’avons adapté pour quelle prenne en compte les
deux piles et l’état de la variable openPar:
CalculatriceInfixe>>active: label
1 (memFonc isEmpty not and: [openPar])
2 ifTrue:
3 [accu addFirst:
4 (accu removeFirst
5 perform: memFonc first
6 with: bufferEcran asNumber)]
|
|
7 ifFalse:
8 [accu removeFirst.
9 accu addFirst: bufferEcran asNumber.
10 openPar := true].
11 self changed: #hist
12 with: bufferEcran , ’ > ’ , label.
13 bufferEcran := accu first printString.
14 self changed: #printString.
15 firstTime := true
|
|
Commentaires:
-
- Lignes 1 à 6: vérifie qu’une opération a été mémorisée et que nous nous
trouvons pas dans un re-début de calcul initié par une parenthèse
ouvrrante. Pour notre suivi du calcul de l’expression 30 − (3 ×,
openPar vient d’être mis à la valeur false. Ce test est donc non
vérifié et nous continuons dans la ligne 8.
Quand openPar est true, le cas ordinaire, le test est vérifié et
la méthode commence par échanger le premier élément de la
collection accu, donc le sommet de la pile des valeurs accu, avec
le résultat de l’opérateur mémorisé appliqué au sommet de la pile
accu et le nombre affiché sur l’écran.
-
- 7 à 10: nous n’arrivons dans la ligne 8 que si le test de la ligne 1
n’est pas vérifié, c’est-à-dire: s’il n’y a pas d’opérateur mémorisé
ou/et un sous-calcul a débuté puisqu’une parenthèse ouvrante a
été entrée préalablement.
Pour notre calcul exemple, le calcul de l’expression 30 − (3 ×,
nous nous trouvons dans le début du calcul d’une sous-expression.
Il faut alors remplacer le sommet de la pile accu, qui est
actuellement la valeur numérique 0, par le nombre affiché sur
l’écran.
stkaccu prend alors la nouvelle valeur #(3 30). Dans
tous les cas, il faut remettre la valeur de openPar à true, sa valeur
par défaut.
-
- Lignes 11 et 12: envoient aux dépendants sachant reconnaître l’aspect
#hist la chaîne de caractères composée du nombre sur l’écran et
de l’opérateur rencontré. Nous le savons, dans notre cas actuel,
l’écran historique va afficher 3 > *.
-
- Ligne 13 et 14: L’écran affiche le sommet de la pile accu. Pour notre
example cela ne change rien, par contre si nous avions exécuté
les lignes 3 à 6, le nombre affiché sur l’écran serait le résultat du
calcul effectué dans ces lignes.
-
- Ligne 15: indique qu’un nouvel nombre doit être entré.
Au retour de la méthode active:, activeFonction: mémoise,
en enpilant dans memFonc, l’opérateur rencontré. La figure A.47
montre un inspecteur ouvert sur notre calculatrice à l’instant du
calcul que nous venons de décrire. On y reconnait les deux piles
et le texte de la variable bufferEcran qui est affiché sur l’écran
historique.
Continuons notre simulation et entrons le nombre 2 suivi d’une parenthèse
fermante. Maintenant nous avons une expression complète, l’expression
“3 × 2”, pour laquelle la parenthèse fermante doit fonctionner comme un
activeEgal: (l’entrée d’un nombre ordinaire dans une calculatrice
ordinaire), qui, soit transfère le nombre juste entré vers l’accumulateur, soit
calcule l’expression formée avec ce nombre comme deuxième opérande. Nous
nous trouvons dans ce deuxième cas. Une fois cette expression claculée,
puisqu’elle entière forme le deuxième opérande d’une autre opération en
attente, nous devons relancer le calcul d’une expression. Dans notre
cas, après avoir calculé 3 × 2, qui livre le résultat 6, nous devons
encore calculer l’expression 30 − 6. C’est exactement cela que font la
méthode activeFermante: et la méthode activeEgal: modifiée
ci-dessous:
CalculatriceInfixe>>activeFermante: label
memFonc isEmpty
ifTrue:
[^ self changed: #hist
with: ’) > ’ , accu first printString]
ifFalse:
[self activeEgal: memFonc first printString.
closePar := false.
accu removeFirst.
self activeEgal: memFonc first printString.
closePar := true]
|
|
CalculatriceInfixe>>activeEgal: label
(firstTime
and: [closePar])
ifTrue: [^ self].
self active: label.
memFonc isEmpty
ifFalse: [memFonc removeFirst].
self changed: #hist with: ’# > ’ , bufferEcran
|
|
Nous avons terminé (temporairement) l’écriture de la calculatrice
postfixe et infixe connaissant les opérateurs +, −, ×, /, xy, 1
/ x et √ et
sachant calculer correctement des sous-expressions. Bien entendu, le
programme marche si les entrées sont formées correctement. Pour le
transformer en un vrai programme utilisable non seulement par nous, les
programmeurs qui en connaissent les forces et les faiblesses, mais
aussi par d’autres personnes, des utilisateurs qui ont juste besoin
d’une calculatrice, beaucoup de travaux de mise au point restent à
faire.
Un programme commerciale ne peut pas se permettre de livrer une erreur
du système, dans lequel le programme a été écrit, quand une situation
imprévue arrive. Déjà, si l’on veut faire calculer à notre calculatrice infixe
une expression comme 30 - (3) une erreur se produit. Simplement
puisque nous avons écrit le programme en vue d’utilisateurs qui
pensent comme nous. Qui mettrait une parenthèse autour d’un
nombre? Nous ne mettons des parenthèses qu’autour d’expression
complète et correctement formée. N’est-ce pas? Et pourtant, cette
expression 30 − (3) est une expression arithmétique parfaitement
correcte et ne produit pas d’erreurs sur toute calculatrice qui se
respecte.
Pour faire un programme “distribuable”, il faut prévoir toutes les manières
possible d’utilisation, et non pas seulement les siennes propres. C’est une
bonne exercice. Mais nous n’allons pas, dans le cadre de ce livre, aborder les
questions de comment rendre les programmes résistants et moins
vulnérables. À vous de vous y lancer!
Cette dernière exercice, l’ajout d’une calculatrice préfixe, devrait confirmer
le bien fondé de notre classe abstraite Calculatrice. Pour qu’elle soit
correcte, nous ne devrions être obligé que de définir les trois méthodes
d’activation: celle pour la touche = et les deux pour les touches des
opérations unaires et les touches binaires. Tout le reste du comportement
doit être hérité, comme pour les calculatrices postfixe et infixe, de la
sur-classe Calculatrice.
Les expressions préfixées ont, comme les expressions postfixées, pas
besoin de parenthèses pour calculer des sous-expressions. Ainsi, notre
expression 30 − (3 × (4 + 5)), qui s’écrivait en notation postfixée comme
30 3 4 5 + × −, s’ecrit en notation préfixée comme −30 × 3 + 4 5. Sur une
calculatrice préfixe nous devons entrer cette expression dans exactement cet
ordre.
Puisque nous aurons deux calculatrices qui ignorent les parenthèses
ouvrantes et fermantes (et seulement une, la calculatrice infixe, qui en a
besoin), commençons par transférer les deux méthodes activeOuvrante: et
activeFermante: de la classe CalculatricePostfixe vers la classe
Calculatrice. Ainsi, toute calculatrice ayant besoin de ces deux touches
peut redéfinir les deux méthodes correspondantes, les autres héritent le
comportement spécifiant de les ignorer.
Pour rendre le caractère abstraite de la classe Calculatrice plus claire, et
pour informer tout programmeur futur d’autres calculatrices que les
méthodes d’activation des touches =, des opérations binaires et des
opérations unaires doivent être définies dans les sous-classes, définissons
aussi ces méthodes dans la classe Calculatrice comme:
Calculatrice>>activeEgal: label
self subclassResponsibility
|
|
Calculatrice>>activeFonctionUnaire: label
self subclassResponsibility
|
|
et
Calculatrice>>activeFonction: label
self subclassResponsibility
|
|
Analysons le calcul d’une expression en notation préfixée. Prenons comme
exemple l’expression
× + 2 3 + 5 5
- À l’activation de la touche × nous ne pouvons rien calculer: nous
avons encore besoin des deux opérandes.
- La même remarque est valide pour l’activation de la touche +.
- L’entrée du nombre 2 livre le premier opérande pour l’opération
+. Nous avons besoin d’un opérande supplémentaire.
- L’entrée du nombre 3 livre ce deuxième opérande. Maintenant
nous pouvons calculer la somme des deux nombres. L’opération
arithmétique + est terminée. Le résultat, 5, représente le premier
opérande de l’opérateur ×. Nous avons besoin encore d’un
deuxième opérande.
- À l’activation de la touche + nous attendons encore deux
opérandes.
- L’entrée du nombre 5 en livre le premier, et
- L’entrée du nombre 5 suivant nous en livre le deuxième.
Maintenant nous pouvons calculer la somme. L’opération
d’addition est terminée. Le résultat, l’entier 10, représente le
deuxième opérande de l’opération multiplication toujours en
attente. Maintenant nous pouvons calculer le produit. Il n’y a
plus aucune opération qui attend des opérandes. Le résultat de
l’expression entière est donc l’entier 50.
Clairement, durant le calcul d’un telle expression nous devons toujours
garder en mémoire les opérations qui attendent encore des arguments, le
nombre d’arguments de chacune des opérations et le nombre d’opérandes
encore en attente pour chacune des opération.
Pour implémenter cette mémorisation, clairement, la manière la plus simple
est d’utiliser trois piles:
-
- la pile memFonc pour les opérateurs,
-
- la pile numArgs pour le nombre d’argumenst des opérateurs,
-
- la pile compteur pour savoir combien d’arguments ont déjà été
calculés pour chacun des opérateurs mémorisés.
Ce raisonnement nous livre la définition de la classe pour les calculatrices
préfixes, CalculatricePrefixe, ainsi que la définition de la méthode
d’initialisation:
Calculatrice subclass: #CalculatricePrefixe
instanceVariableNames: ’memFonc numArgs compteur’
classVariableNames: ’’
poolDictionaries: ’’
category: ’Cours-Smalltalk’
|
|
CalculatricePrefixe>>initialize
super initialize.
memFonc := OrderedCollection new.
numArgs := OrderedCollection new.
compteur := OrderedCollection new.
accu addFirst: 0
|
|
La définition des méthodes activeFonction: et activeFonctionUnaire:
semble très simple également. Principalement, il n’y a qu’à mémoriser
l’opérateur, le nombre d’arguments et le nombre d’arguments encore en
attente. Ce qui nous donne pour la méthode activeFonction::
CalculatricePrefxe>>activeFonction: label
memFonc addFirst: label.
compteur
addFirst: (numArgs addFirst: 2).
self activer: label
|
|
où la méthode activer: est une sous-routine qui se charge, comme chez les
autres types de calculatrices, de la mise à jour de la représentation
graphique de notre calculatrice:
CalculatricePrefixe>>activer: label
bufferEcran := accu first printString.
self changed: #hist with: label , ’ > ’ , bufferEcran.
self changed: #printString.
firstTime := true
|
|
Pour le cas où nous activant une touche d’opérateur après la fin d’un
autre calcul, donc quand un nouveau calcul commence et l’écran
affiche encore le résultat du calcul précédent, nous introduisant
la transmission du message desactiver au début de la méthode
activeFonction:. Son rôle sera limité à reinitialiser l’accumulateur
accu:
CalculatricePrefixe>>desactiver
compteur isEmpty
ifTrue: [accu removeFirst.
accu addFirst: 0]
|
|
La nouvelle version de notre méthode activeFonction: est alors:
CalculatricePrefixe>>activeFonction: label
self desactiver.
memFonc addFirst: label.
compteur
addFirst: (numArgs addFirst: 2).
self activer: label
|
|
La méthode activeFonctionUnaire: est la même chose pour les opérateurs
unaires:
CalculatricePrefixe>>activeFonctionUnaire: label
self desactiver.
memFonc addFirst: label.
numArgs
addFirst: (compteur addFirst: 1).
self activer: label
|
|
Ces deux dernières méthodes sont tellement similaire qu’une factorisation
s’impose. Si nous définissons une méthode auxiliaire avec le nom
activeLesFonctions:aNargs: comme:
CalculatricePrefixe>>activeLesFonctions: label aNargs: n
self desactiver.
memFonc addFirst: label.
numArgs
addFirst: (compteur addFirst: n).
self activer: label
|
|
Nous pouvons simplifier les deux méthodes d’activation d’opérateurs comme
voici:
CalculatricePrefixe>>activeFonctionUnaire: label
self activeLesFonctions: label aNargs: 1
|
|
et
CalculatricePrefixe>>activeFonction: label
self activeLesFonctions: label aNargs: 2
|
|
La méthode qui doit effectuer le plus de travail est alors la méthode
activeEgal:. Elle est activé par la touche =, donc pour l’entrée d’un
nombre. Voici ce qu’elle doit faire:
- Commencer par informer l’écran historique qu’un nombre a été
entré.
- Mettre ce nombre en tête de la pile accu.
- Décrémenter le compteur d’argument courant.
- Regarder si le compteur est égale à zéro.
- Si ce n’est pas le cas, l’opérateur courant a besoin d’arguments
supplémentaires afin de pouvoir calculer. Il suffit alors de mettre la
calculatrice dans l’état d’attente d’entré d’un nouveau opérande:
il faut mettre la variable d’instance firstTime à la valeur true.
- Si le compteur est à zéro, alors il faut calculer le résultat de
la transmission de l’opérateur en attente aux deux arguments
empilés dans la pile accu. Ce résultat doit être empilé dans cette
même pile. L’opérateur doit être dépilé de la pile des opérateurs,
le compteur courant doit être dépilé de la pile de compteurs.
- Si la pile de compteurs est vide, le calcul est terminé. Sinon, le
résultat obtenu joue le rôle d’un opérande d’un opérateur encore
empilé et nous pouvons entrer ce résultat. Bref, le calcul continu
avec une transmission récursive du message activeEgal: à la
calculatrice, ce qui garantit le traitement des opérateurs encore
empilés.
La méthode activeEgal: ci-dessous implémente cet algorithme.
CalculatricePrefixe>>activeEgal: label
self changed: #hist
with: ’Enter > ’ , bufferEcran.
accu addFirst: bufferEcran asNumber.
(compteur addFirst: compteur removeFirst - 1)
= 0
ifTrue: [| x |
compteur removeFirst.
x := accu removeFirst.
numArgs removeFirst = 1
ifTrue:
[accu
addFirst:
(x perform: memFonc removeFirst)]
ifFalse:
[accu
addFirst:
(accu removeFirst
perform: memFonc removeFirst
with: x)].
self activer: label.
compteur isEmpty not
ifTrue:
[bufferEcran := accu removeFirst asString.
self activeEgal: label]]
ifFalse: [firstTime := true]
|
|
| FIG. A.48: | Une
calculatrice préfixe
après calcul de * +
2 3 + 5 5 |
| |
La figure A.48 montre notre calculatrice préfixe après le calcul de
l’expression * + 2 3 + 5 5. Visiblement, l’ajout d’une calculatrice
supplémentaire se réalisait de manière toute naturelle: principalement il n’y
avait qu’à redéfinir les trois méthodes cléfs du calcul: la méthode d’entrée
d’un nombre, activeEgal: et les deux méthodes d’activation de fonctions
arithmétiques, activeFonction: et activeFonctionUnaire:. Acun
changement n’était nécessaire pour les méthodes d’affichage ou l’interface
entre la calculatrice et sa représentation graphique.
C’est cela la force de l’utilisation des dépendances.