From: "lloeki (Loic Nageleisen) via ruby-core" <ruby-core@...> Date: 2024-01-08T15:48:49+00:00 Subject: [ruby-core:116075] [Ruby master Feature#20160] rescue keyword for case expressions Issue #20160 has been updated by lloeki (Loic Nageleisen). > If it were consistent with case/when it would jump to the else case if it did not match. If it were consistent with begin/else it would jump to the else case if no error was raised. The idea is that: - `else` applies when no case has matched, whether they are errors or return values - `ensure` applies always > If an error is raised inside parse_int in your example, does it go through the rescue (I guess you meant handle_int, correct?) Would it be clearer that error matching and return value matching sit at the same level if written this way? ``` case (parsed = parse(input)) when Integer then handle_int(parsed) when Float then handle_float(parsed) when rescuing ParseError # ... when rescuing ArgumentError # ... else # ... fallthrough for all rescue and when cases ensure # ... called always end ``` The core idea is that both a method signature and what said method can raise in exceptional cases (including what its internal dependencies can raise yet are uncaught by said method) are part of the method contract, so handling them both at the same level with case can make sense, which in turn makes `else` and `ensure` make sense as well. ---------------------------------------- Feature #20160: rescue keyword for case expressions https://bugs.ruby-lang.org/issues/20160#change-106072 * 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-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. -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/postorius/lists/ruby-core.ml.ruby-lang.org/