ruby-core@ruby-lang.org archive (unofficial mirror)
 help / color / mirror / Atom feed
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/

  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).