ampex — a practical use of Ruby's & operator

Conrad Irwin — May 2012

Ruby is renowned for both its readability and its flexibility. The ultimate goal of every Ruby program is to work well while appearing as simple as possible.

To achieve this as a Ruby programmer, you're encouraged to push a lot of the fiddly bits of code deep into parts of your program where a casual reader of the source can ignore them.

Out of the box Ruby ships with some very powerful tools to help you do this, the most famous of which is its syntax for custom blocks. This allows you to re-use existing fiddly code (for example iterating over a list) but to insert a little bit of customisation (so now you can iterate over a list doubling as you go).

[1, 2, 3].map{ |i| i * 2 }
=> [2, 4, 6]

The & operator

Blocks are only the beginning of this ability as Ruby has the & operator which can be used to "cast" any object to a block. This behaviour works out-of-the-box on Procs, Methods, and Symbols.

You will have seen the Proc support in action a lot, because that's how you pass &block from one function to the next. Method objects are rarely used in Ruby, though sometimes inserting .tap(&method(:puts)) is a useful way to debug an object in the middle of an expression.

The most interesting example of & however is on the Symbol class. This was ported to the core Ruby language in version 1.8.7 because it was so popular with users of the libraries that implemented it (the Ruby Extensions Project and ActiveSupport).

The whole purpose of using & with a symbol is to generate more readable code, compare:

["1", "2", "3"].map{ |string| string.to_i }
=> [1, 2, 3]
["1", "2", "3"].map(&:to_i)
=> [1, 2, 3]

The version with the symbol has two benefits:

  1. There are fewer punctuation characters.
  2. You don't need to introduce a temporary name for a variable.

This second point is the one that has the biggest impact on people reading your code. The mental machinery that programmers use to track variables is necessarily hefty, so if they don't need to invoke it so often they will find that your code takes less effort to understand.

&X itself

The ampex library takes the idea from &:symbol, and adds a little more flexibility. This means that you can get all the punctuation-free, variable-free goodness, but more often!

A few examples of how cool this can be:

# when you want to pass an argument to a method
[10, 11, 12].map(&X.to_s(16))
=> ["a", "b", "c"]
# when you want to parse some JSON
owners = [{'name' => 'Fred', 'dog' => 'Fido'},
          {'name' => 'Ron', 'dog' => 'Rex'}];
owners.map(&X['name'])
=> ["Fred", "Ron"]
# when you want to chain some method calls
"alpha\nbeta\ngamma\n".lines.map(&X.strip.upcase)
=> ["ALPHA", "BETA", "GAMMA"]

This last example gives you something similar to the new Enumerable::Lazy module added in Ruby 2.0; but without having to wait for that to be released. If you're interested in more that ampex can help you with, the README contains a few more examples.

Installation

The ampex library is distributed as a rubygem, so to use it, you can either install it one-off:

$ gem install ampex

Or add it to your Gemfile.

source :rubygems
gem 'ampex'

We've been using ampex in production for over a year now, and beacuse it's written in pure Ruby, it works on Ruby 1.8.7, Ruby 1.9 and JRuby out of the box.

Further Thinking

While ampex helps a lot, there are still cases where you need to introduce a temporary variable unnecessarily. As mentioned above, for some of these cases you can use &method(:foo); but I find that that code makes me think even harder than the equivalent block version.

I would really love to see something like Scala's underscore in Ruby itself. There have been a few attempts at this, mostly catalogued in Reg Braithwaite's comprehensive guide to Anaphora in Ruby, for example RubyUnderscore adds it to Ruby 1.8.7 by syntax rewriting and this patch adds it to Rubinius, but none are yet ready to actually be used.

Particularly I think it should be possible with some ingenuity to come up with a solution that covers another 90% of the things you want to do. For example, with pure Ruby and a little cunning, the following could be made to work:

[1, 2, 3].map(&».puts(X))

The question, I suppose, is whether or not it can be done simply enough…