Uniform Handling of Failure in Switch
inside.java
inside.java
For a few months I had the feeling that a good switch-expression requires the bottom type... It turns out that the `assert(0)` idiom, used in a lambda, is sufficient:
var u32 a = switch b do {
1 => 2,
/* ... */
else => function(): u32 => {assert(0, "crash");}()
};
The `else` case (similar to C `default`) simply crashes the program but the type system is happy because the lambda return type is compatible.In my Ruby case this is RBS (+ Steep). A typical pattern is to use `__FILE__` or `__dir__` which are references to respectively the current file path and the current file's dir path.
It's quite frequent to see the following - highly summarised - idiom:
# in foo.rb
JSON.parse(File.read(File.join(__dir__, 'some/data.json')))
Here type checking yells at you, because the signature of `__FILE__` and `__dir__` is: () -> String | nil
Which intuitively makes no sense at first! But then you realise that the signature is such because if you `eval` some Ruby code then neither can return a file path: irb> eval('__dir__')
=> nil
But after all this code is in a file! It can't return `nil`! Or, can it? irb> eval(File.read('foo.rb'))
(irb):1:in `join': no implicit conversion of nil into String (TypeError)
Ah. So maybe you know that your code never does that. So to satisfy the typing system, you do: JSON.parse(File.read(File.join(__dir__ || some_fallback, 'some/data.json')))
Where `some_fallback` may be a sane value, or another way to get to the proper path, or just `raise "this file is not meant to be eval'd"`.And well, maybe it doesn't today and in the future it will†; or maybe you have some third party tooling that ends up doing just that†.
It feels like useless boilerplate, but in a way the typing system is totally right, and just made you make your code more robust.
Back to the switch case, I had a few similar situations, and for the life of me I could not figure why it was yelling at me. "That should not happen. That cannot happen. These are the only types that can get through." Well, thinking it through it turns out again that it was totally right and I missed a very specific and legit case in another part, and handing that case (through either conversion, guard clause, or adding the case in switch) was completely warranted.
Aaaand back to TFA IIUC it seems to propose something like:
# instead of this
begin
case (parsed = parse(input))
when Integer then handle_int(parsed)
when Float then handle_float(parsed)
...
end
rescue ParseError
...
end
# or this
begin
parsed = parse(input)
rescue ParseError
...
end
case parsed
when Integer then handle_int(parsed)
when Float then handle_float(parsed)
...
end
# it suggests this, which feels very Ruby, like `def ... rescue ... end`
case (parsed = parse(input))
when Integer then handle_int(parsed)
when Float then handle_float(parsed)
rescue ParseError
...
end
# which is actually equivalent to this, and not the first example which wraps around the whole thing, including when clauses
case (
parsed = begin
parse(input)
rescue ParseError
...
end
)
when Integer then handle_int(parsed)
when Float then handle_float(parsed)
end
# and could optionally be written as this for clarity? or maybe if you write it first it applies to the case expression and if you write at the bottom it applies to the whole case block, and you can write both??
case (parsed = parse(input))
rescue ParseError
...
when Integer then handle_int(parsed)
when Float then handle_float(parsed)
rescue HandlingError
...
end
# or maybe with a new keyword if it's not clear enough, but I find that a bit ugly
case (parsed = parse(input))
when Integer then handle_int(parsed)
when Float then handle_float(parsed)
when_rescuing ParseError
rescuing ParseError
# what else???
end
Interesting!† happened.
The following example shows this nicely:
> Future<String> f = ...
> switch (f.get()) {
> case String s -> process(s);
> case throws ExecutionException(var underlying) -> throw underlying;
> case throws TimeoutException e -> cancel();
> }
Note that I don't get the hate for exceptions. Sometimes, handeling the error somewhere up the callstack is exactly the right solution. The problem was that Java did not have convenient syntax for handeling exceptions at call-site.
FYI: There was also an interesting discussion in the Java mailing lists also going into some implementation details regarding the representing bytecode:
https://mail.openjdk.org/pipermail/amber-spec-experts/2023-D...
I've actually made small streaming libraries which also support exceptions, but the problem is that it only works well if you need to support a single exception type.
As far as I'm concerned, I like checked exceptions because it improves discoverability, documentation and makes exceptional control flow more visible when reading code (including code reviews).
Next, lets add a `never` type to make this legal.
var foo = switch (bar) {
case String s -> s,
case Number n -> return,
};