Blog
0 pixels scrolled
  • Home
  • Work
  • Services
  • About
  • Blog
  • Contact
Statements and Expressions in Ruby
Jared Norman on August 20, 2018
Find out the difference between statements and expressions in Ruby, with a concrete, but highly unusual example.
Statements and Expressions in Ruby
Jared Norman on August 20, 2018
Posted in Ruby
← Back to the blog

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.

Statements and Expressions

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.

The Statements

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.

Default Argument Magic

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.

Conclusion

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.

Afterword

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;
}
Work ServicesAboutBlogCareersContact


Privacy policyTerms and conditions© 2018 Super Good Software Inc. All rights reserved.