Quoting, symboles#

Comprendre le quoting#

Le quoting est un concept qui déroute presque systématiquement les débutants dans les langages de la famille Lisp. Ce chapitre un peu plus théorique que d’habitude vise à démystifier cet outil, essentiel en pratique.

Dans le chapitre sur les listes, nous avons déjà rencontré la syntaxe du quoting, qui se présente sous cette forme  :

'(0 1 2 3 "Bonjour")

Cette expression est (presque, voir ci-dessous) équivalente à (list 0 1 2 3 "Bonjour"). Elle s’évalue à une liste dont les éléments sont 0, 1, 2, 3 et "Bonjour".

Cependant, le quoting possède une bizarrerie que n’a pas la fonction list.

(define quatre 4)

(list quatre (+ 2 3))
 (4 5)

'(quatre (+ 2 3))
 (quatre (+ 2 3))```

Que se passe-t-il  ?

Pour comprendre, il faut se pencher sur des aspects fondamentaux du langage Scheme qui jusqu’ici sont restés plus ou moins cachés.

Gardez à l’esprit la ligne directrice de la conception du langage Scheme : la simplicité maximale. L’un des domaines dans lesquels cette simplicité est appliquée est la manière de représenter les programmes. Pour le comprendre, il est utile de comparer sur ce point Scheme à un autre langage plus classique, par exemple Python. En Python, les programmes sont représentés par des « arbres syntaxiques », qui peuvent être lus à l’aide du module ast. Sur l’exemple 1+2, cela donne :

>>> import ast
>>> ast.dump(ast.parse("1+2", mode="eval"))
'Expression(body=BinOp(left=Constant(value=1), op=Add(), right=Constant(value=2)))'

L’expression 1+2 a donc pour l’interpréteur Python une représentation interne relativement complexe, qui est un objet de type Expression dont l’attribut body est un objet de type BinOp avec des attributs left, op et right.

Rien de tout ceci n’existe en Scheme. La représentation des expressions est beaucoup plus simple. Au lieu de faire appel à des types de données spéciaux (avec des classes, attributs, etc.), elle réutilise simplement les types de données de base. Nous avons déjà rencontré certains de ces types de données : les nombres, les chaînes de caractères, les booléens, les listes. Observez comme l’affichage des listes est très similaire à la manière dont les expressions Scheme sont écrites. Dans les deux cas, on trouve des éléments séparés par des espaces à l’intérieur d’une paire de parenthèses.

(+ 1 2 3 4) ; opération
(1 2 3 4 5) ; liste

Cette ressemblance n’a rien d’une coïncidence. Si l’expression (+ 1 2) a l’apparence d’une liste, c’est que c’est en réalité une liste, dont les éléments sont +, 1 et 2. C’est la simplification apportée par Scheme, là où Python utilise les arbres syntaxiques. En syntaxe Python, cette liste serait approximativement équivalente à ["+", 1, 2].

Avant de continuer, mentionnons une source fréquente de confusion. Il faut distinguer entre une expression et la valeur à laquelle elle s’évalue. Nous venons d’apprendre que (+ 1 2) est une liste, mais n’est-ce pas le nombre 3 ? Non : c’est une expression qui s’évalue au nombre 3 si elle est exécutée.

Voici un point de vue possible sur ce concept d’utiliser des types simples pour représenter les programmes. Les choses se passent comme si les créateurs de Scheme avaient été trop paresseux pour élaborer la syntaxe de leur langage. Ils ont seulement choisi une syntaxe pour écrire les valeurs des types usuels (nombres, booléens, chaînes, listes, entre autres), et ont décrété que les programmes seraient écrits comme des structures de données dans cette syntaxe. C’est comme si tous les programmes Python étaient écrits dans une syntaxe ressemblant à

["def", "f", ["x"],
  ["return", ["+", "x", 1]]]

au lieu de

def f(x):
    return x+1

Lorsqu’un fichier de code Scheme est exécuté, il est d’abord lu comme structure de données par l’analyseur syntaxique de Scheme ; puis cette structure de données est évaluée en tant que programme. Passons en revue les différents types de données et la manière dont ils s’évaluent :

  • Les nombres, comme -5.5. Ils sont qualifiés d‘« auto-évaluateurs » car évaluer un nombre renvoie ce même nombre. Lorsque vous écrivez -5.5, l’interpréteur répond -5.5. Même si cela paraît évident, cela cache un principe. Cette situation est différente des listes : lorsque vous écrivez (+ 1 2), l’interpréteur répond 3 et non pas (+ 1 2), ce qui s’explique par le fait que les listes, elles, ne sont pas auto-évaluatrices.

  • Les chaînes de caractères, comme "abcd", également auto-évaluatrices.

  • Les booléens #t et #f, eux-aussi auto-évaluateurs.

  • Les listes comme (display "abc"). La plupart du temps, une liste est évaluée en évaluant d’abord tous ses éléments, puis en appliquant la valeur du premier élément évalué, en tant que fonction, aux valeurs des éléments suivants.

Il reste un type de données tout à fait fondamental à comprendre. Vous l’avez vu en action constamment au cours de ce tutoriel. C’est le type de display dans (display "abc"). Cet objet est appelé un symbole. En première approximation, les symboles ressemblent aux chaînes de caractères : ce sont des suites de caractères (ici, d, i, s, p, l, a, y). Ils sont différenciés des chaînes dans la syntaxe, car ils s’écrivent sans guillemets doubles (ils s’arrêtent simplement au premier caractère d’espace). Hormis leur syntaxe différente, ils sont fondamentalement différents de par la manière dont ils s’évaluent. En effet, contrairement aux chaînes, les symboles ne sont pas auto-évaluateurs. Le fait d’évaluer un symbole lit la variable qui porte le nom de ce symbole et la renvoie.

"ma-variable" ; chaîne de caractères, évaluée en elle-même
 "ma-variable"

ma-variable ; symbole
 erreur car variable non définie

(define ma-variable "toto") ; définition de la variable
ma-variable ; maintenant le symbole peut s'évaluer sans erreur
 "toto"

De même, dans l’expression (+ 1 2), le + est un symbole. L’interpréteur Scheme contient une fonction d’addition prédéfinie, qui se trouve dans une variable qui a pour nom +.

À ce stade, vous avez compris que lorsque vous écrivez un programme Scheme, vous écrivez en réalité une grande structure de données qui est la représentation du programme par des types de données simples, dans la notation de Scheme des données simples. Dès lors, on rencontre logiquement un problème pour écrire une structure de données comme une liste directement dans le programme, pour l’utiliser en tant que telle, mais pas pour l’évaluer en tant que programme. C’est la fonction du « quoting ». Cet opérateur empêche l’évaluation d’une expression et la renvoie telle quelle, en tant que structure de données.

(+ 1 2) ; la liste s'évalue comme un appel de fonction
 3

'(+ 1 2) ; la liste ne s'évalue pas, on l'obtient telle quelle
 (+ 1 2)

En pratique, le quoting est un moyen très pratique d’entrer les listes. De plus, il constitue le moyen principal d’obtenir un symbole sans l’évaluer. Cette syntaxe est d’usage tellement courant qu’elle vous est probablement déjà familière si vous utilisez LilyPond régulièrement  :

\tag #'edition (
\override NoteHead.style = #'cross

Sachez que l’apostrophe est un raccourci syntaxique pour quote. On peut donc écrire indifféremment

'(1 2.4 "Bonjour")

ou

(quote (1 2.4 "Bonjour"))

Quasiquotes#

La syntaxe des quasiquotes permet d’évaluer des expressions tout de même au milieu d’une quote. Pour cela, il faut remplacer quote par quasiquote, et marquer les expressions à évaluer avec unquote.

(quasiquote (0 1 2 (unquote (+ 1 2)) "Hello"))
 (0 1 2 3 "Hello")

L’équivalent avec la syntaxe pratique est de remplacer l’apostrophe par un accent grave (backtick, le caractère `) et de marquer les expressions évaluées avec une virgule.

`(0 1 2 ,(+ 1 2) "Bonjour")
 (0 1 2 3 "Bonjour")

Cette syntaxe est courante pour créer des listes contenant des symboles. Ainsi, plutôt que

(list 'moveto x y)

on écrira souvent

`(moveto ,x ,y)

Identité des symboles#

À première vue, les symboles sont similaires aux chaînes de caractères, avec pour différence qu’une chaîne est auto-évaluatrice alors que l’évaluation d’un symbole donne la valeur d’une variable. Néanmoins, il existe une seconde différence essentielle entre les deux. Lorsque l’on crée deux chaînes de caractères, même égales, de la mémoire est allouée pour chacune et elles vivent indépendamment l’une de l’autre. Au contraire, les symboles sont uniques. Un même symbole n’est jamais stocké deux fois. Lorsqu’apparaît un symbole déjà connu, il est réutilisé automatiquement, avec la même

Pour le test equal?, il n’y a aucune différence, car equal? teste l’égalité structurelle d’objets. Pour des chaînes de caractères, equal? regardera si elles ont exactement les mêmes caractères. C’est un autre test, nommé eq?, qui détermine si deux objets ont la même adresse en mémoire, autrement dit, si deux objets sont en fait le même objet. Alors que equal? est un test d’égalité, eq? est un test d’identité.

(equal? "bonjour" "bonjour")
 #t
(eq? "bonjour" "bonjour")
 #f
(equal? 'bonjour 'bonjour)
 #t
(eq? 'bonjour 'bonjour)
 #t

L’intérêt de eq? est d’être très rapide. Il suffit de constater si oui ou non les deux objets ont la même adresse. Au contraire, un test equal? peut être coûteux en temps, et sera d’autant plus coûteux que les objets sont grands, comme de longues chaînes de caractères. Aussi, on retient que les symboles peuvent être comparés avec eq?, et qu’il est avantageux de les comparer toujours avec eq?, même si ce n’est pas grave d’utiliser equal?.

Identité et immutabilité des symboles#

Il existe une règle à connaître concernant le quoting. En avoir connaissance pourra vous éviter des problèmes difficiles à comprendre plus tard. Voici le commandement :

Tu ne muteras pas de structure de données littérale.

Cette règle implique des concepts que nous n’avons pas encore abordés. D’abord, que signifie « muter » ? En termes simples, c’est le fait de modifier une valeur existante. Certaines fonctions Scheme le font, même si ce tutoriel ne les aborde qu’à peine. Un exemple est la fonction set-car!, qui change le premier élément d’une liste.

(define liste (list 1 2))
(set-car! liste 0)
liste  (0 2)

Autre concept nouveau : qu’est-ce qu’une structure de données « littérale » ? Ce terme désigne une structure de données qui est écrite directement dans le programme. L’exemple le plus évident est une liste obtenue par « quoting ». La valeur renvoyée par le code '(0 1 2) est une structure de données littérale car elle se lit dans le code (le (0 1 2) juste après le caractère '). On peut mentionner aussi les chaînes de caractères littérales : "abc" s’évalue en une chaîne littérale car cette chaîne provient directement du code, contrairement à celle que renvoie (string-append "a" "bc"). Bien sûr, tous les nombres, les booléens et les symboles qui sont écrits dans le code sont aussi des données littérales. Simplement, la règle n’a pas d’importance pour ces types de données, car ils sont « immuables » : il n’y a pas de risque de les modifier, car le langage Scheme ne dispose tout simplement d’aucune fonction pour les modifier.

Pour revenir à l’analogie avec Python, l’expression Python [1, 2, 3] est plus proche en Scheme de (list 1 2 3) que de '(1 2 3). En effet, la première de ces expressions crée toujours une liste nouvelle. Au contraire, la seconde, qui est littérale, renvoie toujours la même liste, au sens du prédicat eq? vu plus haut.

(define (construire-liste-avec-fonction-list)
  (list 1 2 3))

(eq? (construire-liste-avec-fonction-list) (construire-liste-avec-fonction-list))
 #f

(define (construire-liste-avec-quoting)
  '(1 2 3))

(eq?  (construire-liste-avec-quoting)  (construire-liste-avec-quoting))
 #t

Attention, ce n’est pas le même principe que pour les symboles. Lorsque vous construisez deux symboles avec le même nom à deux endroits différents, vous obtenez le même symbole, ce qui n’est pas le cas des listes ou des chaînes de caractères. Par contre, lorsque vous lisez une liste littérale deux fois depuis le même endroit, les deux listes sont identiques.

(Si vous programmez en C ou C++, pensez à une chaîne de caractères littérale en Scheme comme à à une chaîne littérale en C, un pointeur const char * vers une adresse dans le segment de données du programme. De même, par analogie, une liste littérale est un tableau static const.)

Lorsque vous écrivez

(fonction "chaîne de caractères")```

vous ne vous attendez pas à ce que le code se mette à passer "chaîne de caractères modifiée" à la fonction, ce qui pourrait pourtant arriver si la fonction mutait la chaîne littérale. Pour éviter complètement ce cas, Scheme impose la règle que les données littérales ne soient jamais mutées.

Si vous appelez des fonctions telles que set-car! sur des données littérales, une erreur peut se produire. Malheureusement, il est également possible qu’aucune erreur ne se produise, ceci en raison de limitations de Guile. Le faire n’en reste pas moins interdit.