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.