
Issue #20160 has been updated by austin (Austin Ziegler). lloeki (Loic Nageleisen) wrote:
``` 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 ```
I don't think that this would be necessarily more readable than a standard case or the new pattern matching case. If `#parse` is defined as: ```ruby def parse(input) # parsing logic rescue StandardError => e [:error, e] end ``` You could write the `case/in` patterns like this (I think; I have not yet used pattern matching because the libraries I support are not yet 3.x only): ```ruby case parse(input) in Integer => parsed_int handle_int(parsed_int) in Float => parsed_float handle_float(parsed_float) in :error, ParseError => error # handle ParseError in :error, ArgumentError => error # handle ArgumentError else # all other cases — note that there is no assignment here # but most parsing should probably be exhaustive end The `ensure` case should be executed outside of the `case`. Yes, it means restructuring the parser a little bit, but I think better than mixing `rescue` into `case`. The URI case requires a bit more work (extracting the get to a separate method): ```ruby def fetch(uri, limit = 10) raise ArgumentError, 'too many redirects' if limit == 0 case response = get_response(uri_str) in URI::InvalidURIError => error # handle URI errors in SocketError => error # handle socket errors in ArgumentError => error # assume that ArgumentError is 'too many redirects'? in StandardError => error # handle other more general errors in Net::HTTPSuccess response in Net:NTTPRedirection location = response['location'] warn "redirected to #{location}" fetch(location, limit - 1) else response.value end @counter += 1 end def get(uri_str) Net::Net::HTTP.get_response(URI(uri_str)) rescue => error error end ``` ---------------------------------------- Feature #20160: rescue keyword for case expressions https://bugs.ruby-lang.org/issues/20160#change-106069 * Author: lloeki (Loic Nageleisen) * Status: Open * Priority: Normal ---------------------------------------- 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-N...): ``` 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. -- https://bugs.ruby-lang.org/