Joel VanderWerf wrote in post #1139799:
On 03/12/2014 06:31 PM, Michael B. wrote:
GitHub - michaeljbishop/mulligan: Elegant, Ruby-ish Restartable Exceptions for Ruby (like Lisp's "Restarts")
Curious about API design. The basic example is:
require 'mulligan'
def method_that_raises
puts "RAISING"
raise "You can ignore this" do |e|
e.set_recovery :ignore do
puts "IGNORING"
end
end
puts "AFTER RAISE"
end
def calling_method
method_that_raises
"SUCCESS"
rescue Exception => e
puts "RESCUED"
e.recover :ignore
puts "HANDLED"
end
Was there some reason each recovery handler needed to be in its own
block? Could you do this instead, and save a level of nesting?
def method_that_raises
puts "RAISING"
raise "You can ignore this" do |e|
case e
when :ignore
puts "IGNORING"
end
end
puts "AFTER RAISE"
end
That is a good question! In your example, I’m assuming the block passed
to raise is executed after the exception has raised and a recovery has
been chosen and I believe that would work. The one question I have about
your example is, how does the “rescuer” know what recoveries are
available? It seems to me without passing them with the exception, the
rescuer would have no way of knowing.
This is important because the rescuer might be a human. In Lisp, any
unhandled exceptions are brought up in the console where a person can
choose how to handle it (imagine pry-rescue offering you these choices).
This is really handy, but requires some metadata with the recovery, at
minimum, a list of ids. Having a description and other metadata would
make it much better.
However, I also would like to reduce the indentation, especially since
the retry statement cannot be executed inside a block so I have an
alternate proposal:
case c = RuntimeError.chosen_recovery
when :retry
retry
when :ignore
when { substitute_value:
{ summary: "You can pass back a value as the result"} }
return c.args
else
raise c
end
This doesn’t make any sense at first glance, but here’s what’s going on
under the hood.
RuntimeError.chosen_recovery - returns a “recovery matcher”. I’ve
overridden === on both Symbol and Hash so if they compare against
Mulligan::Matcher, they will set a recovery on the matcher using
themselves as the metadata. As part of that, a continuation is taken for
the recovery. Then, === returns false, passing through to the next
case.
At the ‘else’, the raise is called on the Matcher, which has implemented
#exception to return a RuntimeException with attached recoveries,
which #raiseraises.
Now in the rescue, far away… when a recovery is chosen, the saved
continuation is called, which puts us back into the === compare,
except this time, it returns true. Then, the recovery code is
executed and we continue executing after the case.
Additionally, you can see the substitute_value case has summary
metadata and also uses the args passed into the recovery (from the
rescuer).
I’ve prototyped this and it works so I’ll be bringing it in very soon.
Stay tuned…
_ michael