The 28 Bytes of Ruby Joy!
The 28 Bytes of Ruby Joy will notably clean up your code:
All it does is patching the nil
object that it also returns nil
when you call an unknown method on it. Normally it would raise a NoMethodError
. This is the cleaner, non-golfed version:
Consider the following, common situation: You have an instance variable, which usually contains a hash of objects: @some_hash
. Now you want to access :some_key
and want to do something with the value. You also know, @some_hash
is sometimes nil
. So you might want to write
if @some_hash[:some_key] # good looking version
# do something with @some_hash[:some_key]
end
but it is not working properly, because it fails on when @some_hash
is nil
. Instead, you have to write
if @some_hash && @some_hash[:some_key] # correct version
# do something with @some_hash[:some_key]
end
And this is just an easy example. Let’s take a look at
if @some_hash[:some_key].to_formatted_array[3] # good looking version
# do something
end
vs
if @some_hash && some_hash[:some_key] && @some_hash[:some_key].to_formatted_array && @some_hash[:some_key].to_formatted_array[3] # correct version
# do something
end
The 28 Bytes of Ruby Joy allow you to do use the good looking version in both examples.
Conclusions
A drawback of the hack might be, that the application does not crash at some occasions, where it probably should do so, but forwards the nil
. But I think, in reality, this means just a few more tests.
The primary reason, I dislike the exception raising of nil
is that it creates some kind of redundancy. I do not want to double-check if something is nil
. And to say it with the words of Yukihiro Matsumoto himself: “When information is duplicated, the cost of maintaining consistency can be quite high.”
So, what is a strong counter-argument for not using the 28 Bytes of Ruby Joy?
Update
After reading some feedback, I have realised, that I need to add this disclaimer: Don’t just use this hack, if you don’t see all of its consequences – especially when used together with already existing code that assumes the usual nil
exception way (like Ruby on Rails).
However, if you do fully understand it, it might be a nice help on your own projects (maybe in a more decent form).
Here are some more resources about (and arguments against) the topic:
- Paolo “Nusco” Perrotta wrote a nice and detailed explanation about this technique for the PragProg Magazine
- FailFast – a general approach to early get to know when something does not work as it should
- A very interesting article by Reg Braithwaite about why you should not use this technique
- Another way of applying this partly by coderr
- tie-racks opinion: do not write one liners
- Wikipedia
Update II
This is a nice snippet by Yohan to decently activate this feature when needed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
# Egocentric nil # code by Yohan, slightly edited and comments by me # start ego mode block, usage: # catch_nil do # if @some_hash[:some_key].to_formatted_array[3] # # do something # end # end def catch_nil(&block) # grip methods ori_method_missing = NilClass.instance_method(:method_missing) catch_method_missing = NilClass.instance_method(:catch_method_missing) # activate ego mode NilClass.send :define_method, :method_missing, catch_method_missing # run code yield ensure # no matter what happens: restore default nil behaviour NilClass.send :define_method, :method_missing, ori_method_missing end #alias :egonil :catch_nil # ;) # this is the ego nil class NilClass def catch_method_missing(m, *args, &block) nil end end
Update III
This version does not use method_missing and allows a default value (but with different semantics: after the raise, no more code gets executed).
1 2 3 4 5 6 7 8 9
def egonil(nil_value = nil) yield rescue NoMethodError => e if e.message =~ /NilClass$/ nil_value else raise NoMethodError end end
Update IV
To use the egonil in daily coding, you can use the zucker gem.
Update V
An even more golfed version of the 2826 bytes (from the comments):
Dmytro Shteflyuk | June 24, 2010
Why not use simple :try method, which returns nil for nil objects?
if @some_hash.try([], :some_key).try(:to_formatted_array).try(:[], 3)
Yes, it's a little longer and ... not so DRY. Another idea:
if allow_nils { @some_hash[:some_key].to_formatted_array[3] }
J-_-L | June 24, 2010
Hi Dmytro,
ActiveSupport's try method is quiet nice, but especially the bracket calls do not look nice, as one can see in your first example. The second idea, however, looks like nice compromise.
Matt Todd | June 24, 2010
This is not an appropriate solution for this problem. What you'll want to do instead is properly establish these instance variables in constructors elsewhere. For example:
<code>
@some_hash ||= {}
if @some_hash[:some_key].to_formatted_array[3]
# do something
end
</code>
Place the conditional assignment in the constructor if necessary, or abstract that into a modified attr_reader method:
<code>
def some_hash
@some_hash ||= {}
end
</code>
Then you can:
<code>
if self.some_hash[:some_key].to_formatted_array[3]
# do something
end
</code>
Abusing method_missing (and even just using method_missing in general) is often not recommended, though it can be utilized to great effect. However, when you're altering the behavior of a basic element like this, you're just asking for trouble. Lots of software check for nil values and some expect the NoMethodError so this change would break that software.
I think a good policy would be that if a change you're making would make one of the specs for Ruby fail, it's probably not something that should be done.
Matt Todd | June 24, 2010
Wow, that formatting is terrible. Sorry.
Luismi Cavallé | June 24, 2010
"Fail fast" (https://www.c2.com/cgi/wiki?FailFast) would be a good counter-argument. Some bugs (i.e. problems you didn't foresee in your tests) would be more difficult to fix.
Yohan | June 24, 2010
More secure !
class NilClass
def catch_method_missing(m, *args, &block)
return nil if args.size == 0 && !block_given?
super
end
end
# Usage:
# catch_nil do
# if @some_hash[:some_key].to_formatted_array[3]
# something
# end
# end
def catch_nil(&block)
ori_method_missing = NilClass.instance_method(:method_missing)
catch_method_missing = NilClass.instance_method(:catch_method_missing)
NilClass.send :define_method, :method_missing, catch_method_missing
begin
yield
ensure
NilClass.send :define_method, :method_missing, ori_method_missing
end
end
Yohan | June 24, 2010
catch_nil {
if @some_hash[:some_key].to_formatted_array
....
end
}
J-_-L | June 24, 2010
Sry for the broken markup, fixed it.
@Matt
I know, it is a hard intervention into the style of writing Ruby. The way you have changed the code example is probably one of the cleanest, but what happens, if :some_key does not exist, and the hash returns nil? - the next method will fail. And that's the point which is sometimes a little bit annoying when using Ruby.
@Luismi thanks for the link, interesting
@Yohan thanks for this piece of code, still trying to understand it. Unfortunately, it throws an error when I try:
<code>catch_nil do
nil[5]
end</code>
Sergey Kruk | June 24, 2010
how about this?
if (@some_hash[:some_key].to_formatted_array[3] rescue nil)
# do something
end
J-_-L | June 24, 2010
@Sergey Very interesting approach, but I think, it's not the thing I want, because 1. you need the brackets and 2. has the side effect that it also rescues different exceptions.
kyr | June 24, 2010
oh man. don't you like begin ... rescue ... end?
you don't really need(and/or want) to know if every single condition is true, you just need to do something with your data or process the goddamn exception.
Reg Braithwaite | June 24, 2010
https://github.com/raganwald/homoiconic/blob/master/2009-02-02/hopeless_egocentricity.md
Trans | June 24, 2010
Well first I will say "#ergo". Then I will say what this is really after is an "OOPy If"
@some_hash.if(:some_key) do |val|
...
end
J-_-L | June 25, 2010
@Reg Thank you for the article, really enjoyed it. I think it has the best arguments against the global nil patching.
Sean B | June 25, 2010
How about using @your_hash = Hash.new(DefaultClass.new) -- Then undefined hash keys return your desired default behavior, and you are polluting nil with custom methods. Duck typing can be problematic enough, 'nil' and NoMethodError are there for a reason.
J-_-L | June 25, 2010
@Sean It is not about polluting something - but more about, making your code dryer, when things just work differently.
Yohan | June 25, 2010
I simplified the NilClass.catch_method_missing, just return nil (without testing if the missing method take arguments, or is '[]').
class NilClass
def catch_method_missing(m, *args, &block)
nil
end
end
So ...
catch_nil {nil[5].hello.world('Yohan')}
return just nil.
J-_-L | June 25, 2010
Thank you again Yohan, I've added your little script to the main article :).
tom | June 26, 2010
Listing 1 is very subtle and clever! How does "method_missing*_" work?? How does it differ from a simple "method_missing"?? And what is the significance of "p"? Where is it defined?? How did you discover such techniques?!? very cool......
J-_-L | June 27, 2010
@tom Listing 1 isn't good style, it just takes the language to its extremes ("golfing" - as few bytes as possible). The <code>method_missing*_</code> is just the same as <code>method_missing(*args)</code>. <code>p</code> without argument just returns <code>nil</code>. Don't use this snippet directly (because p might be overwritten), but listing 2 or 3.
Anonymous | June 28, 2010
This is what I call a Black Hole, and it's a pretty large trade-off. I wrote an article about it on PragPub: https://www.pragprog.com/magazines/2010-01/much-ado-about-nothing
Paolo "Nusco" Perrotta | June 28, 2010
Sorry, I didn't mean to be an anonymous coward. The post above is mine. :)
J-_-L | June 28, 2010
Thank you Paolo, for this link/article. Added it to the resource list.
Anonymous | July 06, 2010
def p.method_missing*;end
J-_-L | July 06, 2010
xD thats golfing, damn. Nothing is of course nil.... I've also tried it without the underscore, but it didn't work... now it does oO. thx
Avdi Grimm | July 26, 2010
Speaking from experience in large Ruby codebases, never do this. Someday a programmer who has to maintain your code will come after you with a pickaxre. And I don't mean the pickaxe book.
The thing is, there's absolutely no reason to accomplish this effect by abusing method_missing. Simply use something like #try() (from ActiveSupport), Reg's AndAnd (https://andand.rubyforge.org/), or (my personal favorite) use the Null Object pattern to write self-confident code: https://avdi.org/devblog/2008/10/30/self-confident-code/
Incidentally, I did an hour-long talk on techniques for eliminating the kind of nil-checking you're trying to avoid above. You can watch it here: https://avdi.org/devblog/2010/01/20/confident-code-at-bmore-on-rails/
odo@mac.com | July 30, 2010
I have a suggestion for a less intrusive solution where you have to explicitly put the code in a block.
See the inline documentation for an explanation.
class NoMethodErrorInNil < NoMethodError; end
class NilClass
def method_missing(method_name, *args)
raise NoMethodErrorInNil, "undefined method `#{method_name}' for nil:NilClass"
end
end
class Object
# ================================================
# = Patch Object to optionally ignore nil return =
# ================================================
# this is a patch so you can wrap code
# where you expect to get nil at some point but don't care
# >> ignore_nil{nil.non_existent_method}
# => nil
# you can also pass a default return value:
# >> ignore_nil(0){nil.non_existent_method.length}
# => 0
def ignore_nil(return_value = nil, &block)
begin
yield
rescue NoMethodErrorInNil => e
return return_value
end
end
end
J-_-L | August 01, 2010
Thank you odo. I like the idea with the default return value. The only (minor) disadvantage I see is that it might break existing code which specifically checks for NoMethodError
Pierre Carrier | June 30, 2011
Anyone heard of lambdas? :)
No need to declare any classes.
def simple_procedure_that_restores
nilmm = NilClass.instance_method(:method_missing)
NilClass.send :define_method, :method_missing, lambda {}
yield
NilClass.send :define_method, :method_missing, nilmm
end