RSS Feed

Lisp Project of the Day

parenscript

You can support this project by donating at:

Donate using PatreonDonate using Liberapay

Or see the list of project sponsors.

parenscriptweb

Documentation😀
Docstrings😀
Tests 😀
Examples😀
RepositoryActivity😀
CI 🥺

Parenscript is the CL's answer to the ClojureScript. It makes possible to write client-side code in Lisp and compile it to the Javascript for running in the Browser.

I used Parenscript in one application, based on Electron and Weblocks.

Here is a piece of real code which uses Parenscript to define a client-side code. This code allows switching between different contacts using shortcuts. It uses jQuery to access DOM elements:

(weblocks-parenscript:make-dependency
 (let ((numbers-are-visible nil))
   
   ;; With this code we give user ability to press Alt+1, Alt+2,...
   ;; to select one of first 10 contacts in the list.
   (setf (@ document onkeydown)
         (lambda (e)
           (let ((code (@ e "keyCode"))
                 (numbers (j-query ".contact-list__contact-number")))
             ((@ console log) "Key down with code:" code)
             
             (cond
               ((= code 18)
                ((@ numbers show))
                (setf numbers-are-visible t))
               ((and (>= 57 code 48))
                ;; If user presed 0, then we'l consider it a 10.
                (when (= code 48)
                  (setf code (+ code 10)))
                
                (let ((contact-number (- code 48)))
                  ((@ console log)
                   "Selecting contact" contact-number)
                  ;; jQuery("#contact-0 a").click()
                  ((@ (j-query (+ "#contact-" contact-number " a"))
                      click))))))))
   
   (setf (@ document onkeyup)
         (lambda (e)
           (let ((code (@ e "keyCode"))
                 (numbers (j-query ".contact-list__contact-number")))
             ((@ console log) "Key up with code:" code)
             
             (cond
               ((= code 18)
                ((@ numbers hide))
                (setf numbers-are-visible nil)))))))

There are two ways of using Parenscript:

  • You can manually compile a s-exp into Javascript.
  • You can use it as an ASDF extension to compile some components automatically. But this will require the use of additional system paren-files. We'll review it later.

Let's investigate the first way by defining a simple JS function which sums two numbers and shows the result in Web Inspector's console:

POFTHEDAY> (import '(parenscript:ps
                     parenscript:@))

POFTHEDAY> (ps
             (+ 1 2))
"1 + 2;"

POFTHEDAY> (ps
             (defun foo (a b)
               (+ a b)))
"function foo(a, b) {
    return a + b;
};"

POFTHEDAY> (ps
             (defun foo (a b)
               ((@ console log)
                 (+ a b))))
"function foo(a, b) {
    __PS_MV_REG = [];
    return console.log(a + b);
};"

As you might notice, instead of dot notation, Parenscript uses a @ macro.

Parenscript supports a subset of loop macro:

POFTHEDAY> (ps
             (loop :with collection =  '(1 2 3)
                   :for i :in collection
                   :do ((@ console log)
                       i)))
"(function () {
    var collection = [1, 2, 3];
    var _js8 = collection.length;
    for (var _js7 = 0; _js7 < _js8; _js7 += 1) {
        var i = collection[_js7];
        console.log(i);
    };
})();"

And also it allows you to write custom macroses which will expand into the Javascript Code!

There are few ways to define macroses for Parenscript here is one of them. We'll define a macro in Common Lisp code and then will call this macro from Parenscript:

POFTHEDAY> (ps:defpsmacro awhen (test &body body)
             `(let ((it ,test))
                (when it
                  ,@body)))

POFTHEDAY> (ps
             (let ((some-var 100500))
               (awhen (= some-var 42)
                 ((@ console log)
                  "Some var is" it))))
"(function () {
    var someVar = 100500;
    var it = someVar === 42;
    __PS_MV_REG = [];
    return it ? console.log('Some var is', it) : null;
})();"

Rewriting macro for Parenscript is not a fan. Why you should do this if macro already exists for Common Lisp?

You shouldn't!

Import existing Lisp macro into the Parenscript!

In the next example, I'll show you how to reuse anaphoric if from rutils:

;; First, let's eval our form in Common Lisp
POFTHEDAY> (rutils:aif (= 1 2)
              (list :status :ok
                    :result rutils:it)
              (list :status :fail
                    :result rutils:it))
(:STATUS :FAIL :RESULT NIL)

;; By default it will not work:
POFTHEDAY> (ps
             (rutils:aif (= 1 2)
                (list :status :ok
                      :result it)
                (list :status :fail
                      :result it)))
"aif(1 === 2,
     ['status', 'ok', 'result', it],
     ['status', 'fail', 'result', it]);"

;; But we can import the macro:

POFTHEDAY> (ps:import-macros-from-lisp
            'rutils:aif)

POFTHEDAY> (ps
             (rutils:aif (= 1 2)
                (list :status :ok
                      :result it)
                (list :status :fail
                      :result it)))
"(function () {
    var it = 1 === 2;
    return it ? ['status', 'ok', 'result', it]
              : ['status', 'fail', 'result', it];
})();"

There is also built-in support for HTML rendering. Parenscript supports Allegro HTMLGen DSL:

POFTHEDAY> (ps
             (defun render-link (user)
               (ps:ps-html
                ((:a :href (+ "https://foo.bar/"
                               (get-nickname user)))
                 (get-name user)))))
"function renderLink(user) {
    __PS_MV_REG = [];
    return ['<a href=\\\"',
            'https://foo.bar/' + getNickname(user),
            '\\\">',
             getName(user),
            '</a>'].join('');
};"

And CL-WHO:

POFTHEDAY> (ps
             (defun render-link (user)
               (ps:who-ps-html
                (:a :href (+ "https://foo.bar/"
                              (get-nickname user))
                 (get-name user)))))
"function renderLink(user) {
    __PS_MV_REG = [];
    return ['<a href=\\\"',
            'https://foo.bar/' + getNickname(user),
            '\\\">',
             getName(user),
            '</a>'].join('');
};"

With Parenscript you can create client-side applications or code for running on Node.js. All of this, using Common Lisp power. Isn't it cool?


Brought to you by 40Ants under Creative Commons License