Last week, I discovered an oddity about Ruby’s literal array syntax in conjunction with hashes. The following (simplified) test is syntactically valid Ruby code:
describe do
let(:h) { { "foo" => "bar" } }
let(:a) { [h] }
it { a.should =~ ["foo" => "bar"] }
end
Do you see what’s strange about the syntax? After running the test, I was about to add a second expectation and noticed the missing curly braces in line number 4. As it turns out, the following statement evaluates to true
:
2.0.0-p247 :001 > [{ "foo" => "bar" }] == ["foo" => "bar"]
=> true
For me, this was a surprise, since I would have expected some kind a syntax error (maybe unexpected token '=>'
). Further experiments revealed the following:
2.0.0-p247 :002 > [123, { "foo" => "bar" }] == [123, "foo" => "bar"]
=> true
2.0.0-p247 :003 > [{ "foo" => "bar" }, 123] == ["foo" => "bar", 123]
SyntaxError: (irb):3: syntax error, unexpected ']', expecting =>
from /Users/cs/.rvm/rubies/ruby-2.0.0-p247/bin/irb:16:in `<main>'
Literal array definitions, seem to behave exactly like ordinary method invocations: The last argument is interpreted as a hash – even without the curly braces. In the Rails community, this behaviour is widely known as the options
hash.
Now, every Ruby method call must have a receiver. If no explicit receiver has been specified, the implicit receiver is self
. This suggets, that there should be a method []
defined on self
. Let’s check:
2.0.0-p247 :004 > self.method(:[])
NameError: undefined method `[]' for class `Object'
from (irb):8:in `method'
from (irb):8
from /Users/cs/.rvm/rubies/ruby-2.0.0-p247/bin/irb:16:in `<main>'
Strangely, []
is not defined. Hmmm.
By consulting the standard library documentation, I disovered the method Array.[]
. Except for the explicit receiver, that class method seems to be functionally equivalent to the literal syntax:
2.0.0-p247 :004 > Array[]
=> []
2.0.0-p247 :005 > Array[123]
=> [123]
2.0.0-p247 :006 > Array[123, "bar" => "foo"]
=> [123, {"bar"=>"foo"}]
Just out of curiousity, it would be interesting to find out wether Array.[]
gets called when instantiating a new array with literal syntax. Luckily, Ruby’s meta-programming features make it easy to rewrite existing methods:
2.0.0-p247 :007 > class <<Array
2.0.0-p247 :008?> alias_method :original_square_brackets, :[]
2.0.0-p247 :009?> def [](*args)
2.0.0-p247 :010?> puts "Array.[] has been called!"
2.0.0-p247 :011?> original_square_brackets(*args)
2.0.0-p247 :012?> end
2.0.0-p247 :013?> end
=> nil
2.0.0-p247 :014 > Array[1, 2, 3]
Array.[] has been called!
=> [1, 2, 3]
2.0.0-p247 :015 > [1, 2, 3]
=> [1, 2, 3]
Line number 9 clearly demonstrates that Array.[]
has been rewritten as intended. However, nothing has been printed during instantiation of the literal array in line number 12.
Even though, []
is behaving exactly like Array.[]
(which is an ordinary method), it’s not possible to change []
. In comparision, Array.[]
can be overwritten easily. My guess is, that the interpreter has been optimized to directly use the native implementation of []
and skip the usual method lookup process. Magic ;)
Did you know about [{"foo" => "bar"}] == ["bar" => "foo"]
?