Variables locales#

Syntaxe let#

Dans la partie Définition de variables, on a rencontré define, qui définit des variables dites globales car elles sont disponibles partout. Les syntaxes présentées ici, au contraire, posent des variables locales, valables seulement pour une partie du code, de manière temporaire. Voici un programme qui ne fonctionnera pas.

(define compositeur "Mozart")

(if (equal? compositeur "Mozart")
    (begin
      (define naissance 1756)
      (format #t
              "Mozart est né en ~a, son 300e anniversaire sera fêté en ~a."
              naissance
              (+ naissance 300))))

(NB : Du moins, ce programme ne fonctionnera pas avec Guile 2.2. Les règles sur le placement des define sont complexes, ont varié avec les versions de Guile, et ne sont pas encore stabilisées.)

En effet, naissance est définie comme une variable au plus haut niveau, mais son affectation intervient dans une expression, qui n’est évaluée que sous certaines conditions.

C’est pourquoi define n’est utile qu’au plus haut niveau, pour définir des fonctions, ou encore des constantes, comme dans :

(define PI 3.1415926535)

Pour des variables temporaires qui permettent de parvenir à un résultat final sans devoir perdurer après, on recourt à une syntaxe différente, avec le mot-clé let. La syntaxe est la suivante :

(let ((variable1 valeur1)
      (variable2 valeur2)
      (variable3 valeur3)
      ...)
  expression1
  expression2
  ...)

Lors de l’exécution du let, les valeurs sont d’abord évaluées et affectées aux différentes variables. Puis les expressions sont évaluées une par une, et la valeur de la dernière expression devient la valeur du let, comme avec un begin. À la dernière parenthèse fermante, les variables cessent d’exister. On dit que leur portée est limitée au let.

Pour les exemples suivants, on utilisera la fonction random. L’appel (random n) renvoie un entier aléatoire entre \(0\) et \(n - 1\). Bien sûr, vous obtiendrez peut-être des résultats différents, puisqu’ils sont aléatoires.

(define (loto)
   (let ((x (random 2))
         (y (random 2)))
     (display
       (if (and (= x 1)
                (= y 1))
           "Chance !"
           "Perdu."))))

(loto)
 Perdu.

Ici, on lance deux pièces de monnaie (face = 0, pile = 1). Si elle tombent toutes les deux sur pile, l’utilisateur a gagné.

La forme visuelle du let est caractéristique :

(let (xxxxxxxxxxxxxxxxxxxxxx
      xxxxxxxxxxxxxxxxxxxxxx
      xxxxxxxxxxxxxxxxxxxxxx
      xxxxxxxxxxxxxxxxxxxxxx
      xxxxxxxxxxxxxxxxxxxxxx)
  xxxxxxxxxxxxxxxxxx
  xxxxxxxxxxxxxxxxxx
  xxxxxxxxxxxxxxxxxx
  xxxxxxxxxxxxxxxxxx)

Les programmeurs Scheme, sans compter les parenthèses, reconnaissent le bloc de variables et le bloc d’expressions.

Inventons une variante du jeu précédent, où l’on commence par tirer un dé, qui donne le nombre de boules d’une urne, puis on tire une boule de cette urne, sachant qu’il n’y en a qu’une de gagnante. Ainsi, si le dé tombe sur 4, il y aura quatre boules dans l’urne et donc une chance sur quatre de gagner. Il est tentant d’écrire :

(define (loto2)
  (let ((nombre-boules (random 10))
        (boule-tirée (random nombre-boules)))
    (display
      (if (equal? boule-tirée 0)
          "Chance !"
          "Perdu."))))

Cependant, on obtient l’erreur « Unbound variable : nombre-boules ». En effet, let est un peu capricieux. D’abord les valeurs des variables sont toutes évaluées, et ensuite seulement, elles sont affectées aux variables. Donc, l’interpréteur chercher à évaluer (random 10), puis (random nombre-boules), et enfin à affecter ces deux valeurs à nombre-boules et boule-tirée. Bien sûr, on souhaiterait plutôt que la valeur de (random 10) soit d’abord affectée à nombre-boules, pour que l’expression (random nombre-boules) puisse réutiliser la variable nombre-boules. Dans ce cas, il faut remplacer let par une variante, let*, dont la syntaxe est exactement la même. Notre exemple corrigé devient :

(define (loto2)
  (let* ((nombre-boules (+ 1 (random 9)))
         (boule-tirée (random nombre-boules)))
    (display
      (if (equal? boule-tirée 0)
          "Chance !"
          "Perdu."))))

(loto2)
 Perdu.

Contrairement à let, let* commence par évaluer la première valeur et l’affecter à la première variable, puis évalue la deuxième valeur (qui peut donc utiliser la première variable) et la met dans la deuxième variable, etc.

Dans la pratique, on utilise la plupart du temps let*.

Parenthéser un let#

Les expressions let comportent de nombreuses parenthèses et il est facile de s’y perdre. Faisons le tour des erreurs de parenthésage les plus fréquentes, sur l’exemple :

(let ((a 5))
  (+ a 15))
  • Oublier une parenthèse.

    (let ((a 5) ; manque )
      (+ a 15))
    

    Avec cette expression dans un fichier LilyPond (préfixée d’un croisillon # pour passer en mode Scheme), vous obtiendrez une erreur « end of file », ce qui signale que l’expression Scheme ne s’est jamais terminée.

  • Rajouter une parenthèse.

    (let ((a 5))
      (+ a 15))) ; ) en trop
    

    En LilyPond (toujours en rajoutant un croisillon), l’erreur peut paraître surprenante : « syntax error, unexpected EVENT_IDENTIFIER ». En effet, lorsque l’expression complète est terminée, on repasse en syntaxe LilyPond. Arrive alors la parenthèse superflue. Les parenthèses sont, en syntaxe LilyPond, le moyen d’indiquer les liaisons d’articulation, d’où le message signifiant qu’une articulation n’était pas attendue au plus haut niveau.

  • Déplacer une parenthèse.

    (let ((a 5) ; manque )
      (+ a 15))) ; ) en trop
    

    Ici, il n’y a pas de parenthèse en trop ni en moins, donc votre éditeur de texte ne sera pas capable de signaler d’erreur. Néanmoins, une parenthèse est mal placée. Le message d’erreur est encore plus laconique : « bad let ». Pour le comprendre, il faut revenir à la forme du let :

    (let (xxxxxxxxx
          xxxxxxxxx)
      xxxxxxx
      xxxxxxx)
    

    Dans la première paire de parenthèses se trouvent toutes les affectations. Ici, ces parenthèses courent jusqu’à la fin du let. On pourrait reformater le code comme ceci :

    (let ((a 5)
          (+ a 15))
       )
    

    L’interpréteur essaie de voir (+ a 15) sous la forme (nom-de-variable valeur), ce qui échoue car il y a trois éléments dans la parenthèse. De plus, il manque une expression principale dans le let, d’où le « bad let ».

  • Omettre des parenthèses.

    (let (a 5) ; il faudrait ((a 5))
      (+ a 15))
    

    À nouveau, l’interpréteur se plaint d’un « bad let ». Pour comprendre, rappelons-nous que tout ce qui se trouve dans la première paire de parenthèses est pris comme une suite d’affectations ressemblant à (nom-de-variable valeur). En reformatant le let, le problème devient clair :

    (let (
           a
           5
         )
      (+ a 15))
    

    En effet, a et 5 ne correspondent pas à la forme attendue (nom valeur). C’est pourquoi, même avec une seule variable, il faut bien deux paires de parenthèses dans ((a 5)).

Simplification du code avec let*#

let* est un bon outil dans l’optique de rendre le code plus lisible et compréhensible. Pour les besoins de la démonstration, le code suivant est tiré de LilyPond, et adapté pour ne contenir aucun let*.

(apply ly:stencil-add
       (map
         (lambda (stil accessor)
           (ly:stencil-translate-axis
             stil
             (accessor
               (coord-translate
                 (interval-widen
                   (ly:relative-group-extent
                     (apply append
                            (map
                              (lambda (g)
                                (cons g
                                      (apply append
                                             (map
                                               (lambda (sym)
                                                  (cond
                                                    ((ly:grob? (ly:grob-object g sym))
                                                     (list (ly:grob-object g sym)))
                                                    ((ly:grob-array? (ly:grob-object g sym))
                                                     (ly:grob-array->list (ly:grob-object g sym)))
                                                    (else
                                                     '())))
                                               (ly:grob-property g 'parenthesis-friends)))))
                              (ly:grob-array->list
                                (ly:grob-object grob 'elements))))
                     (ly:grob-system grob)
                     X)
                   (ly:grob-property grob 'padding 0.1))
                (- (ly:grob-relative-coordinate grob
                                                (ly:grob-system grob)
                                                X))))
             X))
         (ly:grob-property grob 'stencils)
         (list car cdr)))

Si vous ne comprenez rien à ce code, vous avez tout compris ! Ce qui le rend un peu difficile à lire, c’est l’imbrication sans fin des expressions, à cause de laquelle on a vite fait de perdre le fil, comme dans une phrase de Proust. De plus, l’ordre d’exécution part de l’intérieur des expressions pour aller progressivement vers l’extérieur, alors que l’on a l’habitude que le code s’exécute linéairement. Voici à présent la version avec des let et let* :

(let* ((elts (ly:grob-array->list (ly:grob-object grob 'elements)))
       (get-friends
         (lambda (g)
           (let* ((syms (ly:grob-property g 'parenthesis-friends))
                  (get-friends-for-symbol
                    (lambda (sym)
                      (let ((friends (ly:grob-object g sym)))
                        (cond
                          ((ly:grob? friends)
                           (list friends))
                          ((ly:grob-array? friends)
                           (ly:grob-array->list friends))
                          (else
                           '())))))
                  (friend-lists (map get-friends-for-symbol syms))
                  (friends (apply append friend-lists)))
             (cons g friends))))
       (all-friend-lists (map get-friends elts))
       (all-friends (apply append all-friend-lists))
       (all-friends-array (ly:grob-list->grob-array all-friends))
       (X-refp (ly:grob-common-refpoint-of-array grob all-friends-array X))
       (my-X (ly:grob-relative-coordinate grob X-refp X))
       (X-ext (ly:relative-group-extent all-friends-array X-refp X))
       (padding (ly:grob-property grob 'padding 0.1))
       (wide-X-ext (interval-widen X-ext padding))
       (parenthesis-positions (coord-translate wide-X-ext (- my-X)))
       (stencils (ly:grob-property grob 'stencils))
       (left-paren (first stencils))
       (right-paren (second stencils))
       (translated-left-paren
         (ly:stencil-translate-axis left-paren (interval-start parenthesis-positions) X))
       (translated-right-paren
         (ly:stencil-translate-axis right-paren (interval-end parenthesis-positions) X)))
  (ly:stencil-add translated-left-paren translated-right-paren))

Sans rien connaître du fonctionnement interne de LilyPond, on peut déjà comprendre des choses : on prend tous les grobs qu’entoure une paire de parenthèses éditoriales (elts), on étend la liste pour qu’elle comprenne leurs « amis » (all-friends), on prend un point de référence (X-refp), on calcule une coordonnée sur l’axe horizontal (my-X), etc.

Ce conseil est particulièrement utile dans les premiers temps : écrire les fonctions compliquées comme un grand let*, dans lequel on pose les variables pas à pas, pour que l’expression finale soit simple et courte.