Journal C'est décidé, j'apprends Common Lisp!

Posté par (page perso) . Licence CC by-sa
34
6
oct.
2017

Il y a trois semaines j'ai décidé d'apprendre Common Lisp, motivé principalement par la curiosité et attiré par l'approche assez différente de la programmation qu'a Common Lisp par rapport à des langages d'autres familles. Je vous raconte ma vie, des fois que vous ayiez aussi envie d'apprendre ce langage et vouliez gagner du temps avec les premier pas.

Mon profil. Je suis mathématicien de formation (cursus math/info, calcul scientifique puis géométrie algébrique) et je travaille depuis 6 ans avec beaucoup de programmation dans des rôles variés – et je programmais déjà auparavant en hobby. Je connais pas mal de langages (OCaml, C, Bourne Shell, JavaScript, TeX) et j'ai déjà écrit des programmes substantiels dans d'autres langages (C++, Python 2.X, perl, PHP, assembleur 8086) donc je ne suis plus tout à fait un débutant en programmation. (Surtout des simulations numériques, du GUI, du traitement de données géographiques, de l'automatisation de traitement de Cloud, des traitements de données.)

Mon premier environnement. J'ai choisi d'utiliser SBCL, une implémentation libre largement disponible sur les systèmes actuels et Emacs/SLIME comme environnement de développement. Dans Emacs la commande slime permet de démarrer une boucle d'évaluation, ce qui est suffisant pour commencer à expérimenter.

Mon premier livre pour Common Lisp. Après avoir évalué les ressources suggérées sur reddit j'ai décidé de prendre le livre de Paul Graham, ANSI Common Lisp. Je l'ai préféré à un autre choix apparemment populaire, le livre de Peter Seibel Practical Common Lisp parceque ce dernier ne propose pas systématiquement des exemples complets (il utilise beaucoup de “supposons que f soit une fonction qui fasse ceci-cela”) alors que Graham ne donne que des exemples qu'on peut réellement utiliser dans la boucle d'évaluation, ce qui est très chouette pour expérimenter et apprendre de façon interactive. A posteriori je suis encore plus content de ce choix parceque le livre de Graham peut aussi être utilisé comme référence, par exemple si on veut se remettre en mémoire les détails de la façon de définir une fonction il suffit de relire le chapitre “Fonctions.” De ce côté là le livre de Seibel est un peu moins pratique mais c'est néanmoins une seconde lecture très recommendable. Le livre de Graham n'est pas trop recommandé pour les débutants en programmation, notamment à cause de son utilisation de mnémoniques un peu abstruses pour nommer les variables et les fonctions, ce qu'on considère aujourd'hui comme un exemple à ne pas suivre.

En me prenant une demi-journée par jour et en faisant un peu de rab le week-end, il m'a fallu une grosse semaine pour faire le tour du livre.

Le seul gros défaut du livre de Graham est qu'il manque quelque peu d'exemples substantiels. Dans le cœur du livre le seul gros exemple est un petit ray tracer – c'est assez fun! Dans les derniers chapitres, il y a un petit système de déductions formelles, un générateur de HTML (cela fait délicieusement rétro aujourd'hui) et une implémentation de la logique objets (le method dispatching). Un des meilleurs livres est le livre sur “Le langage Caml” qui présente plein d'exemples ambitieux (compression de données, mini machine virtuelle, un assembleur et un compilateur Pascal pour n'en citer que quelques uns!)

Aprés le livre. Une fois les bases connues, il est grand temps d'affiner ses connaissances! Des pistes à explorer toutes en même temps sont l'essai de bibliothèques, la lecture de leur code, l'apprentissage de SLIME, notamment grâce à la vidéo de Marco, puis de quicklisp et l'organisation d'un projet (voir les exemples de bibliothèques sur par GitHub et dans la base quicklisp).

Pour ma part je me suis arrêté sur JSCL un compilateur Lisp vers JavaScript et je voudrais y ajouter une intégration de React, pour pouvoir programmer des GUIs React en Lisp.

Amusez-vous bien!

  • # C'est pas common

    Posté par . Évalué à 4 (+4/-0).

    Mais c'est bien!

    Comme disait Pierre Dac :

    Il vaut mieux prendre ses désirs pour des réalités que de prendre son lisp pour une tasse à café.

  • # Lisp + React

    Posté par (page perso) . Évalué à 3 (+2/-0).

    Pour ma part je me suis arrêté sur JSCL un compilateur Lisp vers JavaScript et je voudrais y ajouter une intégration de React, pour pouvoir programmer des GUIs React en Lisp.

    C'est pas un peu ce que fait https://github.com/omcljs/om avec ClojureScript ?

    • [^] # Re: Lisp + React

      Posté par (page perso) . Évalué à 4 (+2/-0).

      C'est pas un peu ce que fait https://github.com/omcljs/om avec ClojureScript ?

      Apparemment oui, mais il ne faut pas oublier que Clojure est un dialecte de Lisp, donc la syntaxe est légèrement différente,.

      • [^] # Lisp + Ecma

        Posté par (page perso) . Évalué à 2 (+0/-0). Dernière modification le 14/10/17 à 06:35.

        Clojure est un dialecte de Lisp

        JS aussi (et Scala itou) du coup, autant y aller carrément ; c'est ce que je fais en tout cas, et les concepts appris en bidouillant du (Emacs plutôt que CL à chaque fois que c'est possible, c'est à dire tout le temps) Lisp me (ma vie est trépidante tavu) servent bien en JS.

        • [^] # Re: Lisp + Ecma

          Posté par (page perso) . Évalué à 3 (+1/-0). Dernière modification le 14/10/17 à 16:41.

          Entre dire que JavaScript est plus proche de Lisp que de Java et dire que c'est un dialecte de Lisp, il y a quand-même un petit monde que je ne franchirais pas! Au-delà de la syntaxe toute différente, les différences notables sont:

          1. La méta-programmation, où on a du côté de JavaScript des possibilités d'introspection très développées et une fonction eval (qu'on essaie de ne pas utiliser) et de celui de Lisp un système de macros très solide, avec notamment loop et do.

          2. Des approches assez différentes pour la programmation par objets.

          3. La portée des variables obéit à des mécanismes différents: le variable hoisting, le mot-clef this et la seule portée lexicale des variables pour JavaScript le distinguent de Lisp.

          En revanche. un gros point commun de Lisp et JavaScript, est l'accent très fort mis sur les structures de données et l'interactivité, ce qui pousse assez naturellement le programmeur à écrire des fonctions très découplées des autres, très faciles à tester.

          Le livre de Crockford, auteur de l'article que tu cites, est une excellente introduction à JavaScript.

  • # Petite parenthèse...

    Posté par . Évalué à 5 (+3/-0).

    Merci pour ces quelques pistes pour commencer. Tu m'as donné envie de me joindre à l'aventure même si ce n'est pas pour tout de suite : je viens de commencer un projet dans un autre langage. Je garde ton journal en mémoire.

    Par contre on sent que tu débutes, tu as encore des paragraphes sans parenthèses ;-)

    Bon courage dans ton aventure !

    (Et ne t'inquiète pas, même après 12-13 ans d'utilisation intensive d'emacs, mes doigts se portent encore plus ou moins bien..)

  • # Oui mais non

    Posté par . Évalué à 4 (+2/-0).

    Salut,
    ravi de lire cela, moi-même investissant du temps pour découvrir le langage et son écosystème (cf mon journal https://linuxfr.org/users/dzecniv/journaux/decouvrons-common-lisp-comparaison-avec-l-environnement-python)

    Mais tu n'y es pas du tout ! Google n'est toujours pas bon en CL donc on doit le nourrir à la becquée. Il faut rediriger vers http://lisp-lang.org/ pour mettre l'eau à la bouche aux lecteurs et lectrices, puis citer https://github.com/CodyReichert/awesome-cl pour éventuellement évoquer Cliki (j'aime pas Cliki). Ensuite moi je cite toujours le Common Lisp Cookbook, la version sur github, vachement améliorée cette année.

    Ensuite as-tu vu Panic, une preuve de concept qui fait marcher React ?
    Pour ce qui est d'un framework web "dynamique", je parie et m'intéresse fort à Weblocks. Mais à la version d'un dév en train de le factoriser et moderniser, dont l'exemple pour faire un TodoMVC est très clair et très simple: http://40ants.com/weblocks/quickstart.html On construit donc une page interactive en pure lisp, sans écrire d'appels Ajax, où ils sont backportés en pur html si besoin, avec l'avantage d'un environnement Lisp (détection d'erreurs, debuggeur, création d'un exécutable,…). Mon idéal en passe de se réaliser c'est de coder une app web en un langage, avec la même expérience de dév et débug par conséquent, de construire des exécutables prêts à être déployés, d'explorer leur runtime en live, de déployer "à chaud" et de fournir une version de bureautique via Electron (Ceramic en CL). On y est quasiment !

    Fais-nous part de tes avancées par ici !

    ps: cf aussi mon tuto en cours de finition (web scraping, création d'exécutables, tests,…)

    pps: vidéo de Marco illisible "fichier corrompu" :(

    • [^] # Re: Oui mais non

      Posté par (page perso) . Évalué à 2 (+0/-0).

      Merci pour toutes ces précisions. J'avais vu Weblocks, Panic et Parenscript

      L'impression que m'a fait Weblocks est qu'il implémente l'état de l'art de la programmation web d'il y a environ 5-10 ans – ce qui est très impressionnant mais on s'en est aujourd'hui largement détourné. Est-il exact que le modèle proposé par Weblocks produit un couplage très fort entre l'UI, la logique de l'application et la base de données? Si c'est le cas c'est précisément le modèle dont on s'est radicalement éloigné ces dernières années, en faisant une UI qui communique avec des microservices via une API explicite (p.ex. en signant les requêtes, ce qui dans certains cas élimine pratiquement le besoin de maintenir une session, ce qui simplifie beaucoup la logique de l'application!), ce qui permet d'utiliser les fonctions de l'application web en RPC JSON/HTTPS et permet aux développeurs de la GUI et ceux de l'application de travailler de façon complètement découplée. (Notamment en testant les composants de la GUI avec des storybooks au lieu de devoir accéder à un environnement complet.)

      Ceci dit Weblocks semble très impressionnant – et ma première impression est peut-être fausse. La documentation que tu proposes en lien ne va pas beaucoup plus loin que l'exemple minimal de la TODO liste, est-ce que tu as d'autres lectures sur Weblocks à recommander?

      • [^] # Re: Oui mais non

        Posté par . Évalué à 2 (+0/-0).

        Non malheureusement pas d'autres lectures sur Weblocks à proposer, l'ancien site n'a pas grand chose.

        Effectivement Weblocks produit un couplage comme tu l'as décrit, mais à l'inverse de ce qu'on pourrait conclure avec les exemples qui mélangent allégrement logique, template et style, on peut faire du MVC en mettant ce qu'il faut dans leurs propres fichiers et modules. Néanmoins, à ce que je comprends un bénéfice de l'approche de Weblocks serait d'écrire une app web de manière plus linéaire, grâce à son système basé sur les continuations (qui gère la session et pour lequel le développeur n'a besoin de connaître que 2 ou 3 macros).

  • # Perlissade

    Posté par . Évalué à 7 (+5/-0).

    Et alors, est-ce que Common Lisp a changé la façon dont tu penses à la programmation ?

    • [^] # Re: Perlissade

      Posté par (page perso) . Évalué à 4 (+2/-0).

      Pas vraiment la façon dont je la pense mais comme programmeur OCaml expérimenté je peux dire que le cycle de développement avec Common Lisp est complètement différent de ce qu'il est avec OCaml.

      La raison principale est la proximité de l'évaluation du programme avec l'écriture de celui-ci. En OCaml on peut utiliser la REPL pour mettre au point une fonction mais c'est assez compliqué à mettre en place parcequ'il faut écrire un .ocamlinit spécifique au projet qui charge les modules nécessaires au module où vit notre fonction et installe les pretty-printers. Si on se rend compte qu'on a besoin du débogueur, il faut refaire toute cette configuration avec le débogueur. Enfin, une fois qu'on est content de sa fonction on peut continuer la mise au point avec une autre fonction d'un autre module et cela demande d'ajuster les fichiers de configuration pour le toplevel ou le débogueur à la main. En Common Lisp tout ce petit yoga disparaît, ce qui est du à l'association du débogueur à la REPL et à la portée dynamique des variables. Ce type de développement très interactif est semble-t-il un des points forts de Common Lisp et la norme parmi les programmeurs, c'est en tout cas une façon très agréable de travailler.

      Même si les PPE de OCaml sont très puissants et relativement faciles à utiliser – moyennant compilation et installation, puis paramétrage de la REPL pour les utiliser, et je ne sais pas si le débogueur les prend en charge – il faut bien reconnaître que la fonction équivalente en Common Lisp fournie par les macros est, en comparaison, d'utilisation enfantine.

      Un point sur lequel OCaml et Common Lisp se rapprochent est qu'en dépit de leur expressivité ces langages restent assez proches de la machine, ils laissent entrevoir très nettement les rouages de la machine.

      Comme en OCaml, le code assembleur généré (ici par SBCL) est très lisible:

      (defun my-average (numbers)
                 (do ((tail numbers (rest tail))
                      (n 0 (+ 1 n))
                      (s 0 (+ s (first tail))))
                     ((null tail) (if (eql n 0) 0 (/ s n)))))
      
      
      (disassemble 'my-average)
      ; disassembly for MY-AVERAGE
      ; Size: 182 bytes. Origin: #x100718AAA8
      ; AA8:       498B4C2460       MOV RCX, [R12+96]               ; thread.binding-stack-pointer
                                                                    ; no-arg-parsing entry point
      ; AAD:       48894DF8         MOV [RBP-8], RCX
      ; AB1:       488B5DF0         MOV RBX, [RBP-16]
      ; AB5:       31C9             XOR ECX, ECX
      ; AB7:       31F6             XOR ESI, ESI
      ; AB9:       EB61             JMP L1
      ; ABB:       0F1F440000       NOP
      ; AC0: L0:   488BD3           MOV RDX, RBX
      ; AC3:       8D43F9           LEA EAX, [RBX-7]
      ; AC6:       A80F             TEST AL, 15
      ; AC8:       0F8588000000     JNE L4
      ; ACE:       488B4201         MOV RAX, [RDX+1]
      ; AD2:       488945E8         MOV [RBP-24], RAX
      ; AD6:       488975E0         MOV [RBP-32], RSI
      ; ADA:       48895DD8         MOV [RBP-40], RBX
      ; ADE:       BF02000000       MOV EDI, 2
      ; AE3:       488BD1           MOV RDX, RCX
      ; AE6:       41BBB0020020     MOV R11D, #x200002B0            ; GENERIC-+
      ; AEC:       41FFD3           CALL R11
      ; AEF:       4C8BC2           MOV R8, RDX
      ; AF2:       4C8945D0         MOV [RBP-48], R8
      ; AF6:       488B5DD8         MOV RBX, [RBP-40]
      ; AFA:       488B75E0         MOV RSI, [RBP-32]
      ; AFE:       488B7BF9         MOV RDI, [RBX-7]
      ; B02:       488BD6           MOV RDX, RSI
      ; B05:       41BBB0020020     MOV R11D, #x200002B0            ; GENERIC-+
      ; B0B:       41FFD3           CALL R11
      ; B0E:       4C8B45D0         MOV R8, [RBP-48]
      ; B12:       488B5DE8         MOV RBX, [RBP-24]
      ; B16:       498BC8           MOV RCX, R8
      ; B19:       488BF2           MOV RSI, RDX
      ; B1C: L1:   4881FB17001020   CMP RBX, 537919511
      ; B23:       759B             JNE L0
      ; B25:       4885C9           TEST RCX, RCX
      ; B28:       7508             JNE L3
      ; B2A:       31D2             XOR EDX, EDX
      ; B2C: L2:   488BE5           MOV RSP, RBP
      ; B2F:       F8               CLC
      ; B30:       5D               POP RBP
      ; B31:       C3               RET
      ; B32: L3:   488BD6           MOV RDX, RSI
      ; B35:       488BF9           MOV RDI, RCX
      ; B38:       4883EC18         SUB RSP, 24
      ; B3C:       48896C2408       MOV [RSP+8], RBP
      ; B41:       488D6C2408       LEA RBP, [RSP+8]
      ; B46:       B904000000       MOV ECX, 4
      ; B4B:       41BBE03CB021     MOV R11D, #x21B03CE0            ; #<FUNCTION SB-KERNEL:TWO-ARG-/>
      ; B51:       41FFD3           CALL R11
      ; B54:       EBD6             JMP L2
      ; B56: L4:   0F0B0A           BREAK 10                        ; error trap
      ; B59:       2F               BYTE #X2F                       ; OBJECT-NOT-LIST-ERROR
      ; B5A:       10               BYTE #X10                       ; RDX
      ; B5B:       0F0B10           BREAK 16                        ; Invalid argument count trap
      

      Un examen succinct montre que L1 est notre if final et le reste de la structure s'en déduit facilement. Pour une version un peu plus “bas niveau” peut être écrite en ajoutant des annotations:

      CL-USER> (defun my-average (numbers)
                 (let ((s 0)
                       (n (length numbers)))
                   (declare (optimize (speed 3) (safety 0)))
                   (declare (type fixnum n s))
                   (dotimes (i n)
                     (incf s (the fixnum (svref numbers i))))
                   (if (eql n 0) (the fixnum 0) (the fixnum (nth-value 0 (floor s n))))))
      WARNING: redefining COMMON-LISP-USER::MY-AVERAGE in DEFUN
      MY-AVERAGE
      CL-USER> (disassemble 'my-average)
      ; disassembly for MY-AVERAGE
      ; Size: 149 bytes. Origin: #x10056CE504
      ; 04:       498B442460       MOV RAX, [R12+96]                ; thread.binding-stack-pointer
                                                                    ; no-arg-parsing entry point
      ; 09:       488945F8         MOV [RBP-8], RAX
      ; 0D:       4C8945F0         MOV [RBP-16], R8
      ; 11:       498BD0           MOV RDX, R8
      ; 14:       4883EC18         SUB RSP, 24
      ; 18:       48896C2408       MOV [RSP+8], RBP
      ; 1D:       488D6C2408       LEA RBP, [RSP+8]
      ; 22:       B902000000       MOV ECX, 2
      ; 27:       41BB6000B021     MOV R11D, #x21B00060             ; #<FUNCTION LENGTH>
      ; 2D:       41FFD3           CALL R11
      ; 30:       488BDA           MOV RBX, RDX
      ; 33:       4C8B45F0         MOV R8, [RBP-16]
      ; 37:       31FF             XOR EDI, EDI
      ; 39:       31C0             XOR EAX, EAX
      ; 3B:       EB12             JMP L1
      ; 3D:       0F1F00           NOP
      ; 40: L0:   498B4C8001       MOV RCX, [R8+RAX*4+1]
      ; 45:       4801F9           ADD RCX, RDI
      ; 48:       488BF9           MOV RDI, RCX
      ; 4B:       4883C002         ADD RAX, 2
      ; 4F: L1:   4839D8           CMP RAX, RBX
      ; 52:       7CEC             JL L0
      ; 54:       4885DB           TEST RBX, RBX
      ; 57:       750B             JNE L3
      ; 59:       31FF             XOR EDI, EDI
      ; 5B: L2:   488BD7           MOV RDX, RDI
      ; 5E:       488BE5           MOV RSP, RBP
      ; 61:       F8               CLC
      ; 62:       5D               POP RBP
      ; 63:       C3               RET
      ; 64: L3:   488BF7           MOV RSI, RDI
      ; 67:       4885DB           TEST RBX, RBX
      ; 6A:       7427             JEQ L6
      ; 6C:       488BC7           MOV RAX, RDI
      ; 6F:       4899             CQO
      ; 71:       48F7FB           IDIV RAX, RBX
      ; 74:       488D3C00         LEA RDI, [RAX+RAX]
      ; 78:       4885D2           TEST RDX, RDX
      ; 7B:       7502             JNE L5
      ; 7D: L4:   EBDC             JMP L2
      ; 7F: L5:   4885F6           TEST RSI, RSI
      ; 82:       7DF9             JNL L4
      ; 84:       48D1FF           SAR RDI, 1
      ; 87:       4883EF01         SUB RDI, 1
      ; 8B:       48D1E7           SHL RDI, 1
      ; 8E:       EBED             JMP L4
      ; 90:       0F0B10           BREAK 16                         ; Invalid argument count trap
      ; 93: L6:   0F0B0A           BREAK 10                         ; error trap
      ; 96:       09               BYTE #X09                        ; DIVISION-BY-ZERO-ERROR
      ; 97:       39               BYTE #X39                        ; RDI
      ; 98:       19               BYTE #X19                        ; RBX
      

      L'assembleur est un peu moins facile à suivre (pour moi) mais on voit quand-même que le seul appel à une fonction (remote call) est celui fait à length pendant l'initialisation de la boucle et que dans la boucle, la seule instruction qui accède à la mémoire est celle en position F00 tout le reste ne consiste qu'en des opérations très rapides sur les registres et des sauts courts. À la fin on nettoie la pile – seule la partie après le IDIV reste pour moi très mystérieuse, mais la boucle critique est simple et claire, même avec mes connaissances rudimentaires en assembleur.

  • # Je déteste CL

    Posté par . Évalué à 3 (+2/-0).

    Je déteste CL. C'est une horreur, l'une des pires choses qui soient arrivé à lisp et à la programmation fonctionnelle.

    Lisp, c'est super beau, on est d'accord. Mais les lisp-1 sont beaux, pas les 2 ! Pour les non lispeurs, la différence c'est la porté des variables. Elle est syntaxique sur les lisp-1, et FUCK YOU sur les lisp-2. Il y a aussi la manière dont sont traitées les fonctions, comme n'importe quelle autre valeur sur les lisp-1, et comme FUCK YOU sur les lisp-2.

    Après, il y a la lib… La convention de nommage utilisée c'est FUCK YOU (plus les exemples précis en tête, mais la convention de de préfixer les fonctions non pures par un "n" n'est pas systématiquement utilisée).

    Après, il y a les macros, et les horreurs qu'on peut faire avec… Genre… Tiens, pourquoi pas faire de la Programmation Orientée Objet (💩) ? Et vous vous retrouvez avec un clusterfuck de méta programmation… Je vous laisse deviner la super méthode pour déboguer tout ça, ça commence par un F.

    Bref:
    Lisp, oui.
    Common Lisp, non.

    • [^] # Re: Je déteste CL

      Posté par . Évalué à 1 (+1/-0).

      La difficulté, c'est que les très beaux langages comme Scheme, GNU Guile… ont une utilité limité pour écrire des programmes génériques, on est plus dans la preuve de concept ou l'outil de théoricien. Ce que je regrette.
      CL est comme le C++ : atroce, "méta", complexe, plein d'inconsistances et de subtilités, mais tu écris ce que tu veux avec.

      Bon, on me fera remarquer que Scheme, c'est comme le C : un langage pur, minimaliste, puissant. Sauf qu'en C, on trouve des bindings pour tout (GTK, boites à outils…), en Scheme, rien. Et la performance n'est pas la même non plus.

      • [^] # Re: Je déteste CL

        Posté par (page perso) . Évalué à 5 (+4/-0).

        Niveau Scheme, il y a Racket, où il me semble qu'il y a quand même pas mal de choses de disponibles.

        Perso, je m'étais pas mal intéressée à Clojure, mais autant l'intégration à la JVM peut être un avantage pour utiliser plein de bibliothèques Java sans (trop) se prendre la tête, autant ça limite aussi si tu préférerais plutôt proposer un « vrai binaire » (notamment, je sais pas si ça a a évolué dernièrement, mais les temps de lancement étaient assez rédhibitoires à mon goût pour un programme en ligne de commande qui est censé s'exécuter plus ou moins instantanément).

    • [^] # Re: Je déteste CL

      Posté par (page perso) . Évalué à 7 (+5/-0).

      Quelles implémentations recommandes-tu ?

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.