Leo
Leo

Reputation: 1443

How to return true if one statement in a block returns true in Ruby?

Is it possible to create a method in Ruby that takes a block and runs each statement in the block until one of them returns false?

done = prepare_it do |x|
  method_1
  method_2
  method_3
end
puts "Something did not work" unless done

I wanted the function prepare_it to run each statement but if one of them fails, the function quits. If the function returns true, it means that all steps were successful.

This seems to be calling for an Exception, but I'm not sure how to trigger and process it. I wish I did not have to modify the method_x functions to throw an Exception upon failure, but rather make prepare_it throw the exception if one of the method_x fails (returns false)

Edit: Thanks for the suggestions, I think I need to add more details to my question. This is my first stab at metaprogramming and creating a DSL. :)

I have a class to send commands to a router. There are many steps that always need to be executed in sequence (connect, login, pass, etc). I thought it would be nice to give the user the ability to change the order of the commands if they wish to do so. For example, some routers don't ask for a user and go directly to the password prompt. Others skip the login altogether. In some cases you need to elevate the privilege or set terminal options.

However, there should be a mechanism to fail the entire block if any of the commands failed. So, if for some reason the password failed (the method returns false/nil), it makes no sense to continue with the rest of the commands. And I should flag it that something failed.

The && method works, but I don't think it would be a nice interface.

Maybe instead of getting a big block, maybe I should force users to give me smaller blocks, one at a time, which are put in a stack and a run command yields then one by one?

my_router.setup do {send_pass}
my_router.setup do {set_terminal}
my_router.setup do {enable_mode}
my_router.run_setup

I think it would be super cleaner if I could do

my_router.setup do |cmd|
  cmd.send_pass
  cmd.set_terminal
end
puts "Done" if my_router.ready?

So any magic trickery that happens behind the scene is welcome! :)

Upvotes: 3

Views: 5275

Answers (5)

ohspite
ohspite

Reputation: 1319

At first I thought, "wouldn't it be nice if this was Lisp..." but that made me realize the exact problem. Doing what you want (executing each step in a block until one is false) would be nice, but requires the access to the AST.

Your idea to do

my_router.setup do {send_pass}
my_router.setup do {set_terminal}
my_router.setup do {enable_mode}
my_router.run_setup

is trying to do exactly what you would do in Lisp--build a list and hand the list off to something else to execute each thing until you get a false return value.

If you're just calling methods in your class without any arguments, then how about as a workaround defining something that takes symbols?:

class Router
  def run_setup *cmds
    # needs error handling
    while c = cmds.shift
      return false unless send(c)
    end
  end
end

my_router.run_setup :send_pass, :set_terminal, :enable_mode

Upvotes: 0

Pablo Fernandez
Pablo Fernandez

Reputation: 105220

Solution

done = prepare_it do |x|
  method_1 && method_2 && method_3
end
puts "Something did not work" unless done

Explanation

The && operator "short-circuits" so if method_1 returns false, 2 and 3 won't be called and done will be false.

Example

http://gist.github.com/1115117

Upvotes: 9

gunn
gunn

Reputation: 9165

Naturally compactness will be your highest priority, so I believe this is the best answer:

done = (1..3).all? { |n| send "method_#{n}" }
puts "Something did not work" unless done

Upvotes: 1

Jeremy Roman
Jeremy Roman

Reputation: 16355

There isn't a good way to hack things to work like that. I would do something like one of the following:

done = prepare_it do |x|
  method_1 && method_2 && method_3
end

Now, in some specific cases, you can do magic trickery to make this work. For instance, if the methods are supposed to be called on the block argument x: you could do something like this:

class SuccessProxy
  def initialize(obj)
    @object = obj
  end

  def method_missing(meth, *args, &block)
    @object.send(meth, *args, &block) || raise SuccessProxy::Exception
  end

  class Exception < ::Exception
  end
end

def prepare_it
  # however you usually generate x
  yield SuccessProxy.new(x)
rescue SuccessProxy::Exception
  false
end

prepare_it do |x|
  x.method_1
  x.method_2
  x.method_3
end

Or alternatively, if the methods are to be called just like method_1 on the default object in context, you could use instance_eval instead of yield to put the proxy in scope.

Finally, if you really wanted to be fancy, you could actually parse the separate statements out using a Ruby parser.

But honestly, I'm not convinced you really want to be going to metaprogramming as a tool for this. Maybe if you provide better detail, I can be more helpful as to what the best solution is, but it's probably similar to the first code sample above.

Upvotes: 1

Ted Kulp
Ted Kulp

Reputation: 1433

If you use exceptions, it's not as pretty, but you don't have to rely on returning values:

done = prepare_it do |x|
  begin
    method_1
    method_2
    method_3
    true
  rescue
    false
  end
end
puts "Something did not work" unless done

def method_1
  # No issues, doesn't matter what we return
  return true
end

def method_2
  # Uh-oh, problem
  raise
end

Upvotes: 0

Related Questions