You know the old saying:
with (JavaScript) {
metaprogramming.is('possible');
}
I’m going to leave the discussion of what constitutes metaprogramming to another day (read: never), but what I will say is that I’m becoming more interested in DSLs and fluent interfaces. I want the code I write to work at a very high level, where it describes what I’m trying to achieve in terms that anyone else (read: me, six months later) can understand. I particularly like this example of a JavaScript DSL for describing state machines:
var Machine = FSM.build(function(fsm) { with (fsm) {
onUnexpectedEvent(function() { ... });
state('start', 'initial')
.event('go')
.goesTo('middle')
.doing(function() { ... })
.doing('phew')
.event('run')
.goesTo('finish')
.onExiting(function() { ... });
state('middle')
.onUnexpectedEvent(function() { ... })
.onEntering(function() { ... })
.event('back')
.goesTo('start')
.onlyIf(function() { return true_or_false })
.event('go')
.goesTo('finish');
state('finish', 'final');
}});
This example, along with various testing/speccing libraries, make heavy use of
the much-maligned with
statement. with
essentially works as follows: inside
the with
block, all variable names are first looked up in the object passed to
with
, before being looked up in the parent scope. so:
var object = {
name: 'James',
getName: function() {
return this.name;
}
};
var theName, name;
with (object) {
name = 'Bob';
theName = getName();
}
// theName == "Bob"
// name == ""
// object.name == "Bob"
// object.theName === undefined
Now, clearly this can cause serious headaches if overused, and I would never
recommend writing your business logic using with
statements. But, for writing
descriptions of things – like specs, or state machine definitions, or class
definitions – it can be quite helpful. Using JS.Class, you can implement
some of Ruby’s class definition syntax in 20 lines of code:
JS.Ruby = (function() {
var extendDSL = function(builder, source) {
for (var method in source) {
if (!builder[method] && typeof source[method] == 'function')
builder[method] = source[method].bind(source);
}
};
var ClassBuilder = function(klass) {
this.def = klass.method('instanceMethod');
this.self = {def: klass.method('classMethod')};
extendDSL(this, klass);
this.extend = function(source) {
klass.extend(source);
extendDSL(this, klass);
};
};
return function(klass, define) {
define(new ClassBuilder(klass));
};
})();
That implements a DSL that, with a little help from with
, lets you write
JavaScript classes that look like Ruby ones, with all the power and flexibility
that entails (let’s pretend you’re porting ActiveRecord to JavaScript):
ActiveRecord.Base = JS.Class();
JS.Ruby(ActiveRecord.Base, function(Ruby) { with(Ruby) {
extend(ActiveRecord.Validations);
validatesPresenceOf('name');
validatesLengthOf('password', {minimum: 8});
with (self) {
def('create', function(attrs) {
var base = new this();
base.setAttributes(attrs || {});
return base;
});
}
def('initialize', function() {
this.attributes = {};
});
def('setAttributes', function(attrs) {
for (var key in attrs)
this.attributes[key] = attrs[key];
});
var methodName = function(string) {
return string.replace(/_(\w)/g, function(match, s) {
return s.toUpperCase();
});
};
// We're in a function, so any kind of logic goes
['name', 'email', 'password'].forEach(function(name) {
def(methodName(name), function() {
return this.attributes[name];
});
def(methodName('set_' + name), function(value) {
this.attributes[name] = value;
});
});
}});
The whole class definition takes place inside a closure, so you can create any helper code you want in there and it won’t touch the global namespace. You have more control over the ordering of method definitions and mixin inclusions, and you can define methods dynamically to your heart’s content. The class behaves as you’d expect:
var me = ActiveRecord.Base.create({name: 'James'});
me.setAttributes({password: 'fizzbuzz'});
me.setEmail('james@jcoglan.com');
me.name() == 'James' // true
The moral of this story is that with
can be useful occasionally, especially
when you want to describe or define something without repeating yourself too
much. It’s useful for writing DSLs for doing such jobs, but don’t go sprinkling
it around inside your actual methods. You’ll just get confused.