Simply put, this adds "restarts" to the Exception
class which allows exception raisers to specify multiple strategies for recovering from the exception. The strategies are attached to the Exception instance which travel as the stack unwinds to the enclosing rescue
clause higher up on the stack. That rescue clause can then invoke a recovery strategy.
What is special is that when the recovery strategy is invoked, the stack is recreated to the point in time where the Exception was raised and the recovery strategy is invoked in that context, as if the exception were never raised to begin with. This allows code higher up in abstraction to inject recovery into low-level code making the low-level code more reusable.
In pseudocode:
def inner_function
raise
<specify some restarts here>
end
def outer_function
inner_function # will cause an exception with restarts
rescue Exception => e
# here we specify a recovery and we are able to repair the lower
# level code as if it never threw an exception to begin with
e.restart <strategy id>
end
In addition, restarts can accept parameters to affect their implementation. In this way, when a restart is invoked, some state can be passed to it, affecting its execution.
We should be be able to specify restart clauses using a nice, concise language.
Here are some examples:
raise Exception, "Could not do the thing!" do
restart :ignore do
# simply doing nothing ignores the error
end
restart :replace_entry do |replacement|
replacement
end
end
We should be able to catch an exception from below, and add our own strategies to it
Here is an example:
def rethrow
should_retry = false
inner_call
rescue Exception => e
e.add_restarts do
restart :retry do
should_retry = true
end
end
retry if should_retry
end
Here is an example:
def rethrow
inner_call
rescue Exception => e
e.add_restarts do |super_restarts|
restart :fix do
super_restarts[:fix].call
end
end
end
Like LISP, it would be nice to be able to handle these in the debugger by default. The debugger should be able to specify a nice interface to the user by outputting messages from the metatdata associated with a restart strategy (like a description of what it does and what parameters it will take)
chosen = raise "can't to the thing" do
restart :ignore do
5
end
end
puts chosen # => [:ignore, 5]
I'm not sure if this is totally needed and think it's a separate item, but here's what it might look like:
class Exception
attr_reader :restart_data
def initialize(m, message = "", c = callback, options={}, &restart_block)
@restart_data = options[:restart_data]
end
end
- #exit, #resume (like ignore, it means just continue as if it hadn't been thrown)
- #outer (reraise with the same strategy identifier except that now returns to this point)
- #pass (reraise the exception)
- #resignalAs:. Raise a different class of exception in place of the current exception, as if the new class of exception had been raised in the first place.
- #retry (note: not sure how we can make this a built-in)The try-block associated with the handler (i.e. the receiver of the #on:do: to which it is the last argument) is re-evaluated. Of course it is pointless retrying if the same exception will be raised, and this is an easy way to create an infinite loop (though Ctrl+Break should #retryUsing: Substitute the argument as the new try block, and #retry. This has particular application for operations which have fast implementations for commonly used execution paths, and slower implementations for less common usages. get you out of trouble). - Super-interesting. Wonder if we might try it.
So a given Exception class would have a documented set of recoveries (they refer to it as a protocol)
One of the difficult problems to solve is, what should #raise return? Currently, it returns the array of the restart chosen and the result of the restart block. I really like that we can get the result of the restart block, but I really don't like that methods written with raise that are ignored suddenly implicitly return an array. Yet to implement a property retry, it's important to know which restart was executed. I'd like the cleanliness of #raise returning only the return value of the block and the cleanliness of being able to know which restart was chosen.
One solution is to have #raise return the value straight through and use a thread-local global variable that holds the id of the last invoked restart. So far, the only use case I've seen that requires that information is to do a retry inside a rescue clause.