On Wed, March 2, 2005 12:04 am, Jos Backus said:
> Here's an example I came up with today while investigating continuations. Hope
> this is useful to somebody trying to understand and use them. Feedback
> welcome!
Hehe, strange, I was lamenting the state of exception handling on the bus just
yesterday. I was thinking along the same lines, some sort of detached exception
handling mechanism. The handler would be defined elsewhere (so as to not clutter
the method) but would probably have to have access to the method's context.
Ruby's positioned well as far as 'normal' exception handling with having allowed
def ... rescue along with the explicit mode, and particularly with incorporating
'retry' (if you don't try to handle the exception, you could just fail with an
ERRNO anyway:). Now if only the default handling were detached from the method..
It'd be great fun if exception handling could *literally* be described
parallel to the method :)
<pre>
def foo() handler
# Do something
do_something() fail? do fix_do_something(); retry :once; end
# Other ops
if something_else
do_something_else() fail? do fix_do_something_else(); retry 2; end
end
end end
</pre>
Good work.
> Output:
>
> lizzy:~% ./continuation-example.rb
> 1
> 2
> exception seen, performing error handling (elem=3)
> continuing
> 4
> 5
> exception seen, performing error handling (elem=6)
> continuing
> 7
> 8
> exception seen, performing error handling (elem=9)
> continuing
> lizzy:~%
>
> Code:
>
> #!/usr/bin/env ruby
>
> # Have you ever had the problem of wanting to report some error condition from
> # deep down within a set of nested method calls, but also wanting processing
> # to continue right after the spot where the error occurred, after the error
> # is reported and handled in the caller?
> #
> # While there are other ways to do this (e.g. using callbacks), this example
> # shows how to do this using exceptions and continuations. The trick is to
> # have the exception propagate the continuation to the appropriate caller
> # which can then handle the error (print an error message, say) and cause
> # processing to continue inside the method that saw the error by calling the
> # continuation passed up.
> #
> # This code uses a Reader class instance to process a list of elements
> # (numbers) using a screening function. The processing is facilitated using
> # the Reader.each method. Inside this method, if the element passes screening
> # it is yielded to the caller, else an exception is raised. The caller
> # processes the exception, then causes processing to continue inside the
> # Reader.each method, while passing a hint into it telling it how to continue.
> # In this particular example, the screening test consists of checking whether
> # a list element is divisible by 3, if which case it is considered "bad".
> # Also, when a maximum of 3 errors is seen (as indicated by the caller)
> # processing of list elements stops.
>
> MAX_ERRORS = 3
>
> class CCException < Exception
> attr_reader :cc, :elem
> def initialize(cc, elem)
> @cc, @elem = cc, elem
> end
> end
>
> class Reader
> def initialize(arr)
> @arr = arr
> end
> def each(check)
> cc = nil
> @arr.each do |elem|
> if check.call(elem)
> ok_to_continue = Kernel.callcc do |cc|
> raise CCException.new(cc, elem)
> end
> break unless ok_to_continue
> else
> yield elem
> end
> end
> end
> end
>
> list = [1,2,3,4,5,6,7,8,9,10,11,12]
> rdr = Reader.new(list)
> errors = 0
> # Returns whether we consider an element 'bad'
> check = proc {|elem| elem % 3 == 0}
> begin
> rdr.each(check) do |elem|
> puts elem
> end
> rescue CCException => exc
> errors += 1
> puts "exception seen, performing error handling (elem=#{exc.elem})"
> # ...error handling...
> puts "continuing"
> exc.cc.call(errors < MAX_ERRORS)
> end
>
> exit
>
> Jos Backus
E