info | twitter | github
J-_-L | Writings on Ruby Programming
24.06.10 08.05.11
29 comments

The 28 Bytes of Ruby Joy!

The 28 Bytes of Ruby Joy will notably clean up your code:

 1
def p.method_missing*_;p;end

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:

 1
2
3
4
5
class NilClass
  def method_missing *_
    nil
  end
end

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:

Update II

This is a nice snippet by Yohan to decently activate this feature when needed.

Listing 3
/26/catch_nil.rb ruby
 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):

 1
def p.method_missing*_;end
Creative Commons License

Comments

24.06.10

Dmytro Shteflyuk

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] }

24.06.10

J-_-L

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.

24.06.10

Matt Todd

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.

24.06.10

Matt Todd

Wow, that formatting is terrible. Sorry.

24.06.10

Luismi Cavallé

"Fail fast" (http://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.

24.06.10

Yohan

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

24.06.10

Yohan

catch_nil {
if @some_hash[:some_key].to_formatted_array
....
end
}

24.06.10

J-_-L

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>

24.06.10

Sergey Kruk

how about this?

if (@some_hash[:some_key].to_formatted_array[3] rescue nil)
# do something
end

24.06.10

J-_-L

@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.

24.06.10

kyr

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.

24.06.10

Reg Braithwaite

http://github.com/raganwald/homoiconic/blob/master/2009-02-02/hopeless_egocentricity.md

24.06.10

Trans

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

24.06.10

J-_-L

@Reg Thank you for the article, really enjoyed it. I think it has the best arguments against the global nil patching.

25.06.10

Sean B

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.

25.06.10

J-_-L

@Sean It is not about polluting something - but more about, making your code dryer, when things just work differently.

25.06.10

Yohan

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.

25.06.10

J-_-L

Thank you again Yohan, I've added your little script to the main article :).

26.06.10

tom

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......

27.06.10

J-_-L

@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.

28.06.10

anonym

This is what I call a Black Hole, and it's a pretty large trade-off. I wrote an article about it on PragPub: http://www.pragprog.com/magazines/2010-01/much-ado-about-nothing

28.06.10

Paolo "Nusco" Perrotta

Sorry, I didn't mean to be an anonymous coward. The post above is mine. :)

28.06.10

J-_-L

Thank you Paolo, for this link/article. Added it to the resource list.

06.07.10

anonym

def p.method_missing*;end

06.07.10

J-_-L

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

26.07.10

Avdi Grimm

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 (http://andand.rubyforge.org/), or (my personal favorite) use the Null Object pattern to write self-confident code: http://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: http://avdi.org/devblog/2010/01/20/confident-code-at-bmore-on-rails/

30.07.10

odo@mac.com

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

01.08.10

J-_-L

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

30.06.11

Pierre Carrier

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