Object#andand & Object#me in Ruby
At
Mobile Commons, we have to write a lot of code under time constraints. We also work on a fairly distributed basis, which means that writing readable code is not a luxury, it’s a necessity. We can’t afford to be clever for clever’s sake, but if a new idiom helps make our code easier for our colleagues to grasp in a hurry, we embrace it. Here’re some idioms we’ve recently created:
Object#andandRuby programmers are familiar with the two
guarded assignment operators
&&=
and
||=
. The typical use for them is when you have a variable that might be nil. For example:
@first_name &&= @first_name.trim
@phone ||= '612-777-9311'
You are trimming the first name provided it isn’t nil, and you are assigning ‘612-777-9311’ to the phone if it
is nil (or false, but that isn’t important right now). The other day we were discussing the guards and we agreed that we wished there was a
guarded method invocation operator. Here’s an example of when you would use it:
@phone = Location.find(:first, ...elided... )&&.phone
Meaning, search the location table for the first record matching some criteria, and if you find a location, get its phone. If you don’t, get nil. (Groovy provides this exact functionality, although Groovy uses
?.
instead of
&&.
) However,
&&.
won’t work because
&&.
is not a real Ruby operator. So what do we write instead?
A feature is “powerful” when at least one of the following holds:
- It can be used to implement something trivial in an pointlessly complicated way.
- It can cause a lot of damage.
Seriously, it seems like 85% percents of the contexts where something is called “powerful,” it really means “useless and dangerous.”
How about:
@phone = (loc = Location.find(:first, ...elided... )) && loc.phone
Note that we need a local variable to make it work without extending the Object class. Note further that although we only need the local variable for one line of code, it is in method scope and we must be careful that we don’t clobber an existing
loc
variable. And when reading this code, we now have to look and see whether
loc
is used again to understand what this line does.
This is a scoping problem. if you are going to use variables, you want to restrict their scope as much as possible. It feels like half of what we do when programming is manage side effects like overwriting a variable you are using elsewhere, and the more you can restrict those side effects, the easier programming becomes. Java supports block scoping natively, and languages like Scheme solve this by providing a block scope macro,
let
. And you can roll your own in languages like
Javascript that provide anonymous functions. But this seems to get
the natural left-to-right order wrong:
@phone = lambda { |loc| loc && loc.phone }.call(Location.find(:first, ...elided... ))
Well, we are tinkerers and toolsmiths. So let’s roll our own. We can’t use
&&.
without rewriting the Ruby parser, and that seems drastic. So let’s use “andand” in the hope that our non-English-speaking colleagues will forgive us. We want to write:
@phone = Location.find(:first, ...elided... ).andand.phone
And get the same effect as
@phone = ->(loc){ loc && loc.phone }.call(Location.find(:first, ...elided... ))
. This would be bloody trivial if we have a macro facility, but “If wishes were horses then beggars would ride.” Instead, we need to write a method that does some conditional evaluation for us. If the receiver (in this case the result of
Location.find(:first, ...elided... )
) is truthy, like an ActiveRecord model, we want
andand
to return it so we can use it. But if it is falsy, we want
andand
to somehow return something that takes any message you send it and returns itself without doing anything. In other words, we want a mock object of sorts. So that’s what we’ll do:
class Object
def andand (p = nil)
self or MockReturningMe.new(self)
end
end
class MockReturningMe
instance_methods.reject { |m| m =~ /^__/ }.each { |m| undef_method m }
def initialize(me)
@me = me
end
def method_missing(*args)
@me
end
end
Actually, there is more to it than this, for example the MockReturningMe class is enclosed in a module to minimize namespace pollution. Consult the source and RSpec (links below) for the latest particulars.
Note that because you accept any method using Ruby’s method invocation syntax, you can accept methods with parameters and/or blocks:
list_of_lists.detect { ...elided... }.andand.inject(42) { ...elided ... }
Some of the other solutions to this problem that use
send
change Ruby’s syntax. This solution emphasizes syntactic regularity: the goal was to make an “&&.” operation that worked like &&=. &&= looks just like normal assignment, you can use any expression on the RHS, only the semantics are different. The andand method also works just like a normal method invocation, only the semantics are modified.
Enhanced Object#tap(Formerly called Object#me)
Ruby 1.9 includes
Object#tap:
Location.find(:first, ...elided... ) do |location|
puts location.inspect
end
=> location
Ruby On Rails includes a similar feature, the
returning idiom:
returning(Location.find(:first, ...elided... )) do |location|
...elided...
end
=> location
Both return the location, not whatever happens inside the block. This is very useful, especially when chaining methods in a pipeline. Sometimes you want to do something for side effects, but continue on with the chain. For example:
returning([1, 2, 3, 4, 5]) do |arr|
arr.pop
end.map { |n| n * 2 }
=> [2, 4, 6, 8]
There are going to be times that using a method instead of a function makes the code more consistent. So let’s imagine what that would look like:
Location.find(:first, ...elided... ).tap do |location|
...elided...
end…
This is much nicer when you are “injecting” some extra code into the middle of an expression, such as when adding some poor-man’s-debugging puts statements.
Would we ever want to go beyond a block and write
Location.find(:first, ...elided... ).tap.some_method
instead of
Location.find(:first, ...elided... ).tap do ...elided... end
? Of course we would! Quite often, we want to send several methods to the same receiver. In Smalltalk, the semicolon does this explicitly. In Ruby, we have to be very careful to make sure our methods return
self to enable chaining. For example:
[1, 2, 3] << 4 << 5
=> [1, 2, 3, 4, 5]
But if a method doesn’t return the receiver, chaining doesn’t work. That’s why we had to use “returning” in the array example above:
[1, 2, 3, 4, 5].pop.map { |n| n * 2 }
=> NoMethodError: undefined method `map' for 5:Fixnum
That’s because
[1, 2, 3, 4, 5].pop => 5
, not
[1, 2, 3, 4]
. So instead, we write:
[1, 2, 3, 4, 5].tap.pop.map { |n| n * 2 }
=> [2, 4, 6, 8] # or we write...
[1, 2, 3, 4, 5].tap { |arr| arr.pop }.map { |n| n * 2 }
=> [2, 4, 6, 8] # both work
When you examine the source, you’ll see that
tap looks a lot like
andand. No surprise, we are doing similar things with slightly different semantics.
This implementation of Object#tap has two advantages over the implementation in Ruby 1.9: first, it works in Ruby 1.8. Second, it adds the ability to call another method (like
pop
) and not have to include a block.
Summary: andand & enhanced tapandand and
tap both provide the same two benefits: First, they either eliminate (in the case of a method call) or limit in scope (in the case of a block) a local variable. Second, they let you maintain a natural left-to-right order when writing pipelined expressions. They are simple and obvious enough that I feel any unfamiliarity for the reader will be more than outweighed by the cleaner, easier-to-digest code that results from their liberal use.
My suggestion is that these two benefits make it worth your while to add these methods to the Object class and use them regularly in your code.
Installationsudo gem install andand
. For the source and more information,
http://andand.rubyforge.org.
Similar solutions:
- Kernel#ergo in Ruby Facets
- send_with_default
- maybe
- _?
- if_not_nil (via François Beausoleil)
- Groovy’s Safe Navigation Operators via Call by Name: "Yo Elvis"
- try() and A better try
postscript: Inline RescuesI have now seen two people asking about using an inline rescue instead of andand:
Location.find( …elided… ).phone rescue nil
Also, one of the alternate solutions seems to use this technique. Obviously, it works for many of the cases you want to try, and you don’t need to add a new method to Object. However, it is—to paraphrase
Chalain—“Sneaking up on the Interpreter.”
It achieves what we want almost by accident, namely that it rescues
NoMethodError
, which by coïncidence gives us the result we want. However, it does not communicate our intent. It says that we want to transmute all exceptions into a value of
nil
. When someone glances at the code, will they think of
NoMethodError
? Or will they assume that we are trying to handle exceptions that the
.phone
method might throw?
And what if
.phone
actually throws something serious? In Rails, what happens if a migration is screwed up and there is no phone column? I think we actually want a
NoMethodError
in that case. The rescue clause will accidentally swallow the exceptions we are trying to catch.
I think if you are absolutely certain that you do not care about other exceptions and also if your team uses this idiom extensively so that it does not miscommunicate its intent, you can live a long and happy life using it. However, I would be hesitant to recommend it as a general-purpose solution.
andand and some of the other solutions obviously require the code reader to learn a new method name, but after that they offer the exact functionality we need and clearly communicate their intent.
Labels: ruby