Seven Languages in Seven Weeks, Week 1: Ruby

In an attempt to get back into programming language theory and implementation—a hobby I’ve neglected since I started working full-time—I recently started reading Bruce Tate’s Seven Languages in Seven Weeks. These are my notes and observations from my first week of study.

In week 1, Bruce introduces Ruby, drawing attention to its dynamic nature, expressive syntax, and metaprogramming capabilities. Together, these properties make it a suitable language for building natural, English-like APIs.

I didn’t think I could learn anything from Ruby that years of writing Python and JavaScript hadn’t already taught me. However, after three days of studying the language, I was pleasantly surprised to be proven wrong. There’s a lot to learn from Ruby about designing languages for humans first and machines second.

Syntax

Most of my experience with dynamic languages has been with Python and JavaScript, both of which are conservative with syntax sugar. This is a design decision I have always appreciated but, after acquainting myself with some Ruby libraries, I feel there’s an argument to be made in favor of liberally adding syntax sugar to make expressing common programming idioms more convenient.

The downside of all the sugar is that Ruby’s syntax comes with many surprises. For example, I can define a method that accepts a block as its final argument like so:

def myFun n, &block
  # do something nice
end

However, I can’t define a method that accepts two blocks as arguments using the same syntax, since the ampersand is a special bit of syntax reserved for defining methods that accept a block as their final argument. So, this is an error:

def myFun &block1, &block2
  # do something nice
end

There are a number of ways of calling a method, which can seem overwhelming at first. For example, consider the following method:

def myFunction param, &block
  # do something nice
end

Both these ways of calling myFunction are correct:

myFunction 1, { |n| puts n }
myFunction(1) { |n| puts n }

But the following is a syntax error:

myFunction(1, { |n| puts n })

If, however, we change the definition of myFunction so that it does not accept a block as its second argument:

def myFunction param1, param2
  # do something nice
end

Then, it can be called in the following ways:

myFunction 1, 2
myFunction(1, 2)

But the following is a syntax error:

myFunction(1) 2

Things can get quite murky when we start talking about defining and calling methods, especially when we throw the ampersand operator into the mix. You win some you lose some.

REPL

For certain classes of programs, I like to use a REPL for interactive development. Ruby comes with irb, which is passable but not great. It helps while debugging and exploring language concepts, but it’s not powerful enough to let you interactively build and test programs.

I’d like to shout out ipython here, which is the best non-Lisp REPL I’ve used in my career.

Introspection

Both Python and JavaScript have great introspection capabilities, but they never feel as natural and as they do in Ruby.

For example, I wanted to check if there was a way to convert a Ruby hash into a bunch of key-value pairs. I knew that, in Ruby, the names of all methods that convert one data type into another conventionally have the prefix “to_”. Armed with this knowledge, I only had to write this bit of code to list all methods on a hash that converted it into a different object:

{ :foo => :bar }.methods.select { |m| m.to_s.start_with? "to_" }

Just for comparison, the equivalent in JavaScript would be:

const isMethod = obj => typeof obj === 'function';

const listAllMethods = obj => {
  const ownMethods = Object.getOwnPropertyNames(obj).filter(p => isMethod(obj[p]));
  const proto = Object.getPrototypeOf(obj);

  if (proto) {
    return [].concat(ownMethods, listAllMethods(proto));
  } else {
    return ownMethods;
  }
};

const s = {
  foo: 'bar'
};

listAllMethods(s).filter(m => m.startsWith('to'));

Metaprogramming

Metaprogramming is Ruby’s strong suit, and there isn’t much I can say about it that hasn’t already been said. Rails’ ActiveRecord is perhaps the best example of how metaprogramming can help build clean, natural-looking APIs.

My opinion of metaprogramming has always been that it’s great for people building libraries and frameworks, but not so great for people building applications. Too much magic can make code unpredictable and hard to debug.

However, I haven’t spent enough time with Ruby to know for sure how the liberal use of metaprogramming affects code clarity and maintainability in a large codebase. Most programming languages make it circuitous to do any kind of metaprogramming, and the APIs are usually an afterthought. In Ruby, the metaprogramming APIs are so well-designed and natural that—used in moderation—metaprogramming might actually enhance code clarity without any negative affect on maintainability.

As an example of the power of metaprogramming, Bruce presents a class that returns the Arabic equivalent of a Roman numeral whenever it’s accessed as a static property:

irb(main):001:0> Roman.X
=> 10
irb(main):002:0> Roman.XC
=> 90
irb(main):003:0> Roman.XII
=> 12

In Ruby, the Roman class looks something like this:

class Roman
  def self.method_missing name, *args
    roman = name.to_s
    roman.gsub!("IV", "IIII")
    roman.gsub!("IX", "VIIII")
    roman.gsub!("XL", "XXXX")
    roman.gsub!("XC", "LXXXX")

    (roman.count("I") +
     roman.count("V") * 5 +
     roman.count("X") * 10 +
     roman.count("L") * 50 +
     roman.count("C") * 100)
  end
end

I attempted to translate this into JavaScript using ES6 Proxies:

'use strict';

String.prototype.count = function(rx) {
    return (this.match(new RegExp(rx, 'g')) || []).length;
};

const Roman = new Proxy({}, {
    get: (_, property) => {
        let roman = property.slice();
        roman = roman.replace(/IV/g, 'IIII');
        roman = roman.replace(/IX/g, 'VIIII');
        roman = roman.replace(/XL/g, 'XXXX');
        roman = roman.replace(/XC/g, 'LXXXX');

        return (
            roman.count('I') +
            roman.count('V') * 5 +
            roman.count('X') * 10 +
            roman.count('L') * 50 +
            roman.count('C') * 100
        );
    }
});

Not too bad, huh?


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *