Some Ruby Goodness
Posted by Stijn Pint on Jul 07, 2008
Last week, while I was writing the following expression for like the 1000th time, it struck me…
<code class="ruby">
name = Product.find_by_code('code') ? Product.find_by_code('code').name : nil
WTH am I doing writing this crazy, long, duplication bloated one-liner in ruby ? YES, in RUBY ! That ‘s just plain WRONG!
There just HAS to be some great, DRYer, magic, aaaah’s and oooooh’s generating, no-longer-than-5-characters expression that handles this!
So instead of bashing my laptop against the wall I IM’d my colleague and local ruby-guru Peter.
Well, he wasn’t really surprised by my question.
Apparently it’s something that comes up quite often in the ruby community / forums, and there is not really a perfect solution for it.
Big disappointment at first, but we started talking about possible alternatives (well, he talked, I mostly listened ;-).
I wanted a solution for 2 slightly different scenarios:
<code class="ruby">
# EXPR #1:
name = Product.find_by_code('code') ? Product.find_by_code('code').name : nil
# EXPR #2:
name = Product.find_by_code('code') ? Product.find_by_code('code').name : 'something else'
h3. SOLUTION #1: and
prod = Product.find_by_code('code') and name = prod.name
p. The part after and will only be executed if prod != nil
And even if the second part doesn’t get executed, the variable name will still be initialized at nil ! Great !
For EXPR #2, this method becomes a bit too complicated, on to solution #2 !
h3. SOLUTION #2: rescue
<code class="ruby">
name = Product.find_by_code('code').name rescue nil
name = Product.find_by_code('code').name rescue 'something else'
Short and clean, but the rescue clause will catch anything and might hide some other problem like this.
So it should be used with care, it is definitely not always a good solution.
Ok, time for the aaaaaah’s and oooooooh’s now.
h3. SOLUTION #3: blocks
For this we first need to define one or two methods that will help clean up our expression.
I’m in Rails, and by injecting the 2 methods in the Object class, I can call them from anywhere I want :
<code class="ruby">
class Object
def use
yield self
end
def chain
yield self if self
end
end
EXPR #1 now becomes:
<code class="ruby">
name = Product.find_by_code('code').use {|e| e ? e.name : nil}
# or with the chain method:
name = Product.find_by_code('code').chain {|e| e.name }
EXPR #2 gives:
<code class="ruby">
name = Product.find_by_code('code').use {|e| e ? e[0] : 'something else'}
And it works also with the chain method, if you make some changes :
<code class="ruby">
def chain(default=nil)
self ? yield self : default
end
name = Product.find_by_code('code').chain('something else') {|e| e.name }
Waw, that’s cool, no ?
But I still wasn’t completely satisfied, so I had another question for Peter.
Wouldn’t it be even cooler if I could write the following:
<code class="ruby">
name = Product.find_by_code('code').chain.name
#or
name = Product.find_by_code('code').chain('something else').name
Well, it is possible, and they even have a name for it: magic dot syntax
h3. Solution #4: Magic dot syntax
<code class="ruby">
class Chainer
def initialize(obj, default)
@obj = obj
@default = default
end
def method_missing(m, *a, &b)
if @obj
@obj.send(m, *a, &b)
else
@default
end
end
end
class Object
def chain(default = nil)
Chainer.new(self, default)
end
end
You might need a minute to figure it out…I know I did :-)
I’ll try to explain it a bit:
- calling the chain method, creates a new Chainer object, by calling the Chainer#initialize method
- the name method, called on this Chainer object will be catched by method_missing
- method_missing will call the name method on your product unless the product was nil, in that case it will just return nil (or the object you passed to the chain method).
Isn’t that just great !
And with such a cute name, how can you not use it ? :-)
p. This probably isn’t that spectacular for the more experienced Ruby developer,
but I do think it’s another good example of the power and flexibility of Ruby !
Entries per category
- 6 pages are tagged with docpublisher
- 11 pages are tagged with events
- 14 pages are tagged with rails
- 30 pages are tagged with ruby
- 7 pages are tagged with sharepoint
