That reminds me a problem I thought I had in STYX...
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.
Makes me think of other similar cases raised by typing systems.
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:
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
I think the transformative potential for error handling in Java cannot be overstated. Effectively, this means exceptions can be handled as errors without polluting the return type with an ever-growing list of error-monads.
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:
I think it depends, but I wonder if anything can be done about the problem with checked exceptions in lambdas / for instance the streams. I think the enhanced switch with handling failure is only part of the solution, but I'm also a proponent of having only unchecked exceptions.
I think the Java team is getting there. Everything in the sytanx-space they have worked on moved towards a functional representation of data. Once you have a uniform way to describe non-uniform types (sum types, rust enums etc.), adding exception support to the JDK seems trivial.
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).
6 comments
[ 3.3 ms ] story [ 31.2 ms ] threadFor 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:
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:
Here type checking yells at you, because the signature of `__FILE__` and `__dir__` is: 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: But after all this code is in a file! It can't return `nil`! Or, can it? Ah. So maybe you know that your code never does that. So to satisfy the typing system, you do: 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:
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.