From: "lloeki (Loic Nageleisen) via ruby-core" <ruby-core@ml.ruby-lang.org>
To: ruby-core@ml.ruby-lang.org
Cc: "lloeki (Loic Nageleisen)" <noreply@ruby-lang.org>
Subject: [ruby-core:116076] [Ruby master Feature#20160] rescue keyword for case expressions
Date: Mon, 08 Jan 2024 16:04:33 +0000 (UTC) [thread overview]
Message-ID: <redmine.journal-106073.20240108160433.13153@ruby-lang.org> (raw)
In-Reply-To: redmine.issue-20160.20240108135854.13153@ruby-lang.org
Issue #20160 has been updated by lloeki (Loic Nageleisen).
> If #parse is defined as:
This requires:
a) parse to be in your control
b) parse to handle every possible exception (including whatever it calls) for which one would want a rescuing clause to control flow.
> extracting the get to a separate method
A somewhat generic alternative would be:
```
def wrap_error
yield
rescue StandardError => e
[:error, e]
end
case wrap_error { parse(input) }
when ...
```
That is the point: exceptions are a first class Ruby concept. To me it feels off to create wrappers (e.g with this new get method or the wrapper above), munge return values with in band metadata (this [:error, exception] return value), or split control flow in two parts (begin rescue followed by case when) when logically it is one control flow, this is why I felt there may be interest in such a proposal.
I do agree that pattern matching feels as much of a good potential candidate as any for such an exception rescuing feature. A specific pattern syntax could be used to match a given exception, and `else` would make sense.
----------------------------------------
Feature #20160: rescue keyword for case expressions
https://bugs.ruby-lang.org/issues/20160#change-106073
* 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/
next prev parent reply other threads:[~2024-01-08 16:04 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2024-01-08 13:58 [ruby-core:116065] [Ruby master Feature#20160] rescue keyword for case expressions lloeki (Loic Nageleisen) via ruby-core
2024-01-08 14:21 ` [ruby-core:116067] " lloeki (Loic Nageleisen) via ruby-core
2024-01-08 15:12 ` [ruby-core:116071] " austin (Austin Ziegler) via ruby-core
2024-01-08 15:18 ` [ruby-core:116072] " rubyFeedback (robert heiler) via ruby-core
2024-01-08 15:21 ` [ruby-core:116074] " kddnewton (Kevin Newton) via ruby-core
2024-01-08 15:48 ` [ruby-core:116075] " lloeki (Loic Nageleisen) via ruby-core
2024-01-08 16:04 ` lloeki (Loic Nageleisen) via ruby-core [this message]
2024-01-08 16:18 ` [ruby-core:116077] " austin (Austin Ziegler) via ruby-core
2024-02-14 5:22 ` [ruby-core:116731] " matz (Yukihiro Matsumoto) via ruby-core
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-list from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
List information: https://www.ruby-lang.org/en/community/mailing-lists/
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=redmine.journal-106073.20240108160433.13153@ruby-lang.org \
--to=ruby-core@ruby-lang.org \
--cc=noreply@ruby-lang.org \
--cc=ruby-core@ml.ruby-lang.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).