Project

General

Profile

Feature #20160

Updated by lloeki (Loic Nageleisen) 4 months ago

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

Back