A.9 exercices de la section 6.3, page 523

  1. L’initialisation des interfaces graphiques est toujours un peu penible: il faut définir la taille, le placement et l’interface avec le reste du programme pour chacun des objets graphiques. C’est toujours une longue énumération de chacun des éléments en faisant partie. Néanmoins, nous pouvons y reconnaître des patterns, des structures qui se répètent. Ainsi, l’expression:
    PluggableButtonMorph  
         on: (Touche new  
                   label: xxx  
                   fonction: yyy  
                   on: self)  
          getState: #etat  
          action: #activer.
    se trouve cinq fois dans la version actuelle de notre méthode openInWorld. Une première modification sera alors de remplacer toutes les occurrences de cette expression par:
    self newTouche: xxx avec: yyy
    après avoir ajouté la méthode newTouche:avec: que voici:
    Calculatrice>>newTouche: etiquette avec: fonction  
       ^PluggableButtonMorph on:  
          (Touche new  
             label: etiquette asString  
             fonction: fonction  
             on: self)  
          getState: #etat  
          action: #activer
    Maintenant, nous avons cinq occurences de la structure:
    view := self newTouche: xxx avec: yyy.  
    view := self initialise: view with: couleur and: xxx.
    Clairement, nous pouvons combiner ces deux expression en une seule comme voici:
    view := (self newTouche: xxx avec: yyy)  
                          with: couleur and: xxx.
    Ceci nécessite de remplacer la méthode initialise:with:and: de la classe Calculatrice par une méthode with:and:, mais cette fois définie dans la classe PluggableButtonMorph, puisque c’est bien une instance de cette classe qui était la valeur de view dans la méthode initialise:with:and: et c’est bien view qui est le receveur de toute la cascade de messages dans cette méthode. Pour nous en assurer, voici son implémentation:
    Calculatrice>>initalise: view with: couleur and: co  
       ^ view offColor: couleur;  
           borderWidth: 1;  
           borderColor: Color black;  
           hResizing: #spaceFill;  
           vResizing: #spaceFill;  
           label: co asString;  
           useRoundedCorners;  
           yourself
    Créons alors un protocol calculatrice dans la classe PluggableButtonMorph et définissons y la méthode with:and: suivante:
    PluggableButtonMorph>>with: couleur and: co  
       ^ self offColor: couleur;  
           borderWidth: 1;  
           borderColor: Color black;  
           vResizing: #spaceFill;  
           hResizing: #spaceFill;  
           useRoundedCorners;  
           label: co asString;  
           yourself
    C’est une copie exacte de notre méthode initialise:with:and:.

    Pour la réduction de la taille de la méthode openInWorld, remplaçons également les trois créations succéssifves des boutons C, 0 et = par:

       self  
          creeTouches: #(’C’ ’0’ ’=’ )  
          with: #(#activeClear: #activeNumber: #activeEgal:)  
          and: window.
    Ce qui nécessite la création de la méthode creeTouches:with:and: que voici:
    Calculatrice>>creeTouches: tab1 with: tab2 and: window  
       | view a1 |  
       a1 := AlignmentMorph newRow.  
        tab1 with: tab2  
          do: [:ro :fonc |  
                   view := (self  
                            newTouche: ro  
                            avec: fonc)  
                               with: window color  
                               and: ro.  
                   a1 addMorphBack: view].  
             window  
                addMorph: a1  
                frame: (0 @  (4 / 5) extent: 3 / 5 @ (1 / 5)).
    La méthode creeTouches:with:and:function:frame:extent: permettra de simplifier l’initialisation des neufs touches numériques et des quatres touches opérateurs:
    Calculatrice>>creeTouches: tab1 with: tab2 and: window  
                function: function frame: fra extent: ext  
       | l c view |  
        c := l := 0.  
        tab1 with: tab2  
               do: [:ro :morph |  
                     l := l + 1.  
                     ro  
                        do: [:co |  
                           c := c + 1.  
                           view := (self  
                                    newTouche: co asString  
                                    avec: function)  
                                       with: window color  
                                       and: co.  
                           morph addMorph: view].  
             window  
                addMorph: morph  
                frame: (fra x @ (l * (fra y)) extent: ext)].
    Avec ces changements, notre méthodes openInWorld s’est réduite de 108 ligne à 43 lignes et aucune des méthodes auxiliaires supplémentaires ne dépasse la vingtaine de lignes. Voici alors la nouvelle version de openInWorld:
    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: 3 / 5 @ (1 / 5).
       self  
          creeTouches: #(’C’ ’0’ ’=’ )  
          with: #(#activeClear: #activeNumber: #activeEgal:)  
          and: window.  
       self  
          creeTouches: #(#(’+’ ’-’ ’*’ ’/’) )  
          with: {AlignmentMorph newColumn}  
          and: window  
          function: #activeFonction:  
          frame: 3 / 5 @ (1 / 5)  
          extent: 1 / 5 @ (4 / 5).  
       window openInWorld
  2. 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:
    1. 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.
    2. 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.
    3. 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.
    4. 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.
    5. 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

    PIC
    >
    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.


    PIC
    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. astkaccu 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é.


    PIC
    FIG. A.47: Un inspecteur sur la calculatrice infixe après calcul de 30 (3 ×

    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!

  3. 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
    1. À l’activation de la touche × nous ne pouvons rien calculer: nous avons encore besoin des deux opérandes.
    2. La même remarque est valide pour l’activation de la touche +.
    3. L’entrée du nombre 2 livre le premier opérande pour l’opération +. Nous avons besoin d’un opérande supplémentaire.
    4. 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.
    5. À l’activation de la touche + nous attendons encore deux opérandes.
    6. L’entrée du nombre 5 en livre le premier, et
    7. 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:
    1. Commencer par informer l’écran historique qu’un nombre a été entré.
    2. Mettre ce nombre en tête de la pile accu.
    3. Décrémenter le compteur d’argument courant.
    4. Regarder si le compteur est égale à zéro.
    5. 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.
    6. 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.
    7. 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]

    PIC
    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.