Ruby-style metaprogramming in JavaScript (plus a port of RSpec)

Posted by Eric Kidd Sun, 01 Jul 2007 19:00:00 GMT

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:

  1. has_many and belongs_to modify our classes at runtime, adding methods as needed.
  2. ActiveRecord looks at our database tables, and notices that we have fields like name. It uses this information to automatically add find_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.)

Tags , , ,

Comments

  1. Michael Neumann said about 10 hours later:

    While I share everything you say about Javascript, I’m going a different route. Translating Ruby code into Javascript code automatically using RubyJS. All top-level meta-programming tricks that are available in Ruby can be abused…

  2. Eric said about 13 hours later:

    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!

  3. Steve Yen said about 17 hours later:

    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.

  4. Eric said about 22 hours later:

    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.

  5. Nicolás Sanguinetti said 1 day later:

    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.curry and Function.prototype.methodize for 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 ;)

  6. Nicolás Sanguinetti said 1 day later:

    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!)

  7. Eric said 3 days later:

    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!

  8. meekish said 24 days later:

    I’m curious why you chose the method name ‘spec’ instead of ‘describe’?

  9. Eric said 24 days later:

    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.

  10. TJ Holowaychuk said 777 days later:

    http://jspec.info may be what your looking for

  11. Eric said 780 days later:

    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.

Comments are disabled