1

I'm trying to define a DSL where rules (for the sake of this example, the rules define whether something is "good" or "bad") are specified in a block in Ruby. The following is a (grossly simplified) version of what I want to do:

def test_block
  # Lots of other code
  is_good = yield   # ... should give me true or false
  # Lots of other code
end

test_block do
  good if some_condition
  good if some_other_condition
  bad
end

Is there any way I can define methods good and bad that make the block break? In the above example, I want to:

  • check if some_condition is true, and if it is, break out of the block and have it return true
  • check if some_other_condition is true, and if it is, break out of the block and have it return true
  • return false from the block unconditionally if we're still in it

i.e. I want to make the above code behave as if I had written the block like so:

result = test_block do
  break true if some_condition
  break true if some_other_condition
  break false
end

Putting break in the definition of the good/bad method obviously doesn't work. Is there some other way of achieving my desired result or should I think about some entirely different way of going about this?

1

1 Answer 1

3

You could raise an exception in the block and catch that exception.

module Tester
  class Breaker < Exception; end
  class GoodBreak < Breaker; end
  class BaadBreak < Breaker; end
end

def test_block(name)
  begin
    yield
  rescue Tester::Breaker=>e
    case e
      when Tester::GoodBreak then puts "All is well with #{name}"
      when Tester::BaadBreak then puts "BAD STUFF WITH #{name}"
      else raise
    end
  end
end

def good; raise Tester::GoodBreak; end
def bad;  raise Tester::BaadBreak; end

test_block('early out') do
  good if true
  good if puts("NEVER SEE THIS") || true
  bad
end

test_block('simple pass') do
  good if false
  good if puts("SEE THIS FROM PASS TEST") || true
  bad
end

test_block('final fail') do
  good if false
  good if puts("SEE THIS BUT PUTS IS NIL")
  bad
end

#=> All is well with early out
#=> SEE THIS FROM PASS TEST
#=> All is well with simple pass
#=> SEE THIS BUT PUTS IS NIL
#=> BAD STUFF WITH final fail

Here's another example using throw/catch (thanks @jleedev!) instead of raise/rescue (updated to pass a return value along):

def test_block(name)
  result = catch(:good){ catch(:bad){ yield } }
  puts "Testing #{name} yielded '#{result}'", ""
end

def good; throw :good, :good; end
def bad;  throw :bad,  :bad;  end

test_block('early out') do
  good if true
  good if puts("NEVER SEE THIS") || true
  bad
end

test_block('simple pass') do
  good if false
  good if puts("SEE THIS FROM PASS TEST") || true
  bad
end

test_block('final fail') do
  good if false
  good if puts("SEE THIS BUT PUTS IS NIL")
  bad
end

#=> Testing early out yielded 'good'
#=> 
#=> SEE THIS FROM PASS TEST
#=> Testing simple pass yielded 'good'
#=> 
#=> SEE THIS BUT PUTS IS NIL
#=> Testing final fail yielded 'bad'
Sign up to request clarification or add additional context in comments.

3 Comments

This might be a better application of throw/catch, since you’re using them for flow control. (Or call/cc. Heh.)
@jleedev Right you are! It's been too long since I used those. Here is introductory documentation on using throw/catch from the free first edition of Programming Ruby.
Thanks @Phrogz and @jleedev - I thought about using raise but kinda scared away from the thought after a lifetime of being told that exceptions are for "very bad situations"(tm) only ;) But the throw/catch way of doing it feels a lot less wrong!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.