ruby-core@ruby-lang.org archive (unofficial mirror)
 help / color / mirror / Atom feed
* [ruby-core:116065] [Ruby master Feature#20160] rescue keyword for case expressions
@ 2024-01-08 13:58 lloeki (Loic Nageleisen) via ruby-core
  2024-01-08 14:21 ` [ruby-core:116067] " lloeki (Loic Nageleisen) via ruby-core
                   ` (7 more replies)
  0 siblings, 8 replies; 9+ messages in thread
From: lloeki (Loic Nageleisen) via ruby-core @ 2024-01-08 13:58 UTC (permalink / raw
  To: ruby-core; +Cc: lloeki (Loic Nageleisen)

Issue #20160 has been reported by lloeki (Loic Nageleisen).

----------------------------------------
Feature #20160: rescue keyword for case expressions
https://bugs.ruby-lang.org/issues/20160

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

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 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
```

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

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [ruby-core:116067] [Ruby master Feature#20160] rescue keyword for case expressions
  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 ` lloeki (Loic Nageleisen) via ruby-core
  2024-01-08 15:12 ` [ruby-core:116071] " austin (Austin Ziegler) via ruby-core
                   ` (6 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: lloeki (Loic Nageleisen) via ruby-core @ 2024-01-08 14:21 UTC (permalink / raw
  To: ruby-core; +Cc: lloeki (Loic Nageleisen)

Issue #20160 has been updated by lloeki (Loic Nageleisen).

Description updated

Some clarifications.

----------------------------------------
Feature #20160: rescue keyword for case expressions
https://bugs.ruby-lang.org/issues/20160#change-106064

* 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
```

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

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [ruby-core:116071] [Ruby master Feature#20160] rescue keyword for case expressions
  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 ` austin (Austin Ziegler) via ruby-core
  2024-01-08 15:18 ` [ruby-core:116072] " rubyFeedback (robert heiler) via ruby-core
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: austin (Austin Ziegler) via ruby-core @ 2024-01-08 15:12 UTC (permalink / raw
  To: ruby-core; +Cc: austin (Austin Ziegler)

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-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/

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [ruby-core:116072] [Ruby master Feature#20160] rescue keyword for case expressions
  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 ` rubyFeedback (robert heiler) via ruby-core
  2024-01-08 15:21 ` [ruby-core:116074] " kddnewton (Kevin Newton) via ruby-core
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: rubyFeedback (robert heiler) via ruby-core @ 2024-01-08 15:18 UTC (permalink / raw
  To: ruby-core; +Cc: rubyFeedback (robert heiler)

Issue #20160 has been updated by rubyFeedback (robert heiler).


Note that I find this example:

    when Float then handle_float(parsed)
    rescue ParseError

Easier to read than:

    case (parsed = parse(input))
    rescue ParseError
    when Integer then handle_int(parsed)

I am also not certain how common it is to assign "in-line", that is the
variable "parsed = ".

In my own code I very rarely do such assignment styles, although I do 
sometimes use it for individual when-lines, when I need the assigned
value only within that when-clause itself; I usually may handle longer
case/when structures via a dedicated method, where I ensure that everything
is as I need it to be, before letting case handle things. I do not really
have a dedicated pro or con opinion on the suggested functionality in
and by itself, though, so my comment should be more regarded as a
peripheral comment than one about the proposed functionality as such
in any way really.

----------------------------------------
Feature #20160: rescue keyword for case expressions
https://bugs.ruby-lang.org/issues/20160#change-106070

* 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/

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [ruby-core:116074] [Ruby master Feature#20160] rescue keyword for case expressions
  2024-01-08 13:58 [ruby-core:116065] [Ruby master Feature#20160] rescue keyword for case expressions lloeki (Loic Nageleisen) via ruby-core
                   ` (2 preceding siblings ...)
  2024-01-08 15:18 ` [ruby-core:116072] " rubyFeedback (robert heiler) via ruby-core
@ 2024-01-08 15:21 ` kddnewton (Kevin Newton) via ruby-core
  2024-01-08 15:48 ` [ruby-core:116075] " lloeki (Loic Nageleisen) via ruby-core
                   ` (3 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: kddnewton (Kevin Newton) via ruby-core @ 2024-01-08 15:21 UTC (permalink / raw
  To: ruby-core; +Cc: kddnewton (Kevin Newton)

Issue #20160 has been updated by kddnewton (Kevin Newton).


```ruby
   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
```

This would make me very uncomfortable. `else` for `case`/`when` is used as a default case. `else` for `begin` is used when error are not raised. These are fundamentally different concerns and concepts, and this would be overloading them.

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. This would be very confusing and difficult to educate people about.

It's also not clear to me if a `rescue` clause is attached to a `case` statement if the rescue applies to just the value of the `case` or if it applies to the entire statement. If an error is raised inside `parse_int` in your example, does it go through the `rescue`? What if the `rescue` is added after the `in`/`when` clause?

I think this loses quite a bit more clarity than it gains.

----------------------------------------
Feature #20160: rescue keyword for case expressions
https://bugs.ruby-lang.org/issues/20160#change-106071

* 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/

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [ruby-core:116075] [Ruby master Feature#20160] rescue keyword for case expressions
  2024-01-08 13:58 [ruby-core:116065] [Ruby master Feature#20160] rescue keyword for case expressions lloeki (Loic Nageleisen) via ruby-core
                   ` (3 preceding siblings ...)
  2024-01-08 15:21 ` [ruby-core:116074] " kddnewton (Kevin Newton) via ruby-core
@ 2024-01-08 15:48 ` lloeki (Loic Nageleisen) via ruby-core
  2024-01-08 16:04 ` [ruby-core:116076] " lloeki (Loic Nageleisen) via ruby-core
                   ` (2 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: lloeki (Loic Nageleisen) via ruby-core @ 2024-01-08 15:48 UTC (permalink / raw
  To: ruby-core; +Cc: lloeki (Loic Nageleisen)

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/

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [ruby-core:116076] [Ruby master Feature#20160] rescue keyword for case expressions
  2024-01-08 13:58 [ruby-core:116065] [Ruby master Feature#20160] rescue keyword for case expressions lloeki (Loic Nageleisen) via ruby-core
                   ` (4 preceding siblings ...)
  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
  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
  7 siblings, 0 replies; 9+ messages in thread
From: lloeki (Loic Nageleisen) via ruby-core @ 2024-01-08 16:04 UTC (permalink / raw
  To: ruby-core; +Cc: lloeki (Loic Nageleisen)

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/

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [ruby-core:116077] [Ruby master Feature#20160] rescue keyword for case expressions
  2024-01-08 13:58 [ruby-core:116065] [Ruby master Feature#20160] rescue keyword for case expressions lloeki (Loic Nageleisen) via ruby-core
                   ` (5 preceding siblings ...)
  2024-01-08 16:04 ` [ruby-core:116076] " lloeki (Loic Nageleisen) via ruby-core
@ 2024-01-08 16:18 ` austin (Austin Ziegler) via ruby-core
  2024-02-14  5:22 ` [ruby-core:116731] " matz (Yukihiro Matsumoto) via ruby-core
  7 siblings, 0 replies; 9+ messages in thread
From: austin (Austin Ziegler) via ruby-core @ 2024-01-08 16:18 UTC (permalink / raw
  To: ruby-core; +Cc: austin (Austin Ziegler)

Issue #20160 has been updated by austin (Austin Ziegler).


lloeki (Loic Nageleisen) wrote in #note-7:
> > If #parse is defined as:
> 
> This requires:
> 
> a) parse to be in your control

It does not. A parse wrapper *could* be written, just as I did with `get` in the URI example.

> b) parse to handle every possible exception (including whatever it calls) for which one would want a rescuing clause to control flow.

Not at all. `rescue => error` and returning `[:error, error]` is sufficient to handle anything that `parse` *does not* already handle.

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

Exceptions are part of Ruby, but exception *handlers* are not cheap.

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

`else` still does not make sense as Kevin Newton said in [#5](https://bugs.ruby-lang.org/issues/20160#note-5), as the `begin/else/end` is a wholly different context than `case/when/else/end` or `case/in/else/end`. The former is for when no exception is thrown; the latter two are when there is no other match made. I don't see a way to reconcile that particular conceptual roadblock *aside* from your parser function returning something more meaningful.

Exceptions aren't really supposed to be used for flow control, which is sort of what you're doing here. `throw/catch` is more related to flow control than exceptions, IMO.

I think that what you've presented here is *interesting*, but I could not see using it.

----------------------------------------
Feature #20160: rescue keyword for case expressions
https://bugs.ruby-lang.org/issues/20160#change-106074

* 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/

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [ruby-core:116731] [Ruby master Feature#20160] rescue keyword for case expressions
  2024-01-08 13:58 [ruby-core:116065] [Ruby master Feature#20160] rescue keyword for case expressions lloeki (Loic Nageleisen) via ruby-core
                   ` (6 preceding siblings ...)
  2024-01-08 16:18 ` [ruby-core:116077] " austin (Austin Ziegler) via ruby-core
@ 2024-02-14  5:22 ` matz (Yukihiro Matsumoto) via ruby-core
  7 siblings, 0 replies; 9+ messages in thread
From: matz (Yukihiro Matsumoto) via ruby-core @ 2024-02-14  5:22 UTC (permalink / raw
  To: ruby-core; +Cc: matz (Yukihiro Matsumoto)

Issue #20160 has been updated by matz (Yukihiro Matsumoto).

Status changed from Open to Rejected

According to the original intention, `rescue` clauses in `case` should only handle exceptions from the target expression, not those from the `case` bodies. But the clause position could confuse readers of the source of exceptions to handle. So I'd rather use the option B in the OP. It's quite straight forward and intuitive (although not being fancy, nor too concise).

Matz.


----------------------------------------
Feature #20160: rescue keyword for case expressions
https://bugs.ruby-lang.org/issues/20160#change-106750

* Author: lloeki (Loic Nageleisen)
* Status: Rejected
* 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/

^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2024-02-14  5:22 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [ruby-core:116076] " lloeki (Loic Nageleisen) via ruby-core
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

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