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\_manyandbelongs\_tomodify 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_nameand 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.
While I share everything you say about Javascript, I’m going a different route. Translating Ruby code into Javascript code automatically using [RubyJS](http://blog.ntecs.de/articles/2007/01/08/rubyjs-javascript-no-thank-you). All top-level meta-programming tricks that are available in Ruby can be abused…
Thanks for the link! RubyJS is an interesting approach. It does all the metaprogramming in Ruby, than introspects the result and compiles it down to JavaScript. Given what I know about the innards the Ruby interpreter, that sounds pretty painful. :-)
I’m hoping that a native JavaScript approach will reduce download size. But I’ll keep an eye on RubyJS!
I’ve been recently defrosting the TrimPath codebase, all because of Google Gears which finally gives us a true RDBMS on the client side. I hope to have an updated TrimPath library out soon.
I’m glad to hear that you’ll be working on TrimPath again! I’ll be watching your work closely, and looking for opportunities for cross-pollination.
I’m not really sure about the ‘def’ to construct classes (although it’s a nice way of adding stuff dynamically, it seems clunky to do that when defining the class).
Other than thatHeh, great stuff! Specially since I was looking to port rspec to javascript :)
As for the spec component, seems nice, I’ll have a closer look at the source and comment on it :)
Have a look at ActiveSupport for JavaScript . It’s a library I’ve uploaded yesterday to port a lot of Ruby’s syntactic sugar to javascript (some of core Ruby, a lot of ActiveSupport).
It works on top of Prototype to add all the magic (only trunk, for now, you need
Function.prototype.curryandFunction.prototype.methodizefor a few aliases and shortcuts).I intended to write a rspec-clone in JS to test my ActiveSupport (I find the one on script.aculo.us a bit over-restrictive), but might end using yours ;)
Gahh, bad copy pasting while editing the comment and an unfortunate click on submit instead of preview (preview should be automatic! or farther away from the submit button!)
Please feel free to use my RSpec clone in other projects. It should play nicely with Prototype, and if it doesn’t, I’ll be happy to fix it.
I’m also quite happy to fill in many of the missing features. Just let me know what you need!
I’m curious why you chose the method name ‘spec’ instead of ‘describe’?
meekish: Good question. :-) Now that I think about it, RSpec’s choice of name is pretty good.
Nicolás: I’m working to make ‘def’ quite a bit less clunky, and to make this style of JavaScript programming as pleasant as possible.
There’s some new stuff in Subversion, which will eventually appear as another blog post.
http://jspec.info may be what your looking for
Ooh, jspec is nice.
But it only gets about halfway there: If you’re going to build a JavaScript preprocessor, it would be really useful to add support for class declarations, etc., and not just for specifications.