Du rasoir au microscope : on n'utilise pas le C pour faire de la grammaire — on utilise le bon outil pour le bon produit.
Depuis le début de ce cours, on a construit des trucs avec du C : une factorielle, une machine virtuelle, un parseur de s-expressions, un évaluateur avec environnements.
Et là, il se passe un truc intéressant : on a aussi écrit un interpréteur LISP. Pas pour apprendre LISP (quoique), mais parce que LISP est un méta-outil : il sert à fabriquer d'autres outils. Le C, lui, sert à fabriquer des trucs qui tournent vite. Les deux ensembles, ça devient un atelier complet.
Ce qu'on a dans les mains, ce n'est pas « un langage ». C'est une caisse à outils.
Ce cours 7 n'est pas un cours de plus. C'est une méta-réflexion sur ce qu'on a construit, pourquoi on l'a construit, et comment ces idées se retrouvent — parfois mot pour mot — dans des logiciels qui changent le monde.
Homoiconicitė (du grec homo = même, icon = représentation) : dans un langage homoiconique, le code source et les structures de données ont la même représentation.
Concrètement : en LISP, une expression comme (+ 1 2) est
à la fois :
+, le nombre 1, le nombre 2Tu peux écrire du code qui fabrique du code comme tu fabriques une liste. Un macro, c'est juste une fonction qui renvoie du code, que l'évaluateur exécute ensuite. C'est d'une simplicité diabolique.
;; Donnée = une liste '(+ 1 2) ; quote: c'est une liste, pas du code ;; → (+ 1 2) ;; Code = pareil, mais évalué (+ 1 2) ; évalué → 3 ;; On peut fabriquer du code : (list '+ 1 2) ; → (+ 1 2) (eval (list '+ 1 2)) ; → 3
/* En C, le code c'est du texte, les données c'est des struct. Jamais les deux ne se mélangent. (sauf avec des pointeurs de fonction, mais c'est pas de l'homoiconicité) */ int x = 1 + 2; // du code int tab[] = {43, 42, 41}; // des données // Essayer de traiter le code comme données ? // Vous pouvez écrire un interpréteur, // mais le langage ne le fait pas tout seul.
Parce que l'homoiconicité est le super-pouvoir qui se cache derrière tous les grands systèmes de calcul formel et numérique. Regardez :
LISP, né en 1958 (même âge que le C, en fait — et pourtant ils n'ont pas du tout la même tête), est le deuxième plus vieux langage de haut niveau encore utilisé (après Fortran). Mais son héritage est partout.
Mathematica est le leader mondial du calcul formel.
Son langage interne (le Wolfram Language) est directement inspiré de LISP.
Toute expression est une liste : Plus[1, 2] au lieu de
(+ 1 2), mais c'est la même structure d'arbre.
;; LISP (+ 1 (* 2 3)) ; → 7 (* Wolfram Language *) Plus[1, Times[2, 3]] ; → 7 (* Et Mathematica peut manipuler ça comme des données *) FullForm[1 + 2x] ; → Plus[1, Times[2, x]]
Les capacités de calcul formel de Mathematica (dérivation, intégration, simplification, résolution d'équations) reposent sur le fait que les expressions sont des arbres manipulables par le programme lui-même. C'est exactement le principe de notre évaluateur LISP.
R est le langage de référence pour les statistiques et la visualisation de données. Son cœur est un dialecte de Scheme (un descendant de LISP). Chaque expression R est une liste :
;; LISP (+ 1 2) (mean (c 1 2 3 4 5)) # R `+`(1, 2) # Oui, c'est un appel de fonction comme une liste mean(c(1, 2, 3, 4, 5)) # Pareil
Le ~ de R (formules de modèle linéaire : y ~ x + z)
est une macrologie LISP déguisée : la formule est une
expression non évaluée, que R réécrit et manipule avant de l'interpréter.
Les data.frame, les list, tout est conçu
sur le modèle code = donnée.
Julia est un langage de calcul haute performance qui veut le beurre (la vitesse du C) et l'argent du beurre (la flexibilité des langages dynamiques).
Le secret de Julia ? Il est compilé à la volée (JIT) via LLVM, et son système de macros est directement pompé de LISP. Les macros Julia manipulent l'AST (Abstract Syntax Tree) — exactement comme notre évaluateur manipule les s-expressions :
# Julia : macro style LISP macro assert(expr) # expr est une expression, pas encore évaluée # On peut l'inspecter, la modifier… quote if !($expr) println("Assertion failed: ", $(string(expr))) end end end ;; Équivalent LISP (notre let.c !) (define-macro assert (expr) `(if (not ,expr) (display (string-append "Assertion failed: " ,(symbol->string expr)))))
Julia peut tourner du code aussi vite que du C optimisé parce que ses types sont optionnellement annotés et que le compilateur JIT les utilise pour produire du code machine spécialisé. Mais en dessous, c'est LISP qui respire.
Clojure est un dialecte LISP moderne qui tourne sur la machine virtuelle Java (JVM). Il apporte l'homoiconicité du LISP à l'écosystème Java — et ça marche tellement bien que des boîtes comme Walmart, Netflix ou Apple l'utilisent en production.
Tout est immutable par défaut (plus de bugs de mutation), les structures de données sont persistantes (partage de mémoire), et les macros sont toujours là, bien sûr.
;; Clojure : un web server en 5 lignes (require '[ring.adapter.jetty :refer [run-jetty]]) (require '[ring.util.response :refer [response]]) (defn handler [req] (response "Hello from Clojure! (LISP on the JVM)")) (run-jetty handler {:port 3000}) ;; C'est quoi cette syntaxe ? Des parenthèses ? ;; Oui. Et ça sert la page en moins de temps que votre café.
AutoCAD est la référence mondiale de la CAO (Conception Assistée par Ordinateur) en mécanique, architecture, génie civil. Et son langage de script s'appelle AutoLISP. Oui : LISP.
Depuis 1986, AutoCAD embarque un interpréteur LISP pour permettre aux ingénieurs d'automatiser leurs tâches de dessin. Tu veux générer 500 poutres avec des espacements paramétriques ? Tu écris une boucle en AutoLISP. Tu veux un script qui dessine un escalier en colimaçon ? C'est 20 lignes de LISP.
;; AutoLISP : dessiner un cercle (command "circle" (list 0 0 0) 5.0) ;; AutoLISP : générer une rangée de piliers (setq i 0) (repeat 10 (command "box" (list (* i 10) 0 0) (list (+ (* i 10) 2) 10 2)) (setq i (1+ i)) )
Pourquoi LISP pour un logiciel de dessin ? Parce que les dessins techniques sont des structures arborescentes (blocs, calques, entités), et que LISP manipule les arbres comme personne. L'homoiconicité permet de traiter un plan comme du code, et du code comme un plan.
Maintenant, regardez ce qu'on a construit dans ce cours :
| Composant | Langage | Fait quoi | Retrouvé dans |
|---|---|---|---|
| VM à pile | C | Exécute du bytecode rapidement | JVM, CLR, LuaJIT, Python VM |
| Parseur s-expressions | C | Transforme du texte en arbre | Wolfram Kernel, R parser, Julia parser |
| Évaluateur (envs chaînés) | C | Évalue des expressions LISP | AutoLISP, Emacs LISP, tout REPL |
| Compilateur calc → bytecode | C | Compile du LISP en bytecode VM | Julia → LLVM, Mathematica → bytecode |
Chaque brique qu'on a écrite est une version miniature mais fonctionnelle de ce qui fait tourner le monde numérique. La VM ? C'est la JVM en plus petit. Le parseur s-expression ? C'est le cœur de Mathematica. L'évaluateur avec environnements ? C'est comment tourne R, Julia, Python.
Ce septième cours est un point d'étape. On a passé six cours à construire des briques. Maintenant, on lève la tête et on regarde ce qu'on a fait.
Et tout ça, vous l'avez fait en C, un langage qui a 50 ans, qui tient dans une poche, et qui fait tourner 99% du monde numérique. Pas parce que c'est le langage le plus élégant (il ne l'est pas), mais parce que c'est le plus fondamental.
Pour que tout soit clair, voici comment chaque concept du cours se retrouve dans des systèmes réels :
| Notion du cours | Notre implémentation | Dans le monde réel |
|---|---|---|
| VM à pile | vm.c |
JVM, CPython, LuaJIT, WebAssembly |
| Parseur s-exp | calc.c (parse) |
Wolfram Kernel, R parse(), Julia Meta.parse() |
| Évaluateur | let.c |
AutoLISP, Emacs Lisp, R eval() |
| Macros / code = donnée | let.c (éval liste) |
Clojure macros, Julia @macro, R non-standard eval |
| Compilateur LISP → bytecode | calc.c (-vm) |
Julia → LLVM, Mathematica → WVM, Clojure → JVM |
| Transpileur | transpile.c |
TypeScript → JS, Python → C, tout transpileur |
| Gestion mémoire | sexpression.c |
malloc/free, arena allocators, GC de Lua/Julia |
On a parlé de concepts, de paradigmes, de langages qui ont changé le monde. Mais si on s'arrêtait là, ce serait un peu comme un cours de menuiserie où on n'a montré que des photos de belles étagères sans jamais en construire une.
Alors construisons.
On a deux fichiers LISP dans notre projet :
mathematica.lisp (dérivation symbolique)
et matlab.lisp (calcul vectoriel et matriciel).
Ce sont de vraies bibliothèques, utilisables depuis let.c.
Et la première chose qu'on remarque quand on les ouvre, c'est ça :
;;; mathematica.lisp — Dérivation symbolique et simplification ;;; ;;; (deriv expr var) — dérive une expression arithmétique ;;; (simplify expr) — simplifie une expression ;;; (deriv-simp expr var) — dérive + simplifie ;;; matlab.lisp — Calcul numérique vectoriel et matriciel ;;; ;;; Vecteurs : addvec subvec mulvec scale dot sum norm norm1 cross ;;; Matrices : addmat submat transpose mulmat identity zeros ones ;;; Utilitaires : abs len append map range
Un en-tête de documentation. C'est la première chose qu'on
écrit — et la dernière qu'on lit, mais c'est elle qui sauve une vie
à 3h du matin quand on cherche pourquoi mulmat retourne une
liste impropre. (Spoiler : c'est parce que () vaut 0
dans let.c. On a perdu 20 minutes là-dessus, et si ce n'était pas écrit,
on y serait encore.)
Ouvrons nos deux bibliothèques et regardons ce qu'elles contiennent :
mathematica.lisp — le laboratoire de calcul formel| Fonction | Paramètres | Retourne | Exemple |
|---|---|---|---|
deriv |
expr var |
Expression dérivée (brute) | (deriv '(Add (Var x) (Val 1)) 'x) → (Add (Val 1) (Val 0)) |
simplify |
expr |
Expression simplifiée | (simplify '(Add (Val 0) (Var x))) → (Var x) |
deriv-simp |
expr var |
Dérivée simplifiée | (deriv-simp '(Mult (Var x) (Var x)) 'x) → (Add (Var x) (Var x)) |
Les expressions sont représentées sous forme d'arbres :
(Add (Mult (Val 3) (Var x)) (Val 1)) pour
3x + 1. Le calcul formel, c'est juste de la manipulation
d'arbres — et LISP est littéralement fait pour ça puisque
le code est un arbre.
matlab.lisp — le couteau suisse du calcul numérique| Fonction | Paramètres | Retourne | Exemple |
|---|---|---|---|
addvec | a b | a + b (vec) | (addvec '(1 2) '(3 4)) |
subvec | a b | a − b (vec) | (subvec '(5 7) '(4 5)) |
mulvec | a b | a × b (vec, elem) | (mulvec '(2 3) '(5 6)) |
scale | s v | s × v (scalaire × vec) | (scale 5 '(1 2 3)) |
dot | a b | a · b (scalaire) | (dot '(1 2) '(3 4)) |
sum | v | Σ v | (sum '(1 2 3)) |
norm | v | ‖v‖₂ | (norm '(3 4)) |
cross | a b | a × b (3D) | (cross '(1 0 0) '(0 1 0)) |
addmat | a b | a + b (mat) | (addmat '((1 2)(3 4)) '((5 6)(7 8))) |
transpose | m | mᵀ | (transpose '((1 2)(3 4))) |
mulmat | a b | a × b (matriciel) | (mulmat '((1 2)(3 4)) '((1 0)(0 1))) |
identity | n | Iₙ | (identity 3) |
zeros | n m | 0n×m | (zeros 2 3) |
ones | n m | 1n×m | (ones 2 2) |
Chaque fonction est définie en quelques lignes de LISP pur, sans une ligne de C supplémentaire. Quand on a un évaluateur, on peut étendre le langage dans le langage lui-même. C'est ça, la puissance d'un REPL : on construite des abstractions par-dessus des abstractions.
Voyons ces bibliothèques en action — avec de vraies sorties du programme :
mathematica.lisp$ ./let '(load mathematica.lisp)' '(deriv-simp (quote (Add (Mult (Val 3) (Var x)) (Val 1))) (quote x))' ; (load mathematica.lisp) -> #<environnement> ; (deriv-simp (quote (Add (Mult (Val 3) (Var x)) (Val 1))) (quote x)) -> (Val 3) ; La dérivée de 3x + 1 est… 3. (Oui, c'est trivial. Mais c'est calculé par du LISP.)
Mathématiquement : d/dx (3x + 1) = 3. Notre bibliothèque le trouve toute seule. Pas de bête de somme. Pas d'appel à Wolfram Alpha. Juste notre évaluateur et 50 lignes de LISP.
$ ./let '(load mathematica.lisp)' '(deriv-simp (quote (Mult (Var x) (Var x))) (quote x))' ; (deriv-simp (quote (Mult (Var x) (Var x))) (quote x)) -> (Add (Var x) (Var x)) ; d/dx de x² = x + x (non simplifié — comme dirait un prof : ; « c'est pas faux, mais on peut mieux faire »)
x + x au lieu de
2x). C'est normal : on a écrit le noyau en un après-midi,
pas en dix ans comme Mathematica. Mais le principe est le même —
et notre version coûte 0 € et zéro licences.
Le prix de la connaissance, c'est le temps.
Le prix du logiciel propriétaire, c'est de l'argent.
Choisissez votre combat.
matlab.lisp$ ./let '(load matlab.lisp)' '(addvec (quote (1 2 3)) (quote (4 5 6)))' ; (addvec (quote (1 2 3)) (quote (4 5 6))) -> (5 7 9) $ ./let '(load matlab.lisp)' '(mulmat (quote ((1 2) (3 4))) (quote ((5 6) (7 8))))' ; (mulmat (quote ((1 2) (3 4))) (quote ((5 6) (7 8)))) -> ((19 22) (43 50)) ; [[1 2] [3 4]] × [[5 6] [7 8]] = [[19 22] [43 50]] $ ./let '(load matlab.lisp)' '(identity 3)' ; (identity 3) -> ((1 0 0) (0 1 0) (0 0 1)) $ ./let '(load matlab.lisp)' '(norm (quote (3 4)))' ; (norm (quote (3 4))) -> 5.0 ; ‖(3,4)‖₂ = √(3² + 4²) = 5. Le théorème de Pythagore, version LISP.
On passe des listes à notre bibliothèque, elle les interprète comme des vecteurs ou des matrices, et elle effectue les calculs. Ce n'est pas encore NumPy — mais c'est le même pattern : une couche LISP par-dessus un noyau C.
matlab.lisp, c'est 150 lignes de LISP avec une interface
LISP. La différence ? L'échelle. Le principe ? Exactement le même.
Vous venez de réimplémenter (en miniature) le cœur de deux des logiciels
les plus utilisés au monde. Ça ne vous donne pas le droit de frimer en soirée,
mais presque. (Enfin, si, un peu. « J'ai écrit une bibliothèque de calcul
formel en LISP. » — effet garanti sur les informaticiens, nul sur les autres.)
Voilà où on en est dans notre voyage C → LISP → outil concret :
On a commencé avec du C pur : une VM, un parseur, un évaluateur.
On a écrit du LISP : des fonctions récursives qui manipulent des arbres.
On a fini avec des outils : une bibliothèque de calcul formel et
une bibliothèque de calcul numérique. Le tout avec le même binaire
./let, sans recompiler, sans installer de package.
C'est ça, l'idée : le socle est en C, la créativité est en LISP. Le C te donne la machine. Le LISP te donne le volant. Et maintenant, tu peux conduire.
./let '(load matlab.lisp)' et c'est parti.
L'outil est dans le langage, le langage est dans l'outil.
Ouroboros informatique.