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)
endThat’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)
endThose 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
endwhich 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)
endThis 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.