4

Because Ruby relies so heavily on the use of blocks for iteration (while and for loops do exist in Ruby, but they're very rarely used by Ruby developers in practice) Ruby allows some keywords often associated with loops in other languages, such as break, next, and redo, to be used inside of blocks. return in Ruby also returns from the method the block is inside, which is consistent with the behavior of a loop, instead of from the block itself as functional programming languages do. This answer explains those behaviors quite nicely.

Now what I'm wondering is how this behavior works from the block caller's perspective (by block caller, I mean the method the block is being called from, as opposed to the method the block is defined in). For some of these keywords it's pretty self-explanatory (e.g. next simply returns from the block, and redo calls the block again with the same arguments), but for others the exact behavior is unclear to me. Specifically...

When I call break from a block, does execution immediately get handed to the point after the block is defined? If I have some post-iteration cleanup steps to perform from within the iterator method, do they get completely skipped? E.g.:

def iterate
  setup
  1.upto(5) do |x|
    yield x
  end
  cleanup # Does this get called? If not, how can I ensure that it does?
end

iterate do |x|
  puts x # Prints 1
  break # Break prevents further looping, somehow
end
# Execution continues here after break

What about with return? I would assume it has similar behavior to that of break in this context (whatever that behavior may be). Is that indeed the case?

What's even more confusing though is that Ruby blocks can be captured as method parameters and converted to Proc objects. These objects can then be called from anywhere, even outside the method that the block was originally defined in:

def get_proc(&block)
  block
end

def get_iterator_proc
  p = get_proc do |x| # Block is defined here...
    puts x
    break
  end
  # ...so `break` makes execution continue here...

  do_other_stuff

  return p
end

p = get_iterator_proc

p.call # ...even though the block isn't actually called until here?

And again, the same goes for return. What does it actually return from in this context? How do these keywords really work?

Community
  • 1
  • 1
Ajedi32
  • 45,670
  • 22
  • 127
  • 172
  • Did you check - http://ruby-doc.org/docs/keywords/1.9/Object.html ? – Arup Rakshit Jun 03 '14 at 19:07
  • @ArupRakshit Oh, so they're actually not keywords but rather instance methods on `Object`? That's interesting... – Ajedi32 Jun 03 '14 at 19:08
  • No they are *keywords*.... Look this http://ruby-doc.org/docs/keywords/1.9/ – Arup Rakshit Jun 03 '14 at 19:09
  • @ArupRakshit Oh, I see. "Yes, I KNOW that they aren‘t methods. I‘ve just put them in that format to produce the familiar RDoc output." Kind of confusing... Is there any similar documentation for Ruby 2.x? – Ajedi32 Jun 03 '14 at 19:14
  • @ArupRakshit I just read those docs as you suggested, and they only seem to address how the keywords work from the block's perspective, not from the block caller's perspective. They also do not address the specific examples I gave at all. – Ajedi32 Jun 03 '14 at 19:15
  • Again for you - https://github.com/ruby/ruby/blob/v2_1_0/NEWS#L170 – Arup Rakshit Jun 03 '14 at 19:19
  • There is a very good blog post.. Just n this topic.. But I am not able to get it now.. forgot where I read about this.. :-) I could answer.. But it is too late here.. And you asked a lots in together although.. If it is short, I could jump on it.. Wait may be someone is writing for you. :-) – Arup Rakshit Jun 03 '14 at 19:22
  • @ArupRakshit Yeah, I was considering splitting this up into 2 separate questions (one about the case where there's extra stuff going on in the iterator after the break or return, and one about the case where the block gets executed after the method it's defined in returns). The stuff in here does seem pretty closely related though, so idk... – Ajedi32 Jun 03 '14 at 19:29
  • @Ajedi32 have you come to an answer for your question? If so, please post your findings and links below. – onebree Oct 19 '15 at 18:41
  • @HunterStevens Sorry, no I haven't. In retrospect though, I could probably figure this out by poking around in IRB a bit... – Ajedi32 Oct 20 '15 at 13:18
  • @Ajedi32 I asked because this is a very interesting question, and I would like to know more. Probably not suitable for StackOverflow, but would be a great essay or blog post. – onebree Oct 20 '15 at 13:19

1 Answers1

1
def iterate
  setup
  1.upto(5) do |x|
    yield x
  end
  cleanup # Does this get called? If not, how can I ensure that it does?
end

iterate do |x|
  puts x # Prints 1
  break # Break prevents further looping, somehow
end
# Execution continues here after break

cleanup # Does this get called?

Yes. However, if there is an exception, then exception handling takes precedence.

cleanup # If not, how can I ensure that it does?

You can use ensure without a rescue, for when there is an exception.

break # Break prevents further looping, somehow

I'd suspect it uses throw as that is how Enumerable works.

What about with return? I would assume it has similar behavior to that of break in this context (whatever that behavior may be). Is that indeed the case?

It depends on the context. You might get a LocalJumpError: unexpected return or it might work as a break

def get_proc(&block)
  block
end

def get_iterator_proc
  p = get_proc do |x| # Block is defined here...
    puts x
    break
  end
  # ...so `break` makes execution continue here...

Effectively, but there is a subtle difference. It is more like your first example.

def get_proc(&block)
  block
  # ...so `break` makes execution continue here...
end

def get_iterator_proc
  p = get_proc do |x| # Block is defined here...
    puts x
    break
  end
  # get_proc returned here (after assigning to p)

Remember, get_proc is just a function call with a special syntax to pass functions.

if you had

def get_proc(&block)
  block
  7
end

Then in get_iterator_proc p would be 7 (And we'd be sad that 7 didn't have a call method )

EnabrenTane
  • 7,428
  • 2
  • 26
  • 44