A better try — chaining methods and nil
Of #try, #andand, and #do_or_do_not, the one I like best (and I don’t even like any of them that much, but that’s my next post) is #try.
What they are all basically for is chaining method calls when one of methods might return nil, resulting in a NoMethodError. Here’s Chris Wanstrath’s implementation:
class Object ## # @person ? @person.name : nil # vs # @person.try(:name) def try(method) send method if respond_to? method end end
It’s almost right!
First of all, I’d argue that if #try is trying to be better than something, it had better be better than @person.name if @person. That looks and reads so nicely I can scarcely see why it needs replacing. @person.name if @person is so obviously better than @person ? @person.name : nil that I hope people are using that before they start thinking about monkey patching Object (which I hear is destroying Ruby). Anyway, I’d fix the documentation first.
But I think the biggest problem with #try is that it’s not doing what it says it’s doing. It’s not a replacement for @person ? @person.name : nil. That’s certainly not what the method is doing. Here’s what I’d do:
class Object ## # @person.name if @person # vs # @person.try(:name) def try(method) self.send(method) if self end end
See? Much clearer; it does what it says. Using respond_to? means that the original #try is about something else; it’s about whether or not an object responds to a message. This #try is about sending a message to an object unless it’s nil or false, which is closer to what the original use case was… right?
But then it occurred to me that the issue is nil, not nil or false. The problem isn’t getting false in the middle of method chains, it’s nil. So maybe what #try should really be is this:
class Object ## # @person.name unless @person.nil? # vs # @person.try(:name) def try(method) self.send(method) unless self.nil? end end
I might even use that.
Arya said,
March 3, 2008 @ 5:03 pm
Interesting approach. I definitely like your way better than do_or_do_not simply because it isn’t rescuing all exceptions with nil (that just seems like a bad idea). Also, I appreciate the fact that you continue to call the method if it’s not nil even if the object does not respond to that method AND that you call self.send except that it doesn’t make a difference (at least I think so, but I may be wrong). Any method defined in global scope exists in every object:
def foo
puts “foo”
end
Object.new.send(:foo)
But in my opinion, you are right to call the method on the object if it’s not nil and does not respond to the method:
@some_value = “Hello World
….
@some_value.try(:uniq) # uniq being the method for an Array and not a String
If someone calls the wrong method on an object they’re expecting to be an array, they have a bug and they should be notified of it via an exception, not a silent fail.
Reg Braithwaite said,
March 3, 2008 @ 5:15 pm
Even though your opening the Object class is destroying Ruby, I like your better try :-)
Here’s something to think about: the code you are quoting doesn’t handle parameters or blocks. So the obvious fix is to capture an optional block and optional parameters. This will allow:
@some_value.try(:a_method, 42) { |foo| … }
Have a look at #send_with_default for an example of someone thinking along those lines:
http://rhnh.net/2007/10/2/object-send_with_default
There are links to a bunch of other approaches to this problem at the bottom of my own post explaining #andand:
http://weblog.raganwald.com/2008/01/objectandand-objectme-in-ruby.html
I am adding your post to the list. Thanks for taking the time to share!
Chris Shea said,
March 3, 2008 @ 6:07 pm
@Reg: You’re right. It should handle arguments and blocks. It’s other glaring deficiency is that it lets you get around a method’s privateness. Object#andand gets it done right.
Reg Braithwaite said,
March 3, 2008 @ 7:22 pm
I’m flattered you say #andand gets it right… I think it (and kernel#ergo, an almost identical implementation done before I wrote #andand) are good for their use case, but I do think there might be something to try()… maybe an Object#please with #andand-like semantics.
So (expr).andand.foo says “do foo if the expression is not nil) and (expr).please.foo says “do foo if you handle foo.” I need to look and see what else people are doing along those lines. In a general sense, I worry about two things:
1. if you use #respond_to? you break when people do method_missing magic. I try to avoid method_missing magic for that reason, it’s hard to use reflection. But you have to write for the code people actuallyt use, and;
2. if you handle NoMethodError, you catch your receiver’s NoMethodErrors, but also any errors raised if your receiver is trying to handle something and THAT raises an error. That is definitely not the semantics I have in mind!
So… It’s all dancing with bears. It’s lovely circus-stuff, but you’re in a heap of trouble if your partner steps on your toes :-)
Thanks again for your post.
coderrr said,
March 3, 2008 @ 11:08 pm
have you seen: http://coderrr.wordpress.com/2007/09/15/the-ternary-destroyer/ ?
A better “try()” for Ruby, why not do the Groovy way? | Urubatan’s Weblog said,
March 5, 2008 @ 8:35 am
[…] new method in Ruby 1.9 is making some people happy and creative too. The only problem with that in my opinion is that every one is fixed […]
프로그래밍 루비 » Blog Archive » 짧은 코드 한 줄이 주는 영감 - try() 놀이 said,
March 14, 2008 @ 2:45 am
[…] 풀려던 문제(@person ? @person.name : nil)와 답이 틀렸다는 주장도 있다. try라는 이름에 걸맞을려면 respond_to?보다는 객체가 닐인지만 […]