Forum: Ruby [ANN] Mulligan gem released

57b1b986894b511ad713b4f7a2d15165?d=identicon&s=25 Michael B. (michael_b)
on 2014-03-13 01:43
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

mbtyke@gmail.com
numerical-garden.com
0e6ac58dab6125c1cd2e7ac645076b6f?d=identicon&s=25 Joel VanderWerf (Guest)
on 2014-03-13 02:09
(Received via mailing list)
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...

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.
57b1b986894b511ad713b4f7a2d15165?d=identicon&s=25 Michael B. (michael_b)
on 2014-03-13 02:31
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
0e6ac58dab6125c1cd2e7ac645076b6f?d=identicon&s=25 Joel VanderWerf (Guest)
on 2014-03-14 07:09
(Received via mailing list)
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
57b1b986894b511ad713b4f7a2d15165?d=identicon&s=25 Michael B. (michael_b)
on 2014-03-15 12:02
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 `#raise`raises.

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
57b1b986894b511ad713b4f7a2d15165?d=identicon&s=25 Michael B. (michael_b)
on 2014-03-18 05:04
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
0e6ac58dab6125c1cd2e7ac645076b6f?d=identicon&s=25 Joel VanderWerf (Guest)
on 2014-04-01 01:32
(Received via mailing list)
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
57b1b986894b511ad713b4f7a2d15165?d=identicon&s=25 Michael B. (michael_b)
on 2014-04-01 01:36
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/...


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
Please log in before posting. Registration is free and takes only a minute.
Existing account

NEW: Do you have a Google/GoogleMail, Yahoo or Facebook account? No registration required!
Log in with Google account | Log in with Yahoo account | Log in with Facebook account
No account? Register here.