Expressions

Le code Scheme ne se construit pas autour d'instructions mais autour d'expressions. Autrement dit, au lieu de donner des ordres à l’ordinateur (fais ceci, puis cela), on explique comment construire le résultat en combinant des opérations. Voyons comment faire.

Premières opérations

Revenons sur un exemple déjà rencontré :

(+ 42 5)

Cette syntaxe, fondement absolu de Scheme, est celle d’un appel de fonction, ou, pour employer le terme plus commun en Scheme, d’un appel de procédure. Ce qui en LilyPond s’écrit :

\fonction argument1 argument2 ...

se note en Scheme :

(fonction argument1 argument2 ...)

Attention, les parenthèses ne sont pas facultatives !

guile> (+ 2 2)
4
guile> + 2 2
#<primitive-generic +>
guile> 2
guile> 2

En effet, + 2 2 sans parenthèses est simplement une suite d’expressions, sans aucun lien les unes avec les autres. Le bac à sable les évalue donc à la file et affiche leurs valeurs, comme si l’on avait fait :

guile> +
#<primitive-generic +>
guile> 2
2
guile> 2
2

Les parenthèses, en plus d’opérer un regroupement, indiquent que le premier élément est une procédure (une fonction) qu’il faut appliquer aux autres. A contrario, il ne faut pas mettre trop de parenthèses.

guile> ((+ 2 2))
ERROR: Wrong type to apply: 4
ABORT: (misc-error)

Que se passe-t-il ? D’abord, l’expression à l’intérieur des parenthèses est évaluée. La valeur de (+ 2 2) est 4. L’expression devient donc :

(4)

ce qu’il faut voir comme

(4
     ; Il pourrait y avoir d'autres choses ici.
     )

Ainsi, (4) est l’appel de la fonction 4 sans arguments. Mais 4 n’est pas une fonction ! D’où l’erreur « Wrong type to apply » : 4 n’est pas un objet que l’on appelle impunément comme si c’était une fonction.

On réalise de même les autres opérations élémentaires :

(+ 2 2) ; deux plus deux
(- 5 3) ; cinq moins trois
(* 5 6) ; cinq fois six
(/ 4 2) ; quatre divisé par deux

Cette notation a de quoi surprendre notre œil familier des écritures habituelles \(2 + 2\), \(3 - 5\), \(5 \times 6\) et \(4 / 2\). Il faut s’habituer à reformuler notre pensée dans cet ordre à la Yoda. Plutôt que « deux plus deux », ou « deux additionné à deux », on dit en Scheme : « l’addition de deux et deux ». De même, « la division de quatre par deux », etc.

Les quatres fonctions présentées ici peuvent d’ailleurs prendre plus que deux arguments :

(+ 3 4 5)

Dans d’autres langages, il aurait fallu répéter l’opérateur + dans l’expression, en écrivant 3 + 4 + 5. En Scheme, ce n’est pas nécessaire, car les parenthèses se chargent de délimiter les arguments. (+ 3 4 5) est simplement l’addition des nombres 3, 4 et 5.

Cette syntaxe est si omniprésente que nous l’avons déjà rencontrée :

(display "une chaîne de caractères")
(skip-of-length #{ { c'8 d' e' f' e' d' } #})

display et skip-of-length sont également des procédures, que l’on appelle sur des arguments de la même manière.

Attardons-nous enfin sur :

guile> +
#<primitive-generic +>

Les procédures en Scheme sont des objets comme les autres. Cette représentation entre crochets est donnée par l’interpréteur faute de mieux, puisque l’on imagine mal comment écrire la « valeur » de la procédure. Cela n’empêche pas cette procédure d’exister en propre, de même que 4 est un entier, 42.5 un nombre à virgule et "abcd" une chaîne de caractères. Mais quelle est donc la nature de cet objet « procédure » ? En réalité, la question en programmation n’est pas tant de savoir ce qu’un objet est que ce qu’il fait. Pensons aux nombres. On peut les afficher, les additionner, les soustraire, les multiplier, etc. Les procédures, elles, n’ont qu’une seule opération à offrir, l’appel. Une procédure est tout simplement un objet que l’on peut appeler, ce que l’on réalise avec la syntaxe (procédure argument1 argument2 ...).

Aussi, comme toutes les valeurs, les procédures peuvent être stockées dans des variables. + n’est rien qu’une variable prédéfinie qui contient la procédure standard d’addition. On pourrait faire :

(define addition +)
(addition 2 2)

Imbrication des expressions

Armés de ces connaissances, attaquons-nous à une expressions légèrement plus complexe.

\[(2 \times 3) + (3 \times 5)\]

Pour traduire cette expression en Scheme, il faut commencer par se demander ce qu’elle est, en premier le type d’opération. C’est ici une addition.

(+ ... ...)

Ensuite, on complète : c’est l’addition « de quoi » ? L’addition de \(2 \times 3\) et \(3 \times 5\). On continue de la même manière. \(2 \times 3\) est la multiplication de 2 par 3, et \(3 \times 5\) est la multiplication de 3 par 5. On en déduit l’écriture du calcul :

(+ (* 2 3) (* 3 5))

On a imbriqué des expressions dans des expressions. Essayez de calculer à l’aide du bac à sable la valeur de

\[1 - (5 \times 6) - (3 \times (4 + (3 \times 12)))\]

Formatage du code

Ainsi, programmer en Scheme consiste essentiellement à imbriquer des expressions dans des expressions plus grandes. Si vous avez réussi l’exercice précédent, vous aurez remarqué qu’il faut quatre paires de parenthèses. Dans des programmes plus complexes, les choses ne vont pas en s’arrangeant et il n’est pas rare de fermer une dizaine de parenthèses d’un coup. Si le nom de Lisp, le père de Scheme, signifie List Processing, les mauvaises langues prétendent qu’il est l’acronyme de « Lots of Insipid and Stupid Parentheses » (Nombreuses Parenthèses Insipides et Idiotes). Les débutants peuvent avoir une image mentale de Scheme qui ressemble à :

          ()(((((()))))((                    )((())((()(    )())         )(()   ())()((()(((((    ())(     ()()  ())()(()(())((
      )))((())((())))(                  (()())((()))        ))))         )(()   ))()()(()(()(     ()((() ))(((   ((()()(())(()
    ))()))(()))()                   (())((())()(            ))()         ())(   (())              ))   )((       (())
  )))))((((()((              ))()()()))()))                 )(()         )(()   ()((              )((   (  ()(   (())
  )()(((()))(()(             )()(()()()()()(                ((()         ()()   )())              )))   )   ()(  ()))
      )()(()()((())(         (()()(((()))))                 ()))())))(((()()(   ))))())((         ))(       (((  ((((()())
              ))(((())(()((  (())((()))(()))                )(((((()(((()())(   )())              )()       )))  )))(
                )()))()()()  ))()))))((()))                 ()()         (())   ()()              ()(       )((  (())
             )(((())(())         )))()()((()(()(            )))(         ))((   ))((              ())       )()  ()((
          ))((((()()))               )(()(()())))           ()((         ()((   )()(              (((       ))(  (()(
       ()()))((()()()                   ((()())))))())))    ()()          ))()  ()()()(())()      (((       )()  ())()(()())(
((())))(((())(()                            )(()()((())(((  ()()           )()( )))))())((()((   )()        ()(  ())(()(())))))

Pour garder la tête froide, le meilleur moyen est de respecter certaines règles simples de formatage du code qui le rendent lisible. Il existe trois formes possibles pour une expression. La première consiste à placer le nom de la procédure et tous ses arguments sur une seule ligne.

(procédure argument1 argument2 ...)

Dans la deuxième, les arguments sont placés chacun sur une nouvelle ligne. Ils sont indentés par rapport à la procédure.

(procédure
  argument1
  argument2
  ...)

Enfin, on peut également placer le premier argument sur la même ligne que la procédure, et les arguments suivants sur des lignes séparées, au même niveau d’indentation que le premier.

(procédure argument1
           argument2
           ...)

Dans tous les cas, on retiendra que les arguments sont indentés au même niveau, et que l’on ne place jamais de parenthèses seules sur une ligne ! En effet, le lecteur humain comprend le code à sa forme, sans avoir besoin de compter les parenthèses. Voici une expression qui mélange les trois formes :

(+ (* 3 456 123)
   (/ 6 (- 10 7))
   (-
     (* 30 4012 (+ 123 5642))
     20
     50003))

Un programmeur Scheme voyant ce code remarque immédiatement que trois arguments sont passés à +, et voit où ils commencent et finissent.

Enfin, on peut retenir une règle supplémentaire, moins observée mais néamoins utile : « closing paren closes the line ». Dès que l’on écrit une parenthèse fermante, il vaut mieux revenir à la ligne. Par exemple, plutôt que :

(+ (* 3 4) 5)

on écrira

(+ (* 3 4)
   5)

ou bien

(+
  (* 3 4)
  5)

ou encore, en échangeant les arguments pour mettre le plus complexe à la fin :

(+ 5 (* 3 4))

Suites d’expressions

Dans un fichier LilyPond, le croisillon introduit une unique expression. S’il en faut plusieurs, par exemple pour définir des variables ou afficher des messages de déboguage, on écrit un croisillon devant chacune.

#(display "Bonjour ")
#(define prénom "Josiane")
#(display prénom)

Une autre possibilité est d’enclore toutes les expressions dans un (begin ...).

#(begin
   (display "Bonjour ")
   (define prénom "Josiane")
   (display prénom))

Dans un begin peuvent apparaître un nombre quelconque d’expressions. Elles sont toutes évaluées, et la valeur de la dernière devient la valeur du begin entier.

guile> (begin
         (display "Bonjour ")
         (define prénom "Josiane")
         (display prénom)
         (display " !\n")
         prénom)
Bonjour Josiane !
"Josiane"