Ruby and/or Methods
A colleague recently brought up the fact that is is possible to do this:
class Symbol
def +(other_thing)
"#{self} plus #{other_thing}"
end
end
:something + "something else"
#=> "something plus something else"
But not this:
class Symbol
def ||(other_thing)
"#{self} or #{other_thing}"
end
end
:something || "something else"
# 2: syntax error, unexpected ||
# def ||(other_thing)
# ^
# 5: syntax error, unexpected keyword_end, expecting end-of-input
Adding a +
method works fine, but adding a ||
method causes a
syntax error. Let’s look at some of Ruby’s virtual machine instructions
to find out what is going on here.
puts RubyVM::InstructionSequence.compile(':a + :b').disasm
# 0002 putobject :a
# 0004 putobject :b
# 0006 opt_plus <callinfo!mid:+, argc:1, ARGS_SIMPLE>, <callcache>
# 0009 leave
This is not surprising. Ruby puts two objects (putobject
at 0002 and 0004)
onto its internal stack and then calls the +
method (at 0006).
(It doesn’t matter at compile time that symbols wouldn’t normally have a +
method. The method might get defined later, or else it will raise a
NoMethodError at runtime.)
Compare that to the instructions for ||
:
puts RubyVM::InstructionSequence.compile(':a || :b').disasm
# 0002 putobject :a
# 0004 dup
# 0005 branchif 10
# 0007 pop
# 0008 putobject :b
# 0010 leave
This is quite different!
The instructions for ||
do not involve a Ruby method call at all.
Instead, Ruby puts an object on the stack (at 0002),
then uses branchif
(at 0005) to either jump to instruction 0010
or pop the first object off the stack (at 0007) and replace it with
a new object (at 0008).
&&
works quite the same way, but with branchunless
:
puts RubyVM::InstructionSequence.compile(':a && :b').disasm
# 0002 putobject :a
# 0004 dup
# 0005 branchunless 10
# 0007 pop
# 0008 putobject :b
# 0010 leave
The instructions for an if/else statement are quite similar:
code = <<-RUBY
if :a
:a
else
:b
end
RUBY
puts RubyVM::InstructionSequence.compile(code).disasm
# 0002 putobject :a
# 0004 branchunless 9
# 0006 putobject :a
# 0008 leave
# 0009 putobject :b
# 0011 leave
There is some difference since we are not using the condition directly
as a return value, but I think the similarities are clear.
&&
and ||
are used for control flow much like if
and unless
. They behave more like keywords than method calls.
Just to confirm ||
is really not a method:
[].method(:+)
#=> #<Method: Array#+>
[].method(:cheese)
#=> NameError: undefined method `cheese' for class `Array'
[].method(:||)
# SyntaxError: unexpected ||, expecting tSTRING_CONTENT or tSTRING_DBEG or tSTRING_DVAR or tSTRING_END
# [].method(:||)
# ^
Yeah, definitely not a method.
If you have been following along in pry
,
you will find that I lied about most of the virtual machine instructions above. Ruby actually does some optimization at compile time to
get rid of instructions that will never get executed.
Since symbols are always truthy, the examples above actual compile as follows:
puts RubyVM::InstructionSequence.compile(':a || :b').disasm
# 0002 putobject :a
# 0004 leave
puts RubyVM::InstructionSequence.compile(':a && :b').disasm
# 0002 putobject :b
# 0004 leave
code = <<-RUBY
if :a
:a
else
:b
end
RUBY
puts RubyVM::InstructionSequence.compile(code).disasm
# 0002 putobject :a
# 0004 leave
Thanks, Ruby!
There is still room for improvement though. This gets optimized:
code = <<-RUBY
if [1]
[1]
else
[2]
end
RUBY
puts RubyVM::InstructionSequence.compile(code).disasm
# 0002 duparray [1]
# 0004 leave
But for some reason this doesn’t:
puts RubyVM::InstructionSequence.compile('[1] || [2]').disasm
# 0002 duparray [1]
# 0004 dup
# 0005 branchif 10
# 0007 pop
# 0008 duparray [2]
# 0010 leave
If you like this sort of thing, you should check out Ruby Under a Microscope