Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are there unintended consequences of Ruby's `begin ... end` without `rescue` used as a code block?

Tags:

I occasionally see begin...end blocks used in ruby without any rescue, else, ensure, etc. statements in between. For example:

foo = begin    whatever = 3    "great"    42 end 

The coder's intent, it seems, is to use the begin...end block just for its block-grouping quality (as if begin were do). Personally I think this usage kind of violates the principle of least surprise (begin implies exception-handling to me).

Are there any unintended consequences of using begin...end in this way? Do begin...end blocks have any semantic differences (maybe in exception-handling?) that make this usage dangerous?

Ruby's syntax is unbelievably subtle, and I wouldn't be surprised if there were weird gotchas lying in wait here.

like image 758
pje Avatar asked Nov 07 '12 22:11

pje


1 Answers

I use this sometimes if I want to assign something to a variable but I have to calculate the value I want to assign first. It makes the code a little bit more tidy this way. I think it's user preference. Basically you are saying: I am assigning something to foo, but in order to get the value I want I first need to do some things. It's particularly useful when doing memoization, so instead of

if @cache.nil?   do_something!   @cache = read_value end 

You can do

@cache ||= begin   do_something!   read_value end 

What you are taking advantage here is that the Ruby interpreter has a stack, and each expression will usually push something on the stack, or take something from the stack. Assignment just takes the last thing from the stack and assigns it (in this case the last line from begin/end). Many times knowing this (stack approach in Ruby) can be useful.

I don't think it violates least surprise though, I think it's user preference wheather you want to use it or not.

You can see that it doesn't do anything unexpected by looking at what bytecode instructions it generates in Ruby MRI 1.9:

 RubyVM::InstructionSequence::compile("c = begin; a = 5; 6; end").to_a   [:trace, 1],  [:trace, 1],  [:putobject, 5],  [:setlocal, 2],  [:trace, 1],  [:putobject, 6],  [:dup],  [:setlocal, 3],  [:leave] 

Trace is just for stack traces, you can ignore that. Dup duplicates the last item on the stack. In this example the number of the local variable a is 2 and the number of the local variable c is 3 (hence putobject, 2 will assign to variable a, etc). The only side-effect of this compared to a = 5; c = 6 is the dup instruction, which means the stack size of your method will be larger by 1 slot. But this is not particularly important because it only has any effect while the interpreter is inside this particular method, and memory for stack is pre-reserved anyway, so it only means the stack pointer will be decremented by 1 more than it would otherwise. So basically no change at all. With optimizations turned on even the dup will probably disappear.

like image 97
mrbrdo Avatar answered Oct 04 '22 08:10

mrbrdo