[ANN] Mulligan gem released

Hello all!

The first public release of ‘mulligan’ has been posted to RubyGems

http://michaeljbishop.github.io/mulligan

If you want a taste of what exception-handling is like in other dynamic
languages like LISP, Smalltalk, and Dylan, give it a look. It’s just
plain-old ruby exceptions, with one twist that changes everything.

As this is the first pubic release, I’m interested in comments,
especially from people who have significant experience with LISP
“conditions+restarts”.

Hope you enjoy it!

_ michael

[email protected]
numerical-garden.com

Joel VanderWerf wrote in post #1139667:

On 03/12/2014 05:43 PM, Michael B. wrote:
Maybe matz’s comment explains why it hasn’t been adopted before.

matz wrote:

… “resume” makes exception handling much harder. With “resume”,
every raise can be re-entered, that means programmers need to care
about re-entrance always.

So allowing new thing is not always a good thing.

Thank you for the link. It’s always good to see some of the previous
discussion. I think I can address that concern.

If every exception was resumable, I’d agree that there would be a
problem because code that wasn’t written to be re-entered suddenly could
be. However, to be clear, there is no automatic “resume” in a mulligan
exception. The code that is at the raise site is (and should be) in
complete control of how the exception can be recovered. If there are no
recoveries attached, the exception cannot be resumed.

Here’s some example code for a resumable exception.

raise “Test” do |e|
e.set_recovery(:resume) do |*args|
puts “resumed!”
end
end

This code is prepared for the exception to be resumed because it has
explicitly said that it can be.

Here’s the same code, but this exception cannot be resumed as it does
not have a recovery attached.

raise “Test”

This code need not be worried that it will be re-entered. It cannot be.

Check out the sample code on the website for more detail. (The github
site’s source code is a little easier to read so I’ll post that)

https://github.com/michaeljbishop/mulligan

I’m happy to address any other concerns or questions!

Still interesting, though. Would be good to hear from anyone who’s
designed a program from the ground up to use resumable exceptions.

Totally agree. I only know maybe one person who knows this deep. I’d
love to hear from more!

_ michael

On 03/12/2014 06:31 PM, Michael B. wrote:

https://github.com/michaeljbishop/mulligan

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

On 03/12/2014 05:43 PM, Michael B. wrote:

As this is the first pubic release, I’m interested in comments,
especially from people who have significant experience with LISP
“conditions+restarts”.

Similar ideas have been discussed before on ruby-talk:

http://compgroups.net/comp.lang.ruby/re-retry-does-not-work/736757

Maybe matz’s comment explains why it hasn’t been adopted before.

matz wrote:

… “resume” makes exception handling much harder. With “resume”,
every raise can be re-entered, that means programmers need to care
about re-entrance always.

So allowing new thing is not always a good thing.

Still interesting, though. Would be good to hear from anyone who’s
designed a program from the ground up to use resumable exceptions.

Joel VanderWerf wrote in post #1139799:

On 03/12/2014 06:31 PM, Michael B. wrote:

https://github.com/michaeljbishop/mulligan

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

On 03/17/2014 09:04 PM, Michael B. wrote:

to rescue clauses attached to Exceptions.
2. Defining the recoveries that are attached to exceptions happens in a
case statement (as I remarked in my previous email)
3. Recovering takes place inside a rescue clause by way of a ‘recover’
statement.

I had a little trouble running the first example in the README from the
command line. The slightly modified, working example is below:

require ‘mulligan’

class IgnoringRecovery < Mulligan::Recovery; end

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

p calling_method

Hi Joel,

I made the changes to the Mulligan gem and you might be interested in
these. I’ve completely changed the syntax now and I think it fits in
more nicely with Ruby.

Here are the big changes:

  1. There are now “Recovery” objects. They are like Exceptions, but for
    recovering from exceptions. They contain the metadata and are sent back
    to rescue clauses attached to Exceptions.
  2. Defining the recoveries that are attached to exceptions happens in a
    case statement (as I remarked in my previous email)
  3. Recovering takes place inside a rescue clause by way of a ‘recover’
    statement.

Here’s an example:

def download_payload(p)
  ... some networking code...
rescue TimeoutException
  case recovery
  when RetryRecovery
    retry
  else
    raise #re-raises current exception
  end
end

def download_all_payloads
  payloads.each do |p|
    download_payloads(p)
  end
rescue TimeoutException
  recover RetryRecovery
end

What do you think?

I’m not totally satisfied with the case-statement way, but I don’t have
any more control over the language. If I did, this is what I’d want it
to look like:

  raise [Exception [,message [,backtrace]]]
  recovery RetryRecovery
    retry
  end

Anyway, I hope that’s interesting to you. You can see the code in a
separate branch at:

https://github.com/michaeljbishop/mulligan/tree/case-syntax

Sincerely,

_ michael

Thanks Joel!

I’ll fix that up ASAp. The IgnoringRecovery a built-in so it should be
included simply by requiring ‘mulligan’. I’ll make an example file and
add it explicitly to the source base. I appreciate your notifying me.

_ michael

PS. There’s some good stuff coming soon…

https://github.com/michaeljbishop/mulligan/commit/4d22757ac7822ee16a8fc68b5f56683cdb051bbe

Joel VanderWerf wrote in post #1141615:

On 03/17/2014 09:04 PM, Michael B. wrote:

to rescue clauses attached to Exceptions.
2. Defining the recoveries that are attached to exceptions happens in a
case statement (as I remarked in my previous email)
3. Recovering takes place inside a rescue clause by way of a ‘recover’
statement.

I had a little trouble running the first example in the README from the
command line. The slightly modified, working example is below:

require ‘mulligan’

class IgnoringRecovery < Mulligan::Recovery; end

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

p calling_method

This forum is not affiliated to the Ruby language, Ruby on Rails framework, nor any Ruby applications discussed here.

| Privacy Policy | Terms of Service | Remote Ruby Jobs