Ruby's magic ampersand does more than you think

inopinatus

We’ve long been enamored of Ruby’s ability to turn anything, although especially a symbol, into a block, with the magic ampersand:

class Person
  attr_accessor :firstname, :lastname

  def initialize(firstname, lastname)
    self.firstname = firstname
    self.lastname = lastname
  end

  def display_name
    "#{lastname.upcase}, #{firstname.gsub(/\w+/, &:capitalize)}"
  end
end

writers = [
  Person.new('percy', 'shelley'),
  Person.new('jean-paul', 'sartre'),
  Person.new('mary', 'shelley')
]
writers.map(&:display_name)
# => ["SHELLEY, Percy", "SARTRE, Jean-Paul", "SHELLEY, Mary"]

Okay, technically it’s the “unary & operator” but in my mind, this is always pronounced Magic Ampersand.

We can also sort with the magic ampersand:

writers.sort_by(&:lastname).map(&:display_name)
# =>  ["SARTRE, Jean-Paul", "SHELLEY, Percy", "SHELLEY, Mary"]

but herein lies a problem. With that sort_by, we’re reaching inside the class and pulling out the lastname. That’s a code smell. It’s the Person class’s business how Person objects get sorted. Instead we treated each Person like a glorified struct. In abusing the Persons we made assumptions and got it wrong - our Shelleys are the wrong way around.

There’s more than one solution to this problem, and I’m going to present my favourite, to illustrate a little-known facet of &:symbol behaviour.

What it really does

We usually have this mental shorthand; that &:symbol gives us a block equivalent to :symbol.to_proc and that this behaves like { |obj| obj.send(:symbol) }. But in fact the block generated behaves more like this:

do |*args|
  args.shift.public_send(:symbol, *args)
end

That’s right; as we know it sends the symbol as a message to the first argument, but then it passes through additional arguments. And since we know that sort will call the comparison function with two values (a, b), we can define and use an arbitrary comparator for the class, like this:

class Person
  # ...
  def compare_name(b)
    [self.lastname, self.firstname] <=> [b.lastname, b.firstname]
  end
end

writers.sort(&:compare_name).map(&:display_name)
# => ["SARTRE, Jean-Paul", "SHELLEY, Mary", "SHELLEY, Percy"]

The very nice part is that this can be moved into a mixin, allowing reuse, and this also works with Rails, as in this slightly fictionalised example:

module Person
  extend ActiveSupport::Concern
  def display_name
    "%s, %s" % [lastname.upcase, firstname.gsub(/\w+/, &:capitalize)]
  end
  def compare_name(b)
    [self.lastname, self.firstname] <=> [b.lastname, b.firstname]
  end
end

class Writer < ApplicationRecord
  include Person
end
class Actor < ApplicationRecord
  include Person
end

creatives = (Writer.all + Actor.all)
creatives.sort(&:compare_name).map(&:display_name)
# => ["FUKUSHIMA, Rila", "SARTRE, Jean-Paul", "SHELLEY, Mary", "SHELLEY, Percy", "STREEP, Meryl"]

The advantage of this over simply implementing Comparable is being able to offer multiple ordering options on a model, such as age, or name, or number of published books, and then refer to them explicitly without needing to know any details of implementation. For example, I’ve used this to allow a computed priority to affect order, placing pinned items at the top of a list.

Argument injection

We can take this a step further. If the trailing arguments are included magically, how about relying on that in conjunction with methods - especially enumerator methods - that inject additional arguments. Want an example? Okay.

Let’s say I have a module used in development that adds methods to closure objects. It’ll include itself thus:

module MissionControl
  #...

  Proc.include(self)
  Method.include(self)
  Binding.include(self)
  UnboundMethod.include(self)
end

Those last four lines ain’t so pretty. We’d like to DRY this up, and the first refactoring looks like this:

module MissionControl
  #...

  [Proc, Method, Binding, UnboundMethod].each do |klass|
    klass.include(self)
  end
end

which is neater, but still longer than necessary. We can’t just write [...].each(&:include) because that argument, self, has to be passed along as well.

The great news is that there’s an enumeration method doing exactly this. The Enumerable#each_with_object method does something like what we wanted:

items.each_with_object(obj) { |item, obj| ... }

and the key is recognising that with the first parameter being an item from the array, and the next being a trailing argument, we can slap this together with the block produced by Symbol#to_proc, for a magical one-liner:

module MissionControl
  #...

  [Proc, Method, Binding, UnboundMethod].each_with_object(self, &:include)
end

This will call include(MissionControl) on every one of those four classes, which is exactly what we wanted.

Whether you find that more immediately comprehensible is rather a matter for the reader, but it’s one of my favourite examples of Ruby, synthesizing the dynamic object system, standard library, and functional syntax in one short but beautiful expression.