Journal LIPS : Lisp dans le navigateur

Posté par  (site web personnel) . Licence CC By‑SA.
6
4
mar.
2023

Pour mon projet actuel, je me suis retrouvé à devoir exécuter du lisp dans le navigateur¹. Plein d'optimisme, j'ai dégainé mon moteur de recherche préféré pour voir si il n'y avait pas un malade qui aurait déjà eu le même besoin que moi et bricolé un truc. Et ben il semblerait que plein de gens se soient penché sur la question.

Sélection de projets au hasard :

Pour l'instant, je n'ai joué qu'avec le premier. Les performances sont catastrophiques, mais il est très facile de dessiner dans un canvas webgl :

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Demo LIPS</title>
    <script src="https://cdn.jsdelivr.net/npm/@jcubic/lips@beta/dist/lips.min.js"></script>
    <script type="text/x-scheme" bootstrap>
(let 
    ((canvas_node (document.getElementById "canvas")))
    (let 
        ((ctx (canvas_node.getContext "2d")))
        (do-iterator 
            (y (range canvas.height)) 
            ()
            (do-iterator 
                (x (range canvas.width)) 
                ()
                (begin 
                    (set! ctx.fillStyle (format 
                        "rgb(~a, ~a, 255)" 
                        (/ (* x 255) canvas.width) 
                        (/ (* y 255) canvas.width)))
                    (ctx.fillRect x y 1 1))))))
    </script>
</head>

<body>
    <canvas id="canvas" width="640" height="480"></canvas>
</body>

</html>

1: je bosse sur des trucs bizarres…

  • # Pourquoi lisp dans le navigateur ?

    Posté par  (site web personnel) . Évalué à 4. Dernière modification le 04 mars 2023 à 21:31.

    Ben je trouve que ça fait un format super élégant pour déclarer les scènes à rendre avec mon raytraceur :

    (scene 
        ; The camera
        (camera 
            ; Position
            (vector3 0 0 0)
            ; View port 
            (vector3 1 1 1))
        ; The root node 
        (union (list 
            ; The floor
            (sphere 
                (vector3 0 -5001 0)
                5000
                (material 
                    (color 1 1 0)
                    1000))
            ; Red sphere 
            (sphere 
                (vector3 0 -1 3)
                1
                (material 
                    (color 1 0 0)
                    500)) 
            ; Blue sphere
            (sphere 
                (vector3 2 0 4)
                1
                (material 
                    (color 0 0 1)
                    500)) 
            ; Green sphere
            (sphere 
                (vector3 -2 0 4)
                1
                (material 
                    (color 0 1 0)
                    10)))) 
        ; The lights
        (list 
            ; An ambiant light
            (ambiant
                0.2) 
            ; An omnidirectional light
            (omnidirectional
                0.6
                (vector3 2 1 0)) 
             ; A directional light
            (directional
                0.2
                (vector3 1 4 4))))

    La possibilité de profiter de la puissance du langage pour faire de la génération procédurale facile de scènes est un énorme plus.

  • # Et pourquoi pas Web Assembly ?

    Posté par  (Mastodon) . Évalué à 4.

    Je n'y connais rien et j'ai sans doute mal compris ton besoin mais ma première réaction a été de me demander pourquoi ne pas utiliser Web Assembly ? Je veux traduire ton code en WASM avant de le faie exécuter dans le browser (ce que ne semble pas faire lips-rs-wasm)

    Est-ce parce qu'en fait, c'est autre chose que tu veux faire autre chose et que je n'ai pas compris ?
    Ou est-ce parce qu'il n'y pas pas encore de solution viable pour créer du WASM à partir de LISP ?

    Surtout, ne pas tout prendre au sérieux !

    • [^] # Re: Et pourquoi pas Web Assembly ?

      Posté par  (site web personnel) . Évalué à 2.

      Je n'y connais rien et j'ai sans doute mal compris ton besoin mais ma première réaction a été de me demander pourquoi ne pas utiliser Web Assembly ?

      ma réflexion a été "je veux lire des s-expr dans le navigateur", j'ai tapé "lisp in the browser" dans mon moteur de recherche préféré, et j'ai cliqué sur lips. Je n'ai pas poussé plus loin ma réflexion ou mes recherches :D

      Mais effectivement, ça peut-être une piste intéressante.

      Ou est-ce parce qu'il n'y pas pas encore de solution viable pour créer du WASM à partir de LISP ?

      Absolument aucune idée ! J'ai touché à lisp pour la première fois de ma vie cette semaine.

      Je suis en train d'écrire un raytracer. Actuellement, il est en rust, utilise sdl pour faire le rendu et les scènes sont décrites en YAML. Mon but est à terme de le faire tourner dans le navigateur pour que les gens puissent facilement jouer avec et faire des scènes avec des maths dans ce style. Et je trouvais que lisp faisait un format de donnée élégant pour intégrer la logique nécessaire à l'écriture de ce genre de scènes pleines de maths, plus flexible que yaml et plus simple d'approche que les shaders webgl.

      Donc là, à la base, je cherchais juste à lire une scène lisp depuis mon rust. Puis je me suis un peu égaré quand j'ai vu que je pouvais exécuter le lisp directement depuis le navigateur.

      Et ça remet plein de choses en question. Parce que en fait, j'aime bien lisp, et j'aimerai bien ré-écrire le moteur dans ce langage. Mais d'un autre côté, j'ai déjà une base de code fonctionnelle dans un langage que je maîtrise mieux et qui je sais supporte wasm.

    • [^] # Re: Et pourquoi pas Web Assembly ?

      Posté par  . Évalué à 3.

      J’ai l’impression qu’aujourd’hui en terme d’outillage et de performance c’est plus simple d’utiliser un LISP qui transpile vers du Javascript plutôt que WASM. Après y’a des trucs qui tentent de le faire, mais ç’a l’air de rester expérimental :

      https://github.com/schism-lang/schism

      Et sinon y’a ce compilateur Common Lisp qui est intéressant parce qu’il compile vers LLVM et interopère très bien avec C++ (et donc il est en principe possible de génénrer du WASM depuis la sortie LLVM je suppose) :

      https://clasp-developers.github.io/

      • [^] # Re: Et pourquoi pas Web Assembly ?

        Posté par  (site web personnel) . Évalué à 2.

        Et sinon y’a ce compilateur Common Lisp qui est intéressant parce qu’il compile vers LLVM et interopère très bien avec C++ (et donc il est en principe possible de génénrer du WASM depuis la sortie LLVM je suppose) :

        J'aime beaucoup ! Est-ce que tu sais si ça marcherai avec parenscript mentionné par le camarade Leirda< plus bas ? Je trouve leur gestion du dom très élégante !

        • [^] # Re: Et pourquoi pas Web Assembly ?

          Posté par  . Évalué à 1.

          Hello!

          Est-ce que tu sais si ça marcherai avec parenscript mentionné par le camarade Leirda< plus bas ?

          Alors… Ce que je peux dire avec certitude, c’est que Parenscript et Clasp sont tous deux des compilateurs du même langage : Common Lisp.

          Cela signifie que dans une moindre mesure tu devrais pouvoir utiliser l’un ou l’autre des compilateurs sur les mêmes sources et obtenir des binaires qui, exécutés dans leurs environnements respectifs (le navigateur ou un binaire pour ta machine), feront le même travail.

          Cependant, tu vas très vite rencontrer des limites à ça car chacun d’entre eux ont leurs spécificités (e.g Parenscript et ses outils de manipulation du DOM, et Clasp avec ses appels natifs vers du C++) qui ne seront pas supportés par les autres compilateurs.

          Donc en principe oui, mais en pratique je dirais plutôt non, sauf si tu n’utilises que des fonctionnalités de base, ce qui limite beaucoup l’intérêt.

      • [^] # Re: Et pourquoi pas Web Assembly ?

        Posté par  . Évalué à 2.

        Alors pour Common Lisp + Web Assembly on a un support qui est en train d'apparaître dans l'implémentation ECL (Embedable Common Lisp, qui génère du C): https://gitlab.com/embeddable-common-lisp/ecl/-/merge_requests/277/ Il est évidemment tôt, il faut être motivé pour essayer.

        Ce sera sûrement possible dans CLASP en effet, mais j'ai pas entendu que ce soit à l'ordre du jour.

  • # et parenscript ?

    Posté par  . Évalué à 2.

    https://parenscript.common-lisp.dev/

    Alors c’est plus proche de Common Lisp que d’un Scheme par contre (en fait, c’est littéralement Common Lisp), mais pourquoi pas ?

    • [^] # Re: et parenscript ?

      Posté par  (site web personnel) . Évalué à 2.

      N'ayant aucune connaissance des deux langages, et donc préjugés, et encore peu de code en scheme, si ça marche facilement et avec de bonne perfs, je dis pourquoi pas !

      • [^] # Re: et parenscript ?

        Posté par  . Évalué à 3.

        Je pense que c’est une des solutions les plus matures pour le moment.

        Common Lisp c’est un standard pour lequel il existe plusieurs compilateurs (SBCL, ECL, Clasp, Parenscript, etc).

        Le langage se veut à la fois permettre des abstractions « haut-niveau », mais permet également de mettre les mains dans le cambouis quand c’est nécessaire. Il propose notamment un système d’objet (par prototypes) CLOS (Common Lisp Object System), une gestion d’erreurs très avancée, et l’écosystème est remarquablement mature.

        Scheme c’est également un standard (ou plutôt un ensemble de standards à différentes versions : on doit en être à R7RS) pour lequel il existe aussi plusieurs compilateurs (Chez, Chibi, Chicken, MIT Scheme, Guile, etc).

        Le langage se veut minimaliste, contrairement à CL. Les standards permettent un peu moins à mon goût de mettre les mains dans le cambouis, mais l’approche fonctionnelle est souvent plus mise en avant. Il ne propose pas de système d’objet par défaut (mais plusieurs bibliothèques existent pour faire ça), mais il propose un système de macros hygiéniques (qui ne brisent pas la frontière entre le code à la compilation et le code à l’exécution), et les SRFI (Scheme Request For Implementation) qui permettent d’étendre amplement ce que l’on peut faire avec le langage.

        Je trouve par ailleurs qu’il est plus simple d’utiliser différentes implémentation sur les mêmes sources en Scheme qu’en Common Lisp.

        Je dirai qu’une des principales différences entre un Scheme et un Common Lisp, c’est la séparation des espaces de noms entre les fonctions et les variables.

        En scheme, il n’y a pas de différence entre define une fonction ou une variable :

        (define (add x y) (+ x y))
        (add 2 1) ;=> 3
        

        est rigoureusement équivalent à :

        (define add (lambda (x y) (+ x y))
        (add 2 1) ;=> 3
        

        Common Lisp, quant à lui, utilise bien deux mots-clés distincts :

        (defun add (x y) (+ x y))
        (add 2 1) ;=> 3
        

        n’est pas du tout pareil à :

        (defvar add (lambda (x y) (+ x y)))
        (add 1 2) ;=> Erreur, "undefined function"
        (funcall add 1 2) ;=> 3
        

        funcall nous permet de considérer le contenu d’une variable comme une référence vers une fonction (ou une lambda), et de l’appeler en tant que tel. On n’a pas du tout besoin de cette notion avec un Scheme.

        • [^] # Re: et parenscript ?

          Posté par  . Évalué à 4.

          Je trouve par ailleurs qu’il est plus simple d’utiliser différentes implémentation sur les mêmes sources en Scheme qu’en Common Lisp.

          Étonnant ! (généralement c'est le sentiment inverse, l'écosystème des Scheme est très fragmenté, alors que les implémentations de CL partagent un standard).

          c’est la séparation des espaces de noms entre les fonctions et les variables.

          Franchement je trouve que ce n'est pas important. Et si jamais tu veux vraiment utiliser une variable comme fonction sans utiliser funcall, c'est possible:

          CL-USER> (defvar add (lambda (x y) (+ x y)))
          ADD
          CL-USER> (setf (symbol-function 'add) (lambda (x y) (+ x y)))
          #<FUNCTION (LAMBDA (X Y)) {52DCC43B}>
          CL-USER> (add 1 2)
          3

          ps: pour les nouvelles et nouveaux venu·es: https://github.com/CodyReichert/awesome-cl

          • [^] # Re: et parenscript ?

            Posté par  . Évalué à 4. Dernière modification le 06 mars 2023 à 08:57.

            oups, du coup dans la première ligne pas la peine du lambda, ça aurait pu être (defvar add "add (variable)").

            Quand on inspecte un symbole:

            CL-USER> (inspect 'add)
            
            The object is a SYMBOL.
            0. Name: "ADD"
            1. Package: #<PACKAGE "COMMON-LISP-USER">
            2. Value: "add (variable)"
            3. Function: #<FUNCTION (LAMBDA (X Y)) {52DCC43B}>
            4. Plist: NIL

            on se rend compte qu'il comporte plusieurs "slots", dont un nom, une valeur, une fonction.

            Si je prends une fonction au hasard:

            CL-USER> (inspect 'search)
            
            The object is a SYMBOL.
            0. Name: "SEARCH"
            1. Package: #<PACKAGE "COMMON-LISP">
            2. Value: "unbound"
            3. Function: #<FUNCTION SEARCH>
            4. Plist: NIL

            le symbole "search" n'a pas de valeur dans l'espace des variables, mais il est lié à une fonction.

          • [^] # Re: et parenscript ?

            Posté par  . Évalué à 1.

            Merci pour ces infos !

            Je trouve que les espaces de noms variables/fonctions c’est un exemple qui démontre assez bien en quoi Scheme et CL sont différents dans leurs philosophies, mais ça reste anecdotique (et merci pour symbol-function!)

            Je pense que tu as raison, c’est sûrement assez simple de changer de compilo sur CL tant que le standard est respecté, et pour Scheme c’est moins évident et il faut faire attention pour que ça se joue bien…

  • # Clojurescript

    Posté par  . Évalué à 4.

    Je te suggère d'essayer clojurescript

    Au besoin tu peux l'utiliser en conjonction avec Clojure côté serveur.

  • # let*

    Posté par  . Évalué à 3.

    Hello, ça a l'air pas mal LIPS, merci du rappel (sauf le nom, à mon humble avis, trop similaire).

    Tu aurais pu utiliser let*, à la place des 2 let successifs:

    (let 
        ((canvas_node (document.getElementById "canvas")))
        (let 
            ((ctx (canvas_node.getContext "2d")))

    =>

    (let* ((canvas_node ())
           (ctx (canvas_node.getContext )))
      )

    (j'ai testé sur la démo que let* fonctionne.

Suivre le flux des commentaires

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