Recently, a reddit user was looking for an explanation for a very unusual piece of code:
def clone(opts = (return self; nil))
# ...
end
This code is from the Sequel gem. It’s a great gem for working with databases, but that’s a pretty strange looking default value. To understand exactly why it is the way it is, we need to understand the difference between statements and expressions in Ruby.
It’s worth noting that there are good explanations in the comments on Reddit too, if you want to piece together what’s going on from them.
Most programming languages have a concept of “expressions” and “statements”. The main difference is this: expressions evaluate to a value, but statements do not. In Ruby, unlike JavaScript, all our conditional and looping structures are expressions:
a = if false
:yay
else
:nay
end
#=> :nay
b = case []
when Array then 10
when Hash then 5
else
3
end
#=> 10
c = loop do
break :potato
end
#=> :potato
Ruby programmers sometimes get tripped up in JavaScript where none of this is possible. I’ve heard Ruby programmers say that “everything is an expression”, but that’s not strictly true.
Ruby does have a few pieces of syntax that are statements. As far as I know, these are the only statements in Ruby: return
, retry
, redo
, next
, break
and alias
.
The first five will all cause a SyntaxError
due to a “void value expression” if you attempt to use them in place of an expression:
x = return
SyntaxError: (eval):2: void value expression
alias
is slightly different, but also a SyntaxError
:
x = alias boop puts
SyntaxError: unexpected keyword_alias
The distinction between these syntax errors is important. Whereas alias
has to be used in a class context, void value expressions can be used anywhere you’d use an expression, so long as the result isn’t accessible in any way.
Back to our unusual code snippet. When Ruby needs the default value of an optional argument, it evaluates the expression provided.
This trick depends on the fact that the default value must be an expression, and that it will be evaluated every time it is needed.
class X
end
def some_method(x = X.new)
x
end
some_method
#=> #<X:0x00007feba10331c8>
some_method
#=> #<X:0x00007feb9ee8c8a8>
Each call re-evaluates the default value, and gives us a new object. It turns out that those default values are evaluated in the same context as the rest of the method. Ruby’s syntax only requires that the default value be an expression.
We can’t write def clone(opts = return self)
because return
is a void value expression and we can’t try to use its return value. In order to trick the Ruby interpreter into letting us do something equivalent, we just put a second expression after it, which will never be reached.
def clone(opts = (return self; nil))
# ...
end
When Ruby encounters a call to this method with no arguments, it evaluates the valid expression (return self; nil)
, and immediately returns from the method after evaluating the return
statement.
The end result is that this method returns the object itself when clone
is called with no arguments. Take a second to consider how else you might do that. You could use the splat operator (def clone(*arguments)
) and check the length of the error, but now you’re going to have raise ArgumentError
yourself if there are too many arguments. You could provide a default value of nil
, but then you’ll have no way of knowing if the user intentionally called clone(nil)
expecting the main body of the method to run.
One Reddit user profiled this and similar approaches and found that this default value return is easily the most performant way to do this kind of early return. I’d never do it in my application code, but if it’s inside of a heavily used part of a gem (which it is) I think it’s justified.
Definitely don’t start putting return statements in your default values; you’re just going to confuse everyone. I think we should all try to write “normal code”, and despite how vague that sounds, we can probably all agree this pattern isn’t normal. I’d probably use the phrase “extremely unconventional.” This is only reasonable here because it’s an optimization on a very heavily used method.
The real takeaway is that Ruby has a type of syntax that I haven’t seen in other languages, the “void value expression.” You can use return
and the other void value expressions I listed above anywhere that you can use an expression, as long as the result of the expression is inaccessible.
I expected to discover that raise
was a void value expression, but it isn’t. Raise is just a method, but it’s impossible to get the return value of it. The return value of raise
is actually marked as UNREACHABLE
in the C code, as the exception handling is initiated before we get there.
static VALUE
rb_f_raise(int argc, VALUE *argv)
{
VALUE err;
VALUE opts[raise_max_opt], *const cause = &opts[raise_opt_cause];
argc = extract_raise_opts(argc, argv, opts);
if (argc == 0) {
if (*cause != Qundef) {
rb_raise(rb_eArgError, "only cause is given with no arguments");
}
err = get_errinfo();
if (!NIL_P(err)) {
argc = 1;
argv = &err;
}
}
rb_raise_jump(rb_make_exception(argc, argv), *cause);
UNREACHABLE;
}