Object#andand
What
Object#andand
lets us write:
@phone = Location.find(:first, ...elided... ).andand.phone
And get a guarded method invocation or safe navigation method. This snippet performs a .find
on the Location class, then sends .phone
to the result if the result is not nil. If the result is nil, then the expression returns nil without throwing a NoMethodError.
As Dejan Simic put it:
Why would you want to write this:
entry.at('description') && entry.at('description').inner_text
when you can write this:
entry.at('description').andand.inner_text
Why indeed! As a bonus, install andand and you will also receive an enhanced Object#tap method, at no extra charge!
Installing
Update to RubyGems 1.2.0 before proceeding!!
sudo gem sources -a http://gems.github.com (you only have to do this once) sudo gem install raganwald-andand
Or:
git clone git://github.com/raganwald/andand.git cd andand rake gem rake install_gem
The basics
Object#andand
Ruby 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.
Object#andand letās us write:
@phone = Location.find(:first, ...elided... ).andand.phone
And get the same effect as:
@phone = ->(loc){ loc && loc.phone }.call(Location.find(:first, ...elided... ))
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 ... }
Object#andand 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.
Block-Structured andand
You can use andand
with a block instead of a method:
@phone = Location.find(:first, ...elided... ).andand { |location| YellowPages.reverse_lookup(location).phone }
If the receiver is nil, the block is not executed and andand
returns nil
. If the receiver is not nil, it is passed as a parameter to the block and andand
returns the value of the block. This makes it possible to perform conditional evaluation and also to make the scope of the variable really obvious.
Scope
Object#andand
only works for one method call. For example, fu.andand.bar.blitz
is going to call nil.blitz
if fu
is nil
. Thatās because fu.andand.bar
is going to evaluate to nil
, and then we will call the method blitz
on it. In most cases, you want to use fu.andand.bar.andand.blitz
. If that seems awkward, you might want to reconsider violating the Law of Demeter in an environment where you canāt be sure if your receiver is nil
or not.
Another example of this (pointed out by Jan Zimmek):
x = nil x.andand.length > 3
This results in a NoMethodError
. Again, x
is nil
, therefore x.andand.length
is nil
, therefore we end up with nil > 3
which results in a NoMethodError
. This can be fixed with x.andand.length.andand > 3
as above, or perhaps:
x = nil x.andand { |value| value.length > 3 }
Enhanced Object#tap
Ruby 1.9 introduces Object#tap. This library implements Object#tap for Ruby 1.8 and enhances it. As in Ruby 1.9, you can call .tap
with a block:
blah.sort.grep( /foo/ ).tap { |xs| p xs }.map { |x| x.blah }But like its sibling
.andand
, you can now call .tap
with a method as well:
[1, 2, 3, 4, 5].tap.pop.map { |n| n * 2 } => [2, 4, 6, 8]
Doctor, it hurts when I do that
So donāt do that!
The popular use case for Object#tap is poor manās debugging:
blah.sort.grep( /foo/ ).tap { |xs| p xs }.map { |x| x.blah }
Perhaps you want to remove the tap, you can delete it:
blah.sort.grep( /foo/ ).map { |x| x.blah }
Or, you can change it to .dont
:
blah.sort.grep( /foo/ ).dont { |xs| p xs }.map { |x| x.blah }
Like .andand
and .tap
, .dont
works with arbitrary methods, not just blocks:
(1..10).to_a.reverse! => [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] (1..10).to_a.dont.reverse! => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
A little more background
Object#andand & Object#me in Ruby explains the original motivations, as well as providing links to similar implementations you may want to consider. A few people have pointed out that Object#andand is similar to Haskellās Maybe monad. The Maybe Monad in Ruby is a good introduction for Ruby programmers.
Thatās cool, butā¦
No problem, I get that andand isnāt exactly what you need. Have a look at the Invocation Construction Kit or āIck.ā The Ick gem generalizes #andand and #tap: Ick provides four useful ways to block-structure your code, the methods #let, #returning, #inside, and #my. Ick also includes four quasi-monadic invocations, #maybe, #please, #tee, and #fork.
Ick provides abstractions for building your own invocations, so you can branch out and build some of your own abstractions with Ickās building blocks.
How to submit patches
Read the 8 steps for fixing other peopleās code.
The public clone url is git://github.com/raganwald/andand.git
. Fork you very much.
License
This code is free to use under the terms of the MIT license.
Shout Out
Mobile Commons. Huge.
Also interesting: Wicked
Contact
Comments are welcome. Send an email to Reginald Braithwaite.