"In golf, a mulligan is a stroke that is replayed from the spot of the previous stroke without penalty, due to an errant shot made on the previous stroke. The result is, as the hole is played and scored, as if the first errant shot had never been made." -- Wikipedia
Once upon a time, there was a spy who had to infiltrate a 17 floor building, each new floor thick with guards. On the top floor was a safe to which he was given a combination. The safe would blow up if the wrong combination was used so he had to be careful. After sneaking through all the floors, he successfully arrived at the safe and then he realized on his notes the combination read "66-99-66".
He couldn't tell if he was reading it upside-down, and because radios hadn't yet been invented, Intelligence couldn't be contacted. Not knowing what to do he bailed on the mission by jumping out the window and was rescued on the ground by the allies. They told him he was indeed holding the combination upside-down but now he'd have to again go through all 17 floors.
This the current state of Ruby exception handling. Once an exception is raised, you "abort the mission" and jump out the window where you are rescued.
... but then you have to start the mission again.
Here's the story again, but let's pretend radios now exist:
Once upon a time, there was a spy ... It was then that he realized the combination he was given was 66-99-66.
Because this mission now includes radios, he was able to call intelligence, tell them what was happening and they told him he was holding the note upside-down. He then continued the mission by turning the note right-side-up and opening the safe.
The Mulligan gem adds the radio to your exception handling. The Ruby
rescue clause is like 'Intelligence' who receives the call (as an Exception instance), but now attached to that Exception instance are 'recovery objects' which contain data about how to solve the problem. By invoking a recovery object, the code continues to exit without the mission aborting.
Here's a very simple contrived example, simply to show flow control:
require 'mulligan' def calling_method method_that_raises "SUCCESS" rescue Exception puts "RESCUED" recover IgnoringRecovery puts "HANDLED" end def method_that_raises puts "RAISING" case recovery when IgnoringRecovery puts "IGNORING" else raise "You can ignore this" end puts "AFTER RAISE" end
Running this at the REPL shows:
2.0.0-p353 :009 > calling_method RAISING RESCUED IGNORING AFTER RAISE => "SUCCESS"
Notice that we did't see "HANDLED" in the output? Here's what happened in detail:
recovery is used at the beginning of a
case structure to indicate that each
when clause is defining a
Recovery instance to be attached to the next raised
Here is the structure for using it:
case recovery when <recovery_class> # or ...code when <recovery_class>.new(...) # or ...code when <method_or_statement_that_returns_recovery_instance_or_class> ...code else raise <exception> end
The structure for this has to be quite strict. You have to put the
raise inside the
else. (For more explanation, see the Appendix)
You can also call
recovery(<recovery_class>) when inside a rescue statement to see if there is a recovery attached to the exception that fits that class.
rescue Exception => e if !recovery(IgnoringRecovery).nil? ... end
rescue clause, this invokes the recovery object. There is no return value from this. Code execution now proceeds back down into the stack to to the location in the case statement that matches the recovery class.
rescue Exception => e recover(IgnoringRecovery)
Here is an example of passing arguments:
rescue Exception => e recover(IgnoringRecovery, "I'm saving you")
Here is an example of retrieving them:
begin case r = recovery when IgnoringRecovery puts r.argv # will output "I'm saving you" else raise end
Mulligan::Recovery is the base class of all recoveries. Use this in the same way you use the
Exception hierarchy, but for recoveries. You can define your own subclasses with different properties that can be read by the
One thing to note. There is useful metadata associated with a recovery. This is because if you are running your code inside Pry using pry-rescue and an exception is raised uncaught, Pry will open and you can choose a recovery from the list attached to the exception. Your program will then continue as if the exception were never thrown.
Here is the metadata:
This is a human-readable description one-line descriptions of what the
Recovery does. By default it returns the class attribute, but you can override it in the instance.
This is a detailed discussion of how to use the recovery. Think of it as if you were writing help for a command-line tool and wanted to describe the options and arguments. By default it returns the class attribute, but you can override it in the instance.
If you call
Exception#recoveries.inspect inside Pry, you will get a string that looks more or less like this:
Mulligan::RetryingRecovery -------------------------- Performs again the last task which caused the failure. Attributes: 'count' - The number of times this recovery has been invoked. In this way, you can keep track of how many times the code has been retried and perhaps limit the total number of retries. Mulligan::IgnoringRecovery -------------------------- Ignores the exception and continues execution. If this recovery is attached to an Exception, you may safely continue.
On all other "compatible" rubies, Mulligan will gracefully degrade to standard exception handling. Though the API will be there, no recoveries will be attached to exceptions. Any calls to the Mulligan API will "pass-through".
This diagram shows what happens to code when running on fully supported Ruby vs. a "compatible" Ruby. Faded code is non-operational or unreachable.
Because of this, adding recoveries to your code is all gravy. By adding recoveries, you are simply making your library more useful on supported rubies and on unsupported rubies, you merely have what you always had.
The truth is, often when we throw an exception in code, we probably could actually continue if we just knew what to do. Specifying recoveries allows you to suggest some options to the rescuing code.
Not only that, you can apply a recovery strategy to large parts of code by handling exceptions at a high level and recovering from them.
From the Dylan Language Manual:
A condition is an object used to locate and provide information to a handler. A condition represents a situation that needs to be handled. Examples are errors, warnings, and attempts to recover from errors.
A "condition" is similar to what we are call "exception" in Ruby except that in Dylan and Lisp, conditions don't always represent errors, but are just a way to send messages higher-level code.
def http_post(url, data) ... networking code... raise CredentialsExpiredException if response == 401 raise ConnectionFailedException if response == 404 end def post_resource(object) ... assemble url and data... http_post(url, data) rescue Exception => e case recovery when RetryingRecovery retry else raise e end end def save_resources post_resource(user) post_resource(post) post_resource(comment) rescue CredentialsExpiredException => e ... fix credentials... recover RetryingRecovery rescue ConnectionFailedException => e ... switch from wifi to cellular... recover RetryingRecovery end
This is going to be inherently messy and for a long-running program like this, potentially painful to restart if the data is found to be incorrect. Much better to just put in some recoveries and choose from them if errors are found.
You might write a parser to read XML or a log file format and it might encounter malformed entries. You can make that low-level parser code much more reusable if you specify a few recoveries in the raised exceptions. Higher level code will have many more choices to handle errors.
You've always known he (or she) knew Lisp and now you have something to ask him about.
I had to make a hard choice about naming the thing that allows an exception to be recovered from. "Restart" is the word used in Lisp, but because it is used as a verb and as a noun, it makes it hard to know what a Ruby method named
#restart would do. Does it return a "restart" or does it execute a restart?
Changing the name to a noun subtracts that confusion (though arguably adds some back for those coming from languages where the "restart" name is entrenched).
No. If an exception didn't have recoveries attached when it was raised, you will not be able to call them. It is incumbent on the code that raises the exception to add the recoveries so they can control the error-handling flow.
I had to pull off some tricks to achieve the
case structure in Mulligan. If I had more control over the Ruby Language, my preferred syntax for specifying recoveries would be:
raise [Exception [, message [, backtrace]]] # ... code that is always executed during a recovery recovery <Recover class> # ... recovery code recovery <Recover class> => args # ... recovery code that uses the args passed back end
Add this line to your application's Gemfile:
And then execute:
Or install it yourself as:
$ gem install mulligan
git commit -am 'Add some feature')
git push origin my-new-feature)
Show off your Mulligans! Feel free to add the following html to your repo...
<a href="http://github.com/michaeljbishop/mulligan"><img src="https://github.com/michaeljbishop/mulligan/raw/master/images/mulligan-badge.png" height="47" width="66" alt="Mulligan"></a>