Feature #20160
closedrescue keyword for case expressions
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
rescueis not localised to parsing and also covers code followingwhen(including calling===),then, andelse, which may or may not be what one wants. - option B
rescueis localised to parsing but moves the definition of the variable (parsed) and the call to what is actually done (parse(input)) far away fromcase.
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.