Project

General

Profile

Actions

Feature #20160

closed

rescue keyword for case expressions

Added by lloeki (Loic Nageleisen) 4 months ago. Updated 3 months ago.

Status:
Rejected
Assignee:
-
Target version:
-
[ruby-core:116065]

Description

It is frequent to find this piece of hypothetical Ruby code:

     case (parsed = parse(input))
     when Integer then handle_int(parsed)
     when Float then handle_float(parsed)
     end

What if we need to handle parse raising a hypothetical ParseError? Currently this can be done in two ways.

Either option A, wrapping case .. end:

   begin
     case (parsed = parse(input))
     when Integer then handle_int(parsed)
     when Float then handle_float(parsed)
       # ...
     end
   rescue ParseError
     # ...
   end

Or option B, guarding before case:

   begin
     parsed = parse(input)
   rescue ParseError
     # ...
   end

   case parsed
   when Integer then handle_int(parsed)
   when Float then handle_float(parsed)
     # ...
   end

The difference between option A and option B is that:

  • option A rescue is not localised to parsing and also covers code following when (including calling ===), then, and else, which may or may not be what one wants.
  • option B rescue is localised to parsing but moves the definition of the variable (parsed) and the call to what is actually done (parse(input)) far away from case.

With option B in some cases the variable needs to be introduced even though it might not be needed in then parts (e.g if the call in case is side-effectful or its value simply leading to branching decision logic).

The difference becomes important when rescued exceptions are more general (e.g Errno stuff, ArgumentError, etc..), as well as when we consider ensure and else. I feel like option B is the most sensible one in general, but it adds a lot of noise and splits the logic in two parts.

I would like to suggest a new syntax:

   case (parsed = parse(input))
   when Integer then handle_int(parsed)
   when Float then handle_float(parsed)
   rescue ParseError
     # ...
   rescue ArgumentError
     # ...
   else
     # ... fallthrough for all rescue and when cases
   ensure
     # ... called always
   end

If more readability is needed as to what these rescue are aimed to handle - being more explicit that this is option B - one could optionally write like this:

   case (parsed = parse(input))
   rescue ParseError
     # ...
   rescue ArgumentError
     # ...
   when Integer then handle_int(parsed)
   when Float then handle_float(parsed)
     ...
   else
     # ...
   ensure
     # ...
   end

Keyword ensure could also be used without rescue in assignment contexts:

foo = case bar.perform
      when A then 1
      when B then 2
      ensure bar.done!
      end

Examples:

  • A made-up pubsub streaming parser with internal state, abstracting away reading from source:
parser = Parser.new(io)

loop do
  case parser.parse # blocks for reading io in chunks
  rescue StandardError => e
    if parser.can_recover?(e)
      # tolerate failure, ignore
      next
    else
      emit_fail(e)
      break
    end
  when :integer
    emit_integer(parser.last)
  when :float
     emit_float(parser.last)
  when :done
     # e.g EOF reached, IO closed, YAML --- end of doc, XML top-level closed, whatever makes sense
     emit_done
     break
  else
    parser.rollback # e.g rewinds io, we may not have enough data
  ensure
    parser.checkpoint # e.g saves io position for rollback
  end
end
  • Network handling, extrapolated from ruby docs:
case (response = Net::HTTP.get_response(URI(uri_str))
rescue URI::InvalidURIError
  # handle URI errors
rescue SocketError
  # handle socket errors
rescue
  # other general errors
when Net::HTTPSuccess
  response
when Net::HTTPRedirection then
  location = response['location']
  warn "redirected to #{location}"
  fetch(location, limit - 1)
else
  response.value
ensure
  @counter += 1
end

Credit: the idea initially came to me from this article, and thinking how it could apply to Ruby.

Actions

Also available in: Atom PDF

Like1
Like0Like0Like0Like0Like1Like0Like0Like0Like0