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
rescue
is not localised to parsing and also covers code followingwhen
(including calling===
),then
, andelse
, 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 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.