«I never understood why LISP was a good idea until I started playing with python.»

Il est étrange de voir à quel point on imagine toujours que le changement est un phénomène que l'on trouve plus derrière soit que devant.

Regardez un instant dans le passé, au delà des brumes de votre plasticité cérébrale, et vous constaterez à quel point vous avez évolué entre «maintenant» et il y a 5 ans. Maintenant, demandez vous si vous changerez autant dans les 5 prochaines ?

Votre cerveau vous dit que non, et il a bien tort.

Pourquoi vous parler de ça ? Parce que j'ai remarqué rétrospectivement un changement assez profond dans ma conception des langages informatiques; et il n'y a à ça qu'un coupable : LISP.

Alors, oui, j'en ai parlé il y a un moment, (en le comparant à Ruby) mais c'est sans scrupules que je le fait a nouveau.

Une ruse généralement valable pour analyser les phénomènes qui nous entourent consiste à changer de perspective à leur endroit.

Jesse Schell a écrit un livre qui est une référence en matière de ludopoésie :

The Art of Game Design: A Book of Lenses

Si j'en recommande la lecture a tous ceux qui voudraient un jour ou l'autre créer des jeux ou les analyser, je trouve que le plus profond du livre reste sa structure : l'auteur donne une suite de perspectives (qu'il nomme « lenses ») au travers desquelles le ludopoète doit contempler sa création et ainsi en découvrir les failles et les beautés.

Quel rapport avec ma vie de programmeur (surtout que ce livre, je l'ai lu il y a des années) ? Eh bien, cette procédure est applicable a bien des problèmes, sa difficulté étant justement de concevoir cette autre perspective.

Cheminement d'un programmeur ordinaire

En informatique comme dans toutes les cultures créatives, on construit par dessus les acquis et les choix de ceux qui nous ont précédé.

La difficulté de l'informatique, c'est qu'elle n'est pas une petite discipline; elle est une nouvelle façon d'utiliser le langage.

Mettre le pied dans l'informatique et découvrir la programmation reviendrait à ce que vivrait un extra-terrestre débarquant sur terre et découvrant le concept de langue.

Il y en a plein. Certaines collant à l'écrit (l'allemand, l'italien…) d'autres non (le français, l'anglais…), d'autres encore décomposant les mots en symboles plutôt qu'en syllabes (Le mandarin, le Japonais) tandis que d'autres encore ne s'écrivent pas du tout.

On pourrait mentionner également qu'il existe des langues dont les syllabes sont basées sur des variations de timbre «ahhh» «ohhh» et d'autres sur des variations de hauteur (le mandarin toujours, mais pas que).

Ajoutons à cela l'importance de l'histoire, les consanguinités et les croisements, les académies et les diglossies…

Tout un programme (passionnant).

L'informatique est un monde plus jeune mais aussi vaste, dont la toile de fond est mathématique et la culture tissée par délà les frontières.

La question que posera notre extra-terrestre passionné pourrait être la suivante : « Ça a l'air cool, mais par quel bout je le prend ? ». Bienheureux celui qui pourra lui donner une réponse absolue («Le bon, c'est à dire le mien ! »).

C'est la même question que pose l'aspirant programmeur à son enseignant.

En réalité, il est de nombreuses façon de voir le code, la majorité des langages s'attardent sur la façon d'exprimer des calculs compliqués/fiables simplement. Ça n'est pas le point qui m'intéresse ici (même s'il est essentiel).

Je suis électronicien de formation, et j'ai appris l'informatique à travers les chemins de données, les compteurs ordinaux, les unités arithmétiques et logiques, les registres a décalage et les rouages presque mécaniques d'une machine physique chargée d'exécuter du code.

De cette perspective, le code est une liste de nombres; une succession de bits , une partie de ce nombre donne la nature de l'opération (une addition, une multiplication, un déplacement mémoire…) l'autre sur qui il faut l'appliquée.

C'est ce qu'on nomme le langage machine, c'est le seul que comprend l'appareil avec lequel vous lisez ces lignes.

si on les écrit (en binaire), ça donne des trucs sympa du genre :

000000 00001 00010 00110 00000 100000
100011 00011 01000  0000000001000100
000010   00000000000000010000000001

les espaces sont là pour la «lisibilité» mais ne sont évidement pas représentés en mémoire.

Mettez un trou pour 0 et un plein pour 1, et vous avez compris le principe des cartes perforées et peut-être une idée de «pourquoi on s'y est vite pris autrement».

On a promptement construit dessus l'assembleur, où on remplace le code instruction (l'OP code) par un mnémotique genre CPY, MOV etc.

Notre code est toujours une liste:

movl $4, %eax
movl $1, %ebx
movl $str, %ecx
movl $8, %edx
int $0x80

mais c'est une liste alphanumérique, et l'être humain en tire plus d'infos.

Cependant, si un processeur monocœur exécute les instructions comme une liste, à plat, l'être humain a du mal à concevoir ainsi de gros programmes, il s'est donc mis à représenter ses programmes par des arbres, représentés dans une liste de lignes de texte.

si on prend du python, par exemple:

while (True):
  print ("do you love me?")
  if love == True:
    print ("yep!")
  else:
    print ("nop!")

Si on analyse le code, le nœud «while» comprend une branche conditionnelle (fixée ici a True, on peut donc l'ignorer) une «print» ainsi qu'une branche «if». Le sous nœud if comprend trois branches, la première étant la condition (est-ce que love vaut True ?) la seconde la branche qui est exécutée si oui, et la troisième celle qui est exécutée sinon.

Le processeur n'aura jamais «conscience» de cet arbre qui sera représenté par des sauts à différentes positions de la liste d'instructions (un «if» est souvent en assembleur «si l'opérande est égale à 0, je saute l'instruction suivante» l'instruction suivante en question étant elle-même généralement un renvoi vers un autre groupe d'instructions).

Cependant il vous apparaît assez évident qu'une telle structure colle mieux au cerveau que ne l'eût fait la liste. Mais, vous pouvez le voir, la structure représentée par l'indentation (l'alinéa variable à chaque ligne) n'est pas exactement celle que j'évoque plus haut.

En effet, visuellement, «if» est au même niveau que la condition, tout comme «else», alors que, d'après ma description de l'arbre de tantôt, else devrait être une branche du nœud «if».

Et c'est là qu'on comprend une chose qui devrait faire frissonner même les plus courageux logiciens : la syntaxe nous ment.

Ça n'est pas arrivé par hasard, c'est un choix réfléchi des créateurs du langage, qui considèrent qu'«if» est une exception et qu'il est bien mieux de faire de la condition une entitée séparée des instructions quelle discrimine. Le else étant encore un autre cas (un genre de nœud dépendant de l'existence d'un nœud plus haut? mouais).

On voit que python trace une ligne entre la structure des instructions et la structure du code; ok, c'est une bonne chose : la liste c'est chiant. Mais il en trace également une entre la structure du code et la structure du texte qui le représente; et là, c'est plus gênant.

Une solution ancienne

Ce qui est amusant dans cette histoire, c'est que ce problème a été adressé tôt dans l'histoire.

Dans les origines des langages lisibles par les humains, le passage de la liste à l'arbre a été construit depuis les mathématiques.

LISP, une famille de langages, a été introduite en 1958. Elle repose sur le concept de réduction d'expression ressemblant fortement a ce qu'Alonzo Church a introduit dans les années 30 en formalisant la notion de calcul (avant ça, les calculs étaient quelque chose que l'on faisait, il n'existait pas de méthode systématique pour les décrire) j'ai nommé : λ-calcul.

Le principe est simple : vous prenez un arbre constitué d'expressions, ces expressions peuvent contenir des sous-expressions ou des termes. Lorsqu'on réduit une expression, elle devient un terme; lorsqu'il n'y a plus que des termes, on réduit l'expression qui les contient, etc.

Le lecteur attentif aura compris alors qu'il s'agit d'un arbre.

Le fait intéressant, contrairement à python, est qu'ici l'arbre est explicite dans la nature même du langage, au lieu d'être une structure sous-jacente que l'on cache sous le tapis.

Et, comme souvent chez les matheux, la notation est un point important.

Sexpression, mon amour

Si les jeux de plateau nous on appris une chose, c'est qu'il est plus intéressant d'avoir un jeu avec peu de règles qui génère des comportements complexes qu'un grand nombre de règles modélisant un comportement simple.

Le Go est le champion toutes catégories des jeux simples à comprendre et durs à jouer. Les échecs arrivant probablement en second (ce classement tient compte de la popularité, croyez-vous que sinon la modestie m'eût empêché de parler de Rec-Chess? Allons, ne soyez pas stupide).

Les S-expressions sont devenues la syntaxe de base de lisp.

Le principe en est simple, on fait une liste en énumérant des éléments entre parenthèses :

'(hello world)

Comporte l'élément hello et l'élément world. Un élément a le droit d'être une liste :

'((hello bye) 
  (mother father world))

Ok, appliquons ce principe à du code, a tout hasard celui de tantôt :

(while #t
       (display "do you love me?")
       (if (love)
           (display "yep!")
           (display "nop!")))

Admettez que l'air de famille est plutôt franc. Notons qu'ici les retours à la ligne n'ont aucune valeur autre que de rendre le code lisible pour vos petits yeux sensibles, mais voyons surtout que la structure sous-jacente au code de tantôt est explicite.

En réalité, on n'écrirait probablement pas le code de cette façon en scheme (la répétition de display est inélégante, mais qu'importe).

Ici pas d'exceptions, if est visiblement traité pareil que while et display (ça n'est pas tout à fait le cas en réalité, display est légèrement différent, mais il ne constitue pas un cas particulier pour autant, je me permet ici cette simplification).

Cette absence d'exceptions, et le côté systématique de cette syntaxe dénote une propriété rare et pourtant fascinante :

L'homoiconicité.

Le code est un arbre formé exactement de la même façon que les données exprimées plut haut ((hello world) etc.). Des listes composées formant éventuellement un arbre.

Dès lors, non seulement on ne ment pas «on exécute un arbre, on écrit un arbre», mais en plus on fournit gratuitement un moyen pour lisp de manipuler du code lisp : exactement le même que pour manipuler une liste quelconque.

Voici en substance, la bonne façon de changer de structure de code : En assumant plutôt qu'en faisant mine de rien.

Et l'avenir ?

On peut se demander si un arbre est la structure ultime pour représenter du code.

Si on prend le problème à l'envers, on peut considérer qu'une liste est un cas particulier d'arbre a un seul nœud.

Mais un arbre est un cas particulier de graphe (pour être précis, depuis le début il est même question d'arbre enraciné, ce qui est un type particulier d'arbre).

Je ne vais pas rentrer dans les détails de ce qu'on nomme communément la «théorie des graphes», mais je vais exprimer ici un certain regret.

Si l'arbre est meilleur que la liste c'est parce qu'il est représenté en deux dimensions là où la liste n'en a qu'une (on indente en fonction de la profondeur).

Ajouter des dimensions permet d'exploiter plus de portions de notre cerveau normalement dédiées à des problèmes différents. C'est pour la même raison que l'on colore différents éléments de syntaxe de différentes couleurs et que pendant la rédaction de cet article j'ai essayé de voir ce que pourrait donner d'utiliser les fonctions stéréoscopiques (ie: «3D») des écrans modernes pour rajouter encore de la sémantique.

(Pour les curieux, ça donne ça : stereo emacs

Il est possible de visualiser le relief sans aide en louchant de façon a «réunir» le texte de droite et celui de gauche; avoir un écran 3d est plus confortable celà dit. La profondeur exprime ici le niveau de récursion de l'expression)

La difficulté de l'utilisation d'une structure de données plus complexe est liée à la forme de notre écriture intrinsèquement en 2D. Même une généralisation légère de l'arbre, le graph dirigé acyclique ne peut plus être dessiné à plat sans chevauchement. On triche donc en collant des labels à des morceaux de code et en disant «va le chercher» (la mauvaise méthode pour faire ça repose sur l'instruction goto, la bonne sur les fonctions).

On essaye de grouper code et données en classes, de créer à un autre niveau des structures d'organisation suceptibles d'exprimer plus clairement nos intentions et les liens qui joignent les différentes parties de notre code.

Notre cortex visuel traite de la 2D pour créer une sensation de volume en analysant les images 2d de chaque œil (en utilisant la perspective) et corrige les aberrations en les corrélant pour confirmer cette profondeur a l'aide d'une opération dite de «stereomatching».

Je ne peux m'empêcher de penser cependant que les interfaces neurales directes fourniront sans doute une solution paliant aux effets pervers des limitations de notre perception (Si nous voyons réellement en 3d, un objet ne pourrait en cacher un autre), rien n'empècherait le système de nous communiquer alors directement une représentation des liens unissant des portions de code à d'autres. Le code pourrait aussi être écrit en profondeur.

Mais, est-ce qu'il serait utile de se limiter à 3 dimensions ? Peut-être qu'en jouant correctement avec la plasticité cérébrale…

Mais c'est une histoire pour un autre jour, peut-être même un autre temps.