Feature #20160
Updated by lloeki (Loic Nageleisen) 11 months ago
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](https://ruby-doc.org/stdlib-2.7.1/libdoc/net/http/rdoc/Net/HTTP.html#class-Net::HTTP-label-Following+Redirection): ``` 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](https://inside.java/2023/12/15/switch-case-effect/), and thinking how it could apply to Ruby.