Reputation: 1443
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
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
Reputation: 105220
done = prepare_it do |x|
method_1 && method_2 && method_3
end
puts "Something did not work" unless done
The &&
operator "short-circuits" so if method_1
returns false
, 2 and 3 won't be called and done
will be false
.
http://gist.github.com/1115117
Upvotes: 9
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
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
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