Ruby-style metaprogramming in JavaScript (plus a port of RSpec)
Programming in Ruby makes me happy. It’s a lovable language, with a pleasantly quirky syntax and lots of expressive power.
Programming in JavaScript, on the other hand, frustrates me to no end. JavaScript could be a reasonable language, but it has all sorts of ugly corner cases, and it forces me to roll everything from scratch.
I’ve been trying to make JavaScript a bit more like Ruby. In particular, I want to support Ruby-style metaprogramming in JavaScript. This would make it possible to port over many advanced Ruby libraries.
You can check out the interactive specification, or look at some examples below. If the specification gives you any errors, please post them in the comment thread, and let me know what browser you’re running!
Taking inspiration from Ruby
Ruby libraries often seem a little bit magical. Rails is an excellent example. Assuming we have a database with two
tables, employees
and projects
, we can write:
class Employee < ActiveRecord::Base
has_many :tasks
end
class Task < ActiveRecord::Base
belongs_to :employee
end
employee = Employee.find_by_name("Joe Smith");
employee.tasks.each {|task| print task.name }
This is a complete interface to our database! We only need to declare
the relationship between employees and tasks, and Ruby automatically
declares find\_by\_name
, tasks
, and dozens of other
methods for us.
There are two tricks here:
has\_many
andbelongs\_to
modify our classes at runtime, adding methods as needed.- ActiveRecord looks at our database tables, and notices that we have
fields like
name
. It uses this information to automatically addfind_by_name
and other methods to our class.
This style of programming is powerful, flexible, and concise. It also has some limitations. There’s no way to type-check this kind of code, so we need to write lots of test cases.
Can we do stuff like this in JavaScript?
Yup! You can grab the necessary code from my Subversion repository:
svn co http://www.randomhacks.net/svn/planetary/trunk/ planetary
The jsr
subdirectory of this project contains everything you
need to build Ruby-style libraries in JavaScript. Let’s begin with a
Ruby-style class declaration:
var Greeter = JSR.Class.extend();
with (Greeter.prototype) {
def("initialize", function (message) {
this.message = message || "Hello!";
});
def("hello", function () {
return this.message;
});
}
Here, Greeter
is a class with two methods,
initialize
and hello
. The def
function adds a new member function to Greeter
at run time,
just like the def
statement in Ruby.
We can use our new class as follows:
var greeter = new Greeter("Hello, world!");
println(greeter.hello());
We can also subclass Greeter
and override our
hello
method. Note that we can call the original version of
hello
using applySuper
:
var ChattyGreeter = Greeter.extend();
with (ChattyGreeter.prototype) {
def("hello", function () {
var before = arguments.callee.applySuper(this, arguments);
return before + " How are you today?";
});
}
Getting JavaScript to support applySuper
was fairly tricky; I
owe many thanks to Joshua Gertzen for explaining how to do it.
Now, we need to write some test cases!
Behavior-driven development
Test-driven development (TDD) is a technique for designing and building software incrementally. First, you begin by writing a test case. Then, you write just enough code to make that test case work. Finally, you repeat the whole process from the beginning.
But many programmers find TDD fairly counter-intuitive. It’s hard to know which tests to write when, and how big each test should be. When Dan North encountered this problem, he argued that programmers found TDD confusing because of bad terminology. He proposed Behavior-driven development (BDD), which basically just replaces “test cases” with “specifications,” and changes the other terminology to match. But this small change has a powerful psychological effect, making it easier to write good test cases.
One popular BDD library is RSpec, which has been catching on in the Ruby community. It provides a concise language for writing specifications:
describe "Array" do
it "should have a last() method returning the last element" do
[1,2].last.should == 2
lambda { [].last }.should raise_error(IndexError)
end
end
We can do the same thing in JavaScript. Unfortunately, we have to put up with quite a bit of syntactic noise:
spec("An array", JSSpec.Spec, function () {with(this){
it("should have a last() method returning the last element", function () {
[1,2].last().shouldEqual(2);
(function () { [].last(); }).shouldThrow();
});
}});
The it
function works much like def
in the previous
section.
To see this library in action, check out the interactive specification.
What’s next?
There are several projects which improve JavaScript in various ways.
Prototype adds a wealth of standard Ruby features, including
each
and many other iterator functions. TrimPath includes
a partial implementation of ActiveRecord in JavaScript, but it hasn’t been
updated in the past two years. Or if you’d prefer a less dynamic approach,
haXe offers static type declarations, a full-fledged type inferencer,
and a server-side VM.
The biggest problem with the approach described in this article is the syntactic noise. Perhaps a haXe-style syntactic preprocessor would help?
(Thanks to Aubrey Alexander for testing an earlier version of this library with IE 7 and Opera.)
Want to contact me about this article? Or if you're looking for something else to read, here's a list of popular posts.