This is part 1 of a series on Weird Ruby. Don’t miss Weird Ruby Part 2: Exceptional Ensurance, Weird Ruby Part 3: Fun with the Flip-Flop Phenom, and Weird Ruby Part 4: Code Pods (Blocks, Procs, and Lambdas).
This is the first in a series of posts inspired by my recent speaking engagement at the Keep Ruby Weird conference in Austin, Texas. I’ve been using Ruby full-time for several years and have seen plenty of things along the way that made me scratch my head. As I’ve gotten to know a bit more about the language, the reasons for some of those design decisions have become clearer to me, and I’d like to share some of that perspective.
This series is about some of the unusual and potentially counterintuitive behaviors of Ruby, particularly those that surprise new developers or newcomers from other languages. It’s not intended to be yet another list of ‘WTF Ruby?!’ code samples. Rather, I hope to present an alternative view of some of these behaviors and why they are in fact important to how we use the language.
Enter the begin-end block, a seemingly innocuous control structure that usually doesn’t affect much at all in your code. Combined with
rescue it can collect and handle errors, and if we add an
else to the rescue we can run code when we don’t see errors. Begin-end blocks can also be used with an ‘ensure’ block to guarantee that some code will run. It’s when you combine them with conditionals that… unexpected things start to happen. Let’s start at the beginning.
Reticulating the splines
The begin in a lone begin-end block doesn’t really begin anything, nor does the end actually end anything. Wrapping code in a begin-end block alone doesn’t change the behavior of your code at all, which itself is quite weird.
Have a look at this example:
We have some splines that need reticulating, as they often do, and we’re going to wrap them in a begin-end block and see what changes:
begin @splines.reticulate end
All of the behavior for the code in these examples is the same. The return value is the same and the bytecode generated is almost exactly the same.
How do we know that? You can use a RubyVM::InstructionSequence to get a peek at the bytecode for your Ruby. This is the bytecode for our reticulating splines example:
0000 trace 1 ( 1) 0002 getinstancevariable :@splines, 0005 opt_send_simple <callinfo!mid:reticulate, argc:0, ARGS_SKIP> 0007 leave
0000 trace 1 ( 1) 0002 trace 1 0004 getinstancevariable :@splines, 0007 opt_send_simple <callinfo!mid:reticulate, argc:0, ARGS_SKIP> 0009 leave
Ruby uses a stack-based virtual machine (VM), so values are pushed onto the stack and then pulled off one at a time to act on the next value, until we reach the beginning state and return the result.
The first instruction is a trace, which we’ll get back to in just a moment. The second instruction is
getinstancevariable :@splines, which will do about what you expect: it will get the value of
@splines. The next
opt_send_simple line sends the
reticulate message to the
@splines value, and the final leave instruction tells us we’re all done.
That trace instruction we skipped over earlier enables Ruby’s TracePoint functionality. You can use TracePoint to run some code for each statement in your program. Notice that the second begin-end example actually has an extra trace instruction; that trace is for the begin-end block. The TracePoint feature is a great deal of fun, go check out the documentation and play around. Just remember not to do that sort of thing in production, it will slow down your application considerably.
Will the splines reticulate?
I think we’ve pretty well established that begin-end blocks by themselves don’t do much, so let’s see what happens when we use them with some conditionals. Time for the ‘Will it reticulate?’ game:
@splines.reticulate if false
Will it reticulate? Absolutely not, those splines are dead in the water and our ship is likely to crash because of it. That if conditional functions exactly as you’d think it would.
How about this?
begin @splines.reticulate end if false
No? You’re correct again. If we don’t get reticulating soon we’re doomed. Lets see if this will reticulate:
@splines.reticulate while false
Unfortunately not. Well, if that didn’t work, surely this won’t work either:
begin @splines.reticulate end while false
Surprise! This will actually reticulate our splines. Just once, but everyone knows once is enough for a good set of splines.
Here we’ve stumbled across Ruby’s do-while construct, the begin-end-while loop. This is a post-condition loop: it checks the loop condition only after processing the body of the loop. The code above will reticulate the splines once, then check the boolean post-condition and determine that it should no longer reticulate.
More often than not, this begin-end-while loop surprises the hapless developer who stumbles upon it, as begin-end blocks in and of themselves are not generally expected to change behavior (as we saw before). Matz himself has said that he regrets this behavior in Ruby, and that he would much prefer developers use something like this instead:
loop do @splines.reticulate break if true end
The effect of the suggested loop-break construct is exactly the same: we’ll always reticulate the splines exactly once. The only difference is that the loop code is much less likely to confuse.
Tune in soon for more weird Ruby and we’ll look at some other potentially unexpected behavior: the possibility that your
ensure block will not actually ensure anything.
Ruby image courtesy of Shutterstock.com.