Fork me on GitHub

Parenscript Is Awesome

Warning: I am still very new to parenscript. This is more of a rant than anything.

I'm building an HTML5-ified graphical chat client. It draws the conversation as a comic book. There have been clients like it before but I want to try using the wonderful new web technologies we have today to try and take the idea further. It's called mug.io.

I decided to build it in Common Lisp. I've been learning a lot about the language and decided that if I'm going to be building this thing in my free time I might as well enjoy doing it and use the language I want to use. Common Lisp has so much going for it that I wish I could use it in my day job... but I can't so there you go.

However at some point I knew I'd have to write some Javascript. Initially I thought I would just do all the positioning calculations server side and push updates to a dead-simple client that would just issue the canvas calls to draw what I wanted. However, the more I played with the idea the less I liked it. And I didn't relish the thought of writing a lot of Javscript either. What's a Lisp hacker-wannabe to do?

Parenscript, duh.

This amazing little library can compile a subset of Common Lisp to Javascript. And it's really good at it too. The Javascript that parenscript generates looks eerily hand-coded. The best thing about it is that I get all the power of Common Lisp that Javascript is lacking — macros, backquote... the whole nine yards.

My next question was... will I be able to use third-party Javascript libraries like JQuery? Yes you can. Awesome.

So I whipped up a bit of parenscript code to play around with it and get a feel for the library and what's possible. Please pardon any glaring wierdness... I'm still new to the library and haven't figured it all out yet (eg: how to turn off the implicit return statement).

(setf (ps:chain window onload) init)

(defvar *WIDTH* 400)
(defvar *HEIGHT* 400)

(defvar *canvas* nil)
(defvar *context* nil)
(defvar x 15)
(defvar y 0)
(defvar dx 10)
(defvar dy 5)

(defun init ()
  (init-canvas)
  (set-interval animate-box 20))

(defun init-canvas ()
  (progn
    (setf *canvas* (ps:chain document (get-element-by-id "canvas")))
    (setf *context* (ps:chain *canvas* (get-context "2d")))
    (setf (ps:@ *canvas* width) *WIDTH*)
    (setf (ps:@ *canvas* height) *HEIGHT*)
    1))

(defun draw-rect (x y w h)
  (progn
    (ps:chain *context* (begin-path))
    (setf (ps:@ *context* stroke-style) "#000")
    (ps:chain *context* (fill-rect x y w h))
    1))

(defun draw-grid ()
  (ps:chain *context* (begin-path))
  (loop for x from 0.5 below *WIDTH* by 10
     do (progn (ps:chain *context* (move-to x 0))
           (ps:chain *context* (line-to x *HEIGHT*))))
  (loop for y from 0.5 below *HEIGHT* by 10
     do (progn (ps:chain *context* (move-to 0 y))
           (ps:chain *context* (line-to *WIDTH* y))))
  (progn (setf (ps:@ *context* stroke-style) "#eee")
     (ps:chain *context* (stroke)))
  1)

(defun clear ()
  (ps:chain *context* (clear-rect 0 0 *WIDTH* *HEIGHT*)))

(defun animate-box ()
  (progn (clear)
     (draw-grid)
     (draw-rect x y 20 20)
     (if (or (> (+ x dx) (- *WIDTH* 20)) (< (+ x dx) 0))
         (setf dx (- dx)))
     (if (or (> (+ y dy) (- *HEIGHT* 20)) (< (+ y dy) 0))
         (setf dy (- dy)))
     (setf x (+ x dx))
     (setf y (+ y dy))
     1))

I put this in a file I called wafflemaker.paren and I compile it with parenscript using the following form:

(with-open-file (f "wafflemaker.js" :if-exists :supersede :direction :output)
       (write-string (ps:ps-compile-file "wafflemaker.paren") f))

And this writes out a nice little file with some javascript in for me. I created a simple index.html file that loads it and open that in my HTML5 compliant browser (Firefox 5). I see a little bouncing box on a grid background. Pretty nifty.

window.onload = init;
var WIDTH = 400;
var HEIGHT = 400;
var CANVAS = null;
var CONTEXT = null;
var x = 15;
var y = 0;
var dx = 10;
var dy = 5;
function init() {
    initCanvas();
    return setInterval(animateBox, 20);
};
function initCanvas() {
    CANVAS = document.getElementById('canvas');
    CONTEXT = CANVAS.getContext('2d');
    CANVAS.width = WIDTH;
    CANVAS.height = HEIGHT;
    return 1;
};
function drawRect(x, y, w, h) {
    CONTEXT.beginPath();
    CONTEXT.strokeStyle = '#000';
    CONTEXT.fillRect(x, y, w, h);
    return 1;
};
function drawGrid() {
    CONTEXT.beginPath();
    for (var x = 0.5; x < WIDTH; x += 10) {
        CONTEXT.moveTo(x, 0);
        CONTEXT.lineTo(x, HEIGHT);
    };
    for (var y = 0.5; y < HEIGHT; y += 10) {
        CONTEXT.moveTo(0, y);
        CONTEXT.lineTo(WIDTH, y);
    };
    CONTEXT.strokeStyle = '#eee';
    CONTEXT.stroke();
    return 1;
};
function clear() {
    return CONTEXT.clearRect(0, 0, WIDTH, HEIGHT);
};
function animateBox() {
    clear();
    drawGrid();
    drawRect(x, y, 20, 20);
    if (x + dx > WIDTH - 20 || x + dx < 0) {
        dx = -dx;
    };
    if (y + dy > HEIGHT - 20 || y + dy < 0) {
        dy = -dy;
    };
    x += dx;
    y += dy;
    return 1;
};

Now this is just a toy script and does not demonstrate completely what's entirely possible with parenscript. You still get full access to Common Lisp macros. You still have the SLIME development environment in emacs. There's even a nice little emacs hack I'm looking into called slime-proxy that will stream your compiled parenscript defuns to the browser directly — updating your running Javascript code right from the REPL as its running... just like you can when writing a pure-Lisp program.

This is some seriously good kit.

If anyone can help me out with the explicit return thing.. that would be appreciated....