Monday, 3 September 2012

Ruby 1.9 barewords - wat?

Recently I've seen this screencast by Destroy All Software entitled "Wat", really fun talk I'd recommend to watch,  despite the LONG loading time of the site.... maybe you should go there, click play, click pause for it to start loading, come back, read the rest of this post, grab yourself some coffee and watch the screencast.
Like any normal person geek I went to try this out myself

awls99@artworking:~$ jirb
irb(main):001:0> def method_missing *args; args.join ' '; end
=> nil
irb(main):002:0> Hello ruby bare words
SystemStackError: stack level too deep
from org/jruby/RubyKernel.java:1093:in `eval'
from org/jruby/RubyKernel.java:1419:in `loop'
from org/jruby/RubyKernel.java:1205:in `catch'
from org/jruby/RubyKernel.java:1205:in `catch'
from /home/awls99/jruby-1.6.4/bin/jirb:13:in `(root)'

It didn't work! How disappointing. But then I remembered that I was running jirb for Ruby 1.9 and that screencast has some time, so maybe it only works on Ruby 1.8, so after setting my jirb to 1.8 mode the above code sample simply worked.
That's neat and all, but why won't this work on 1.9? This sample, is one of those things that you should not use but the fact that it works simply shows how awesome Ruby is.
I've tried a few variations of the code, since this is a stack too deep issue, it seems that this method is calling itself ad infinitum, but I can't put my finger on why he's doing it.
I decided that I'd find a way to make this work on Ruby 1.9, it should be simple! Well, I found out how, it's a bit longer, so I've separated them:

awls99@artworking:~$ jirb
irb(main):001:0> def method_missing m, *args
irb(main):002:1> [m.to_s, args].flatten.join ' '
irb(main):003:1> end
=> nil
irb(main):004:0> ruby onenine has barewords too
=> "ruby onenine has barewords too"

Now this got me wondering, "why do I need to go over this extra loop for 1.9?"
For those of you unfamiliar with method_missing, it goes like this:
the first argument is the method (that is missing) as a symbol, the second an array with all arguments passed, the third a block, thus you usually see this:
def method_missing method, *args, &block
[code]
end
If you suppress the first argument it will be included in the args array as the first element, because that's the way the splat (*) operator works and it's awesome!
So it occurred to me that the reason it's going stack too deep is because when Ruby is trying to cast the method as a string (for the join) it's using a method missing and since we re-wrote the Object::method missing it ends up looping on itself. It makes sense.
To prove my theory I've fired up two jirbs, one with Ruby 1.8 the other 1.9

Ruby 1.8:


awls99@artworking:~$ jirb
irb(main):001:0> [:method].join ' '
=> "method"
irb(main):002:0> def method_missing m
irb(main):003:1> 1
irb(main):004:1> end
=> nil
irb(main):005:0> [:method].join ' '
=> "method"

As can be see clearly here line 005 didn't jump inside the method missing declared on line 002.

Ruby 1.9


irb(main):001:0> [:method].join ' '
=> "method"
irb(main):002:0> def method_missing m
irb(main):003:1> 1
irb(main):004:1> end
=> nil
irb(main):005:0> [:method].join ' '
TypeError: Symbol#to_str should return String
from org/jruby/RubyArray.java:1866:in `join'
from (irb):5:in `evaluate'
from org/jruby/RubyKernel.java:1093:in `eval'
from org/jruby/RubyKernel.java:1419:in `loop'
from org/jruby/RubyKernel.java:1205:in `catch'
from org/jruby/RubyKernel.java:1205:in `catch'
from /home/awls99/jruby-1.6.4/bin/jirb:13:in `(root)'

Here however, it did, as I predicted, Ruby 1.9 casts symbols to string using a method missing, that's why the original "wat barewords" method didn't work, however, I bet that if I enclose it inside a class it will work.

awls99@artworking:~$ jirb
irb(main):001:0> class A
irb(main):002:1> def method_missing *a;a.join ' ';end
irb(main):003:1> def bare; barewords inside a klass;end
irb(main):004:1> end
=> nil
irb(main):005:0> A.new.bare
=> "barewords inside a klass"

SCIENCE!