Wednesday 24 June 2009

Lisp macros explained in Javascript

My two favourite programming languages are Common Lisp and Javascript.

I want to explain some of the features of Lisp in a syntax that is understandable to everyone. Of course everyone does not understand Javascript, but this example is simple and understandable also to C and Java coders.

The story

One day you see that you have a lot of small fragments in your code looking more or less like this:

if (someFunction(blaha)) {
doSomeThingWith(someFunction(blaha))
log("I did something with", someFunction(blaha));
};

You make a temporary variable to avoid running someFunction three times.

var temp=someFunction(blaha);
if (temp) {
doSomeThingWith(temp);
log("I did something with", temp);
};

You have now done this 2344 times in your career and at 34 places in your current project. SomeFunction takes 9999 ms. It smells. How long will it take until you want to refactor it?

In Javascript you can create new functions, but not syntactical constructs. And I can't really see how you would make a function to improve on this. But it would be cool if we could invent our own statements. What about this idea: An if statement that makes an automatic temporary variable that you can use later?

ifTemp (someFunction(blaha)) {
doSomeThingWith(temp);
log("I did something with", temp);
}

ifTemp would create a temp variable behind the scenes, put the result of someFunction in it and you could then refer to temp in the if clause. Now, if we are to hardcode the name of the variable, I would rather choose "it" instead of "temp". It makes the code almost poethic when read out loud.

ifit (someFunction(blaha)) {
doSomeThingWith(it);
log("I did something with", it);
}

I don't know who was the first to invent this, but I got the idea from Paul Graham and his book OnLisp. He calls this if-construct anaphoric if, as all we scholars instantly recognize from Greek rethorics. We could call this statement aif (a for anapahoric) instead of ifit.

An anaphoric if-macro in Javascript

Disclaimer: Don't try this at home. It won't work. This is just an example for how we probably would go about to do it, if it was possible.

macro.defineMacro("ifit",function() {
macro.defineSyntax("ifit",
macro.expression,
macro.block,
macro.startOptional,
"else", macro.block);
macro.defineExpander(function (context) {
var testExpression=context.getExpression();
context.moveForward();
var block=context.getBlock();

context.addStatement("var it=",textExpression);
context.addStatement("if (it) ",block);

// Handle else part
context.moveForward();
if (context.hasMore) {
context.skipForward("else");
var elseBlock=context.getBlock();
context.addStatement("else",elseBlock);
};
});
};

In theory we could make something like this, parsing js files on the server, expanding macros and creating new js files to be loaded by the browser. But I won't do it. If I wanted to I would use a Lisp-like dialect instead (like Parenscript).

Translating the Javascript to Lisp

Lisp statements have a simple syntax. The whole statement is surrounded by a pair of parenthes.

In semicolon based languages:

someFunction(someValue,someOtherValue); 

In Lisp:

(someFunction someValue someOtherValue)

Exactly the same number of parenthes by the way, and one less meaningless semicolon.

Javascript:

ifit (someFunction(blaha)) {
doSomeThingWith(it);
log("I did something with", it);
}

Translated to Lisp:

(whenit (someFunction blaha)
(doSomeThingWith it)
(log "I did something with" it))

"When" in Lisp is an "if" with no "else" clause. There is also an if-statement, but if we are to use it we need to put "doSomething" and "log" into a block (otherwise it thinks the log-statement is the else clause). A block in Lisp is actually a function called "progn". Progn means execute statements in order and use the last value as value.

(ifit (someFunction blaha)
(progn
(doSomeThingWith it)
(log "I did something with" it)))

Defining a variable in Lisp is a statement "let" that takes a list of variables and value pairs as the first parameter, and then any number of statements where the variable is visible.

The thing we looked at first was this piece of Javascript:

var temp=someFunction(blaha);
if (temp) {
doSomeThingWith(temp);
log("I did something with", temp);
};

Translated to Lisp:

(let ((temp (someFunction blaha)))
(if temp
(progn
(doSomeThingWith temp)
(log "I did something with" temp))))

An anaphoric macro in Lisp

Lisp macros are defined with defmacro. It is evaluated at compile time, and simply returns a Lisp statement. You get pieces of source-code as input parameters. These parameters are not evaluated. The macro can do things witht the source code it gets in and should return some new source code as output. Since Lisp has such a simple uniform syntax, creating and inspecting source code is not a difficult task.

(defmacro ifit (testForm thenForm elseForm)
(let ((srcCode "(let ((temp EXPRESSION))
(if temp THEN ELSE"))
(replace srcCode "EXPRESSION" testForm)
(replace srcCode "THEN" thenForm)
(replace srcCode "ELSE" elseForm)
(return srcCode))))))

To be truthful, actually the real world syntax it is even shorter and prettier using the backquote and comma subsitution mechanism. Notice that this is quite a bit shorter than the fake Javascript macro earlier.

(defmacro ifit (testForm thenForm elseForm)
`(let ((it ,testForm))
(if it ,thenForm ,elseForm)))

What is the deal?

Macros can be used for a lot of things. From small utilities like these to advanced embedded languages. Offloading lengthy calculations to compile time (Substitute "compile time" to on the "the server" for Javascript). Everything that Java uses "Annotations" for, but with more flexibility. If you get bored with Javascript, i suggest you take a look at Lisp. I recommend the book Practical Common Lisp as a good start. Some other great things about (Common) Lisp is a fresh view of object-orientation and unique exception-handling with restarts.

Disclaimer

I have made Lisp a little more mainstream in these examples in order to arguable make it easier to grasp. I would not call this simplification. On the contrary. Some would call it obfuscating.

No comments:

Post a Comment