Commentaires de Hofstadter sur Lisp (1983)
(gist.github.com/jackrusher)- Le texte de 1983 de Douglas Hofstadter présente Lisp, largement utilisé dans la recherche en IA, comme un langage élégant et flexible bâti sur un petit noyau mathématique
- L’interactivité de Lisp apparaît dans la read-eval-print loop : l’utilisateur saisit une expression, l’interpréteur la lit, l’évalue, l’affiche, puis attend l’entrée suivante
- Les objets Lisp sont structurés autour des atomes (atom) et des listes (list) ; nil occupe une position particulière, étant à la fois la liste vide et un atome
quote,eval,car,cdr,cons,setq,set,lambda,def,condpermettent de traiter le code et les données avec la même structure de listes, et ainsi de construire progressivement de nouvelles fonctions- La composition de fonctions, les expressions conditionnelles, les effets de bord et le style applicatif, ainsi que la pensée récursive, sont reliés par des exemples ; la fonction finale
poweret la métaphore du « porpuquine » suggèrent la récursion à la Lisp
La recherche en IA et la place de Lisp
- En 1983, l’IA était un domaine de recherche visant à implémenter sur ordinateur des comportements tels que la flexibilité, le bon sens, l’intuition, la créativité, la conscience de soi ou l’humour
- Aux États-Unis, environ 2 000 personnes travaillaient spécifiquement sur l’IA, et il existait à l’étranger une communauté de chercheurs d’une taille comparable
- Les chercheurs en IA étaient très divisés sur la meilleure voie pour parvenir à l’IA, mais, pour le choix du langage de programmation, ils utilisaient presque tous Lisp
- Le nom Lisp vient de « list processing » et n’est pas un acronyme complet
- L’attrait de Lisp tient au fait qu’il s’agit d’un langage « crisp » et « elegant »
- Là où beaucoup de langages accumulent de nombreuses fonctionnalités arbitraires, certains langages comme Lisp et Algol sont organisés autour d’un noyau naturel, à la manière d’une branche des mathématiques
- Le noyau de Lisp possède une « crystalline purity », et cette pureté contribue non seulement à son esthétique, mais aussi à sa flexibilité
De la logique mathématique au langage de traitement de listes
- Les racines profondes de Lisp se trouvent dans la logique mathématique
- Des idées laissées par Thoralf Skolem, Kurt Godel et Alonzo Church dans la logique des années 1920 et 1930 ont ensuite été intégrées à Lisp
- La programmation informatique à proprement parler a commencé dans les années 1940, et les langages de haut niveau comme Lisp sont apparus dans les années 1950
- Le premier langage de traitement de listes n’était pas Lisp, mais IPL (Information Processing Language)
- IPL a été développé au milieu des années 1950 par Herbert Simon, Allen Newell et J. C. Shaw
- John McCarthy a créé entre 1956 et 1958, à partir d’idées existantes, un langage algébrique de traitement de listes appelé Lisp
- Lisp s’est rapidement diffusé parmi les jeunes chercheurs autour du MIT Artificial Intelligence Project
- Il a été implémenté sur IBM 704, puis s’est propagé à d’autres groupes de recherche en IA
- Plusieurs dialectes sont apparus, mais ils partageaient un noyau central élégant
Lisp interactif et REPL
- Contrairement à beaucoup d’autres langages de haut niveau, Lisp est présenté comme un langage interactif
- L’utilisateur lance Lisp dans un terminal, voit une invite, puis saisit une expression
- Exemple : si l’on saisit
(plus 2 2),4est affiché et une nouvelle invite apparaît
- Exemple : si l’on saisit
- Le fonctionnement central de l’interpréteur Lisp est la read-eval-print loop
- Lire l’expression
- Évaluer l’expression
- Afficher la valeur appropriée
- Signaler qu’il est prêt à lire l’expression suivante
- Grâce à cette structure, l’utilisateur peut saisir ses « souhaits » un par un et vérifier immédiatement le résultat
- Dans de nombreux langages non interactifs, il faut d’abord écrire tout le programme puis l’exécuter ; plusieurs étapes doivent donc s’emboîter parfaitement, et la correction des erreurs est indirecte et coûteuse
- Lisp rend possibles le développement et le débogage incrémentaux en exécutant les expressions une par une
Notation polonaise et parenthèses
- Les expressions arithmétiques de Lisp utilisent la notation polonaise, où l’opérateur précède les opérandes
- Exemple :
(times (plus 6 3) (difference 6 3))s’évalue à 27
- Exemple :
- Cette notation a été inventée avant l’ordinateur par le logicien polonais Jan Lukasiewicz
- Les expressions Lisp contiennent beaucoup de parenthèses
- Il est fréquent que de longues expressions se terminent par plusieurs parenthèses fermantes
- Au début, cela peut sembler lourd, mais, avec l’habitude, la structure logique devient intuitive
- Les exemples sont présentés sous forme de pretty printing, avec l’indentation qui fait ressortir la structure
Atomes, listes, nil
- Le cœur de Lisp est constitué de structures manipulables, et tous les programmes fonctionnent en créant, modifiant et détruisant des structures
- Il existe deux types de structures
- Atome (atom) : objet de base qui ne se subdivise pas davantage
- Liste (list) : structure regroupant plusieurs éléments dans un ordre donné
- Tout objet Lisp est soit un atome, soit une liste, avec une seule exception : nil
- nil est à la fois un atome et une liste
- nil représente la liste vide
()etnilont le même sens, mais nil est plus souvent utilisé
- Les éléments d’une liste peuvent être des atomes ou d’autres listes
- Exemple :
(zonk blee strill (croak flonk))est une liste de quatre éléments, dont le dernier est lui-même une liste de deux éléments
- Exemple :
- Les expressions Lisp elles-mêmes sont des listes
(plus 2 2)est aussi une liste- L’interpréteur Lisp peut manipuler des listes et des atomes pour créer de nouvelles « commandes » et les évaluer
Valeurs, quote, eval
- Les atomes peuvent avoir des valeurs
- Les atomes numériques ont eux-mêmes pour valeur permanente
- La valeur de nil est nil
- t est également un atome spécial ayant lui-même pour valeur permanente
setqassigne une valeur à un atome(setq pie 4)donne àpiela valeur 4(setq pie (plus 2 2))donne aussi àpiela valeur 4
- La valeur d’un atome peut être non seulement un nombre, mais n’importe quel objet Lisp
- Il peut s’agir d’un atome ou d’une liste
- quote empêche l’évaluation et permet de traiter une liste ou un atome tel quel comme une donnée
(setq pie (plus 2 2))évalue l’expression interne et stocke 4 danspie(setq pi '(plus 2 2))stocke danspila liste(plus 2 2)elle-même
evalréévalue une expression stockée comme valeur- Si la valeur de
piest(plus 2 2), alors(eval pi)renvoie 4 - L’interpréteur n’évalue normalement qu’un seul niveau, mais
evalpermet de demander une évaluation supplémentaire
- Si la valeur de
car, cdr, cons
- Toute liste non nil possède au moins un élément, et le premier élément est appelé car
- Le car de
(eval pi)esteval - Le car de
(plus 2 2)estplus
- Le car de
- Le car n’a pas nécessairement à être un nom de fonction
- Le car de
((1)(2 2) (3 3 3))est(1)
- Le car de
- La liste qui reste après suppression du car est le cdr
- Le cdr de
(a b c d)est(b c d) - Puis il se réduit à
(c d),(d), puis nil
- Le cdr de
- nil n’a ni car ni cdr, et tenter d’en prendre le car ou le cdr devrait produire une erreur
consajoute un nouveau car au début d’une liste existante pour créer une nouvelle liste- Quand
xvaut(cake cookie),(cons 'pie x)crée(pie cake cookie) - La valeur existante de
xn’est alors pas modifiée
- Quand
- Le résultat change selon la présence ou l’absence de quote
- Si la valeur de
pieest 4,(cons pie x)renvoie(4 cake cookie) (cons 'pie x)ajoute le nom d’atomepielui-même au début
- Si la valeur de
Différence entre setq et set
- L’interpréteur Lisp n’évalue pas toujours ses arguments, et
setqen est l’exemple représentatif - Le premier argument de
setqn’est pas évalué : il est traité tel quel comme un nom de variable- Le q de
setqsignifie quote, et le premier argument est traité comme s’il était cité
- Le q de
setressemble àsetq, mais évalue aussi son premier argument- Quand la valeur de x est k,
(set x 7)ne modifie pas x, mais donne à k la valeur 7
- Quand la valeur de x est k,
- L’exemple présente la séquence suivante
(setq a 'b)(setq b 'c)(setq c 'a)(set a c)(set c b)
- Après cette exécution, les valeurs de a, b et c sont toutes respectivement
a setest rarement utilisé, et ce type de confusion ne se produit pas souvent
Définition de fonctions et lambda
- La grande force de la programmation tient à la capacité de définir de nouvelles opérations composées à partir d’opérations existantes, puis de répéter ce processus pour accumuler un répertoire d’opérations plus complexes
- En Lisp, on définit de nouvelles fonctions à partir de fonctions déjà connues
racest un exemple de fonction qui renvoie le dernier élément d’une liste- Pour obtenir le dernier élément, il suffit d’inverser la liste puis d’en prendre le car
- Définition :
(def rac (lambda (lyst) (car (reverse lyst))))
- Le
(lyst)qui suitlambdareprésente le paramètre de la fonction, c’est-à-dire une variable muette - Après sa définition avec
def,racdevient une fonction utilisable commecar(rac '(your brains))renvoiebrains
- La fonction
readers-digest-condensed-versioncrée, à partir d’une longue liste, une courte liste ne contenant que le premier et le dernier élément- Si l’on traite l’intégralité de Finnegans Wake de James Joyce comme une liste de mots, l’exemple obtient
(riverrun the)
- Si l’on traite l’intégralité de Finnegans Wake de James Joyce comme une liste de mots, l’exemple obtient
- L’opération inverse, appelée
rejoyce, est une fonction imaginaire qui générerait un roman dont le début et la fin sont fixés par deux mots ; son écriture est laissée en exercice au lecteur
Valeurs de retour, effets de bord et style applicatif
- Certains estiment que deux objectifs sont souhaitables et possibles dans Lisp et les langages apparentés
- Toutes les expressions renvoient une valeur
- L’effet d’une expression ne se produit qu’au travers de sa valeur de retour
- Les dialectes Lisp abordés dans le texte satisfont à la première condition, mais pas nécessairement à la seconde
(reverse x)ne modifie pas x lui-même : il crée et renvoie une nouvelle liste identique à x, mais dans l’ordre inverse- De la même manière que
(plus 2 2)ne modifie pas la valeur de 2
- De la même manière que
consrenvoie lui aussi une nouvelle liste sans modifier la liste existante- En revanche,
setqest un exemple de commande qui laisse un effet de bord en modifiant une liaison de variable- Les effets de bord peuvent aussi inclure les entrées-sorties, en plus de la modification de liaisons de variables
- Les partisans de la programmation applicative préfèrent que les fonctions calculent et transmettent des valeurs sans effets de bord
- Dans ce style, les seules liaisons autorisées sont les liaisons lambda, qui apparaissent brièvement pendant un appel de fonction
- Une fois le calcul de la fonction terminé, les liaisons des variables muettes disparaissent
- Le narrateur du texte trouve le style applicatif élégant, mais le juge peu pratique pour créer de grands programmes de style IA
- Dans une programmation applicative stricte, même
defdevient un cas extrême non autorisé, puisqu’il stocke en mémoire une définition de fonction permanente
Expressions conditionnelles cond et décisions
- Pour que Lisp puisse faire des choses plus intéressantes, il doit pouvoir prendre des décisions en fonction de ce qui se produit au cours des étapes intermédiaires ; il lui faut alors des expressions conditionnelles
- Exemple :
(cond ((eq x 1) 'land) ((eq x 2) 'sea))- Si x vaut 1 :
land - Si x vaut 2 :
sea - Sinon, renvoie nil
- Si x vaut 1 :
eqest une fonction Lisp qui renvoietsi les valeurs de ses deux arguments sont identiques, etnilsinon- Une expression
condcommence par le nom de fonctioncondet comporte plusieurs clauses- Chaque clause est une liste de deux éléments composée d’une condition et d’un résultat
- Les conditions sont testées dans l’ordre ; dès qu’une condition renvoie une valeur non nil, le résultat correspondant est évalué et renvoyé comme valeur de l’ensemble du
cond - Les clauses suivantes ne sont pas examinées
- En plaçant à la fin une clause fourre-tout dont la condition est
t, on peut renvoyer un résultat par défaut au lieu de nil- Exemple : un
condrenvoyantairsi les conditions land et sea échouent toutes les deux est présenté
- Exemple : un
Puissances et structure récursive
- À la fin du texte apparaissent des définitions de fonctions au motif très net
squarevautk * kcubevautk * square(k)4th-powervautk * cube(k)- Le même schéma continue ainsi
- La question posée est de savoir s’il est possible de définir une fonction à deux paramètres
powerqui englobe d’un seul coup tout ce motif(power 9 3)doit donner 729(power 7 4)doit renvoyer 2 401
- Tous les outils nécessaires ont été présentés dans le texte, et il est fait appel à l’ingéniosité du lecteur
- L’histoire finale du porpuquine glazunkien traite la structure récursive comme une fable
- Les piquants du porpuquine sont des porpuquines plus petits
- En Glazunkia extérieure, il y a toujours 9 piquants ; en Glazunkia intérieure, toujours 7
- Un porpuquine de 0 pouce n’a pas de piquants, ce qui arrête la régression infinie
- Le « buying power », ou « power », de cet animal est lié au nombre de nez de petits porpuquines de 0 pouce qu’il contient, et le texte se conclut sur cette métaphore reliée au problème
powerprésenté plus haut
Contexte de republication du texte original
- Le début du Gist indique qu’il s’agit d’une introduction à Lisp de Douglas Hofstadter, découverte dans un ancien numéro de Scientific American du milieu des années 1980
- Les exemples peuvent encore être exécutés dans Emacs en installant quelques alias
plus→+quotient→/times→*difference→-
- La fin indique que, si l’on a apprécié la présentation singulière de Lisp par Hofstadter, on peut trouver d’autres textes similaires dans son livre Metamagical Themas
1 commentaires
Avis sur Hacker News
Les noms de fonctions
"oval"et"snot"dans l’exemple m’ont embrouillé, puis au bout de quelques secondes j’ai compris qu’il s’agissait respectivement de coquilles pour"eval"et"snoc"Le code original devrait se lire comme
(cond ((eq (eval pi) pie) (eval (snoc pie pi))) (t (eval (snoc (rac pi) pi))))En récupérant l’article original de Scientific American sur https://www.jstor.org/stable/24968822 pour vérifier, il s’est avéré que
oval/snotétaient bien des coquilles poureval/snoc"oval"et"snot"sont bien des coquilles pour"eval"et"snoc"Et
snocsemble êtreconsà l’envers,racétantcarà l’enversCet article montre une fois de plus que l’une des raisons essentielles de la faible popularité de Lisp tenait à la manière dont on expliquait Lisp
Ce genre d’articles sur Lisp ne m’a rien appris, quand j’étais enfant, qui soit réellement utilisable, et ne montrait pas non plus à quel point une tâche X devenait plus facile par rapport à l’assembleur/C/Pascal, etc.
Aujourd’hui, j’aurais montré quelque chose comme : « Un correcteur orthographique qui a pris 7 mois en assembleur ? Sur un micro-ordinateur utilisant de la mémoire commutée par banque, c’est presque trivial en Lisp, et même sur un CPU médiocre on peut rendre le garbage collection déterministe »
Tant d’articles et de manuels sur Lisp répétaient seulement listes, récursion et IA, sans montrer comment faire des choses utiles, que j’ai l’impression d’avoir perdu du temps à programmer comme avec une pince à épiler, des grains de riz et de la colle
Le premier magazine Byte que j’ai vu enfant contenait du code Lisp pour de la dérivation symbolique et de la simplification algébrique ; c’était difficile à suivre, mais il y avait quelque chose de séduisant
Ça n’aurait certainement pas été plus simple en Basic, et j’ai découvert plus tard que ce code n’était pas si excellent que ça, mais ce n’est qu’après avoir rencontré XLisp sur PC à la fin des années 80, puis SICP, que je suis vraiment tombé dans Lisp
J’aime vraiment l’écriture de Hofstadter. Il saisit bien l’émotion que j’ai ressentie en découvrant Lisp
J’étais un gamin qui apprenait à programmer dans les années 80 et, au lycée puis au début de l’université, j’avais déjà un peu touché à BASIC, Fortran, Pascal et COBOL ; malgré leurs différences, ils avaient fondamentalement des points communs
Mais le premier cours d’informatique à UC Berkeley se faisait avec Scheme, un dialecte de Lisp, et ça a été un choc total
Comme le dit Hofstadter, c’était ce qui se rapprochait le plus des mathématiques, cela me rappelait beaucoup les cours de théorie mathématique, et c’était le premier beau langage que je découvrais
J’ai particulièrement aimé cette citation : « Lisp et Algol sont bâtis autour d’un noyau naturel, comme une branche des mathématiques. Au cœur de Lisp se trouve une pureté cristalline qui ne fait pas seulement appel au sens esthétique, mais le rend aussi bien plus flexible que la plupart des langages »
Même du point de vue de quelqu’un qui voulait devenir topologue algébrique, Scheme ressemble plutôt à une expression élégante et minimale d’un style de programmation répandu dans l’ensemble des langages à typage dynamique et à garbage collection
Il paraît complet, et en ce sens ressemble à une « théorie » : on peut réfléchir à une manière de résoudre un problème en Scheme, puis la transposer dans un autre langage à typage dynamique et aboutir à une solution élégante
Scheme était un langage qui simplifiait et clarifiait le Lisp traditionnel de l’époque, notamment avec la portée lexicale et un espace de noms unique pour les fonctions et les variables
En 1983, environ vingt ans après les débuts du domaine, on parle déjà d’IA visant à programmer des ordinateurs dotés de « flexibilité, bon sens, intuition, créativité, conscience de soi, humour »
C’est assez amusant, parce que cette liste se lit comme celle de tout ce que les LLM ne savent vraiment pas faire
Malgré tout, on peut dire qu’il y a eu au moins un progrès non nul dans cette direction
Peut-être même davantage, même si le dire ainsi me paraît un peu étrange à moi-même
Même si beaucoup de choses n’ont pas encore débouché sur des résultats, cela reste intéressant à lire
Les deux articles compagnons mentionnés avec celui-ci figurent dans les chapitres 17 à 19 du livre de Hofstadter Metamagical Themas, avec d’autres articles de la même chronique de Scientific American
[0]: https://www.goodreads.com/book/show/181239.Metamagical_Thema...
Le titre du livre vient du titre de la chronique, une anagramme de « Mathematical Games », que Martin Gardner tenait dans Scientific American, et dont Hofstadter a pris la suite
L’article affirme qu’appliquer
caroucdrànildevrait produire une erreur, comme une division par zéro, mais ce n’est souvent plus le cas dans les Lisp modernes.Dans le Lisp original défini par John McCarthy,
CARetCDRn’étaient pas définis pourNIL: <https://dl.acm.org/doi/pdf/10.1145/367177.367199>Mais Common Lisp et Emacs Lisp définissent
(car nil)et(cdr nil)comme valantnil: <https://www.lispworks.com/documentation/HyperSpec/Body/f_car...>, <https://www.gnu.org/software/emacs/manual/html_node/elisp/Li...>Dans
LISP 2156,(status lispversion)renvoyait/2156, et(car nil)comme(cdr nil)renvoyaient tous deuxNIL.caretcdraient joué un rôle central dans la naissance d’un langage conçu mathématiquement.Les opérateurs les plus fondamentaux et les plus célèbres de « List Processor » n’ont pas été conçus pour agir sur des listes, mais sur cons, un élément particulier de la représentation machine utilisée par Lisp pour construire des structures de données.
Un cons n’est pas non plus toujours interprété comme une liste, et la liste, pourtant cruciale comme cas de base des fonctions récursives sur les listes, n’est même pas représentée par un cons.
Soixante ans plus tard, la plupart des programmes Lisp restent remplis d’opérations sur les cons, et un nom plus exact aurait peut-être été « Cons Processor ».
Cela rappelle que Lisp est né à une époque où langage et implémentation devaient s’emboîter étroitement, ce qui rend d’autant plus impressionnante la réussite consistant à fonder un langage informatique sur la logique mathématique.
CARou leCDRdenilest une erreur.car/cdrdenilest une erreur, ce qui rend le code assez peu ergonomique pour un débutant comme moi.Je préfère tout de même Guile Scheme à Common Lisp, donc c’est dommage.
nilne produit pas d’erreur et renvoie simplementnil.SQL me vient aussi à l’esprit, et comme je considère personnellement que c’est un mauvais choix, à défaut d’autre terme je serais tenté d’appeler cela « bleeding nils/NULLs ».
C’est encore pire si
niln’est pas équivalent àfalsedans les comparaisons booléennes.En Ruby et en Elixir,
nilest traité commefalse, et Elixir fournit séparémentand, qui n’accepte que de vrais booléens, et&&, qui traitenilcommefalse.Ce type de conception peut rendre le code plus propre au début, mais un
nilmal géré qui est en réalité une erreur peut finir par se manifester dans une partie totalement différente du programme, plusieurs piles d’appels plus loin, ce qui rend le débogage bien plus difficile.Je connaissais déjà les informations sur Lisp dans cet article, mais je l’ai quand même lu avec plaisir. Hofstadter a vraiment un style savoureux.
J’ai particulièrement aimé sa plaisanterie consistant à inventer
rejoyce, l’opération inverse dereaders-digest-condensed-version: en exécutant(rejoyce 'Stately 'Yes), une fée Lisp générerait depuis le début l’intégralité d’Ulysses telle que James Joyce aurait pu l’écrire.Cela a pris un certain temps, mais nous y sommes finalement arrivés ; même si l’IA de 2024 n’est pas exactement celle qu’il imaginait en 1983, reproduire du texte à partir d’une courte amorce est aujourd’hui une tâche qui correspond assez bien à l’IA.
Je pense que Lisp reste l’un des principaux langages capables d’exprimer le concept de boucle étrange abordé par Hofstadter
Lisp n’est pas le seul langage homoïconique, mais parmi les langages que les gens savent réellement utiliser, il fait partie des plus importants dont
evalne reçoit pas une chaîne à parserJe n’aime pas que les gens assimilent Lisp à l’ensemble de la programmation fonctionnelle
Je n’ai rien contre la programmation fonctionnelle, mais le caractère symbolique de Lisp est bien plus intéressant, et je trouve infiniment amusant qu’on puisse aussi très facilement écrire du code piloté par des sections
(go tag), à la manière d’une programmation avec GOTOAvec XSLT et XML, des fonctionnalités de métaprogrammation consistant à générer du XSLT au lieu d’écrire directement du code répétitif ont été assez largement utilisées
En revanche, la syntaxe était un problème encore plus important que celle de Lisp
Il n’est pas facile de créer un langage dont la syntaxe soit agréable au quotidien tout en restant maniable comme arbre de syntaxe abstraite, et Lisp est l’un des rares exemples relativement réussis
functoolsde Python ne permet pas vraiment : https://docs.python.org/3/library/functools.htmlJe construis une AGI symbolique sans Lisp, et j’aimerais avoir des pistes de la part de spécialistes
Ce que j’ai compris des fonctionnalités côté Python, ce sont des fonctions comme
filter(),map()etreduce()qui appliquent d’autres fonctions à des objets itérables, des wrappers comme@singledispatchqui routent les appels, des fonctions comme@cacheetpartial()qui offrent du contrôle de flux ou des commodités de performance, et des mécanismes commewraps()qui permettent d’envelopper arbitrairement des fonctionsLa plupart de ces éléments me semblent surtout être des commodités pour appeler des fonctions de façon particulière, et ne pas atteindre le niveau des éloges faits à Lisp, où « l’introspection » est traitée comme une préoccupation de premier ordre
J’aimerais savoir ce que Lisp apporte concrètement qui manque à ces fonctionnalités
Ils utilisent Lisp comme moyen de transmettre des concepts, et ces concepts tournent généralement autour de la récursion fonctionnelle
Scheme et la culture qui l’entoure y sont aussi pour quelque chose
Scheme n’est pas uniquement un langage fonctionnel, mais il met davantage l’accent sur la programmation pure que les familles Lisp précédentes ; son langage de base fournit des structures de récursion terminale plutôt que des structures d’itération, et il exige des implémentations l’optimisation des appels terminaux
Même en Common Lisp, si l’on définit
defaliascomme une macro, on peut l’utiliser comme ledefaliasd’Emacs : https://stackoverflow.com/questions/24252539/defining-aliase...J’avais aimé l’article de Hofstadter sur Lisp paru dans Metamagical Themas, et j’ai adapté et mis au propre le code du dernier article de cette série en Clojure pour un groupe d’étude au travail
[1] http://johnj.com/posts/oodles/