Feedback on nulls in switch
Stephen Colebourne
scolebourne at joda.org
Wed Aug 12 09:48:01 UTC 2020
On Tue, 11 Aug 2020 at 15:32, Brian Goetz <brian.goetz at oracle.com> wrote:
> >> I think we all are going to have to learn to read the bottom of a switch as a probable catch-all.
> >> Maybe we want to add a modifier to enforce the assumption, and exclude the implicit default; I’ve suggested ‘final case’ as a cite bikeshed color.
> > :-) I woke up this morning and had almost exactly this thought.
>
> Now you're on a better track. The problem, if there is one, is the
> schizoid treatment of totality in switch, which (I'm sure you remember
> well) was a deliberate compromise we made to avoid having a
> proliferation of switch-like constructs. Having a way to add back
> totality explicitly may be a path to helping people see the totality
> more clearly.
Developers are already familiar to some degree with the concept of
totality as static analysis tools and code reviews often push the need
to always have a `default` label in switches. Teaching default a new
trick (pattern match binding) feels like a very understandable
extension to the language wrt totality. But I don't think developers
want to think too much about totality - it should just be natural.
Since some switches will be total without a default label, I think I'd
like to propose something subtly different to the question of whether
`default` is required to make it total.
Switches over sealed types are total using just case labels because
they contain every possible type. Thus it makes sense that switches
over non-sealed types that contain `case Object o` are also total (the
type hierarchy can be seen as a different kind of sealing). But with
switch's inherent null-hostility and what I've already argued, this
would be without `case Object o` accepting nulls:
String s = switch (randomObject) {
case BigDecimal b -> b.toPlainString();
case Object o -> o.toString(); // o is never null
// compiler effectively adds `case null` throwing NPE making it total
}
String s = switch (randomObject) {
case BigDecimal b -> b.toPlainString();
default Object o -> Objects.toString(o); // o can be null
}
And this would also apply to nested patterns:
String s = switch (box) {
case Box(Chocolate c) ...
case Box(Object o) ... // o is never null
// compiler effectively adds `case null` and `case Box(null)`
throwing NPE making it total
}
String s = switch (box) {
case Box(Chocolate c) ...
default Box(Object o) ... // o can be null
// compiler effectively adds `case null` throwing NPE making it total
// no need for `case Box(null)` to be added
}
Developers can manually write `case null` or `case Box(null)`, and
maybe `default null` or `default Box(null)`, if they want to take
control, but these are automatically added by the compiler if they
don't. This smoothes over the sharp edge with enums and sealed types -
switch is inherently null hostile, including nested patterns, unless
enabled using `default <pattern>` or manually chosen using the
`Box(null)` or similar. The totality falls out naturally without
needing developers to think too much about the interaction with nulls.
As I argued before, I think most developers writing logic don't want
the null, even in nested patterns. Iif they are null-hostile
developers, they certainly don't, but if they are null-loving there is
also a good chance that they don't want them when extracting data in
switch, just like an Optional with a value is more useful for logic
processing than an empty one. Given this, I suspect over 80% of
switches would not need to use `default <pattern>` at all - the
totality would be obtained just using `case <pattern>` and accepting
NPEs.
The more complex example is:
switch (container) {
case Box(Frog f): ...
case Box(Chocolate c): ...
case Box(var x): ....
case Bag(Frog f): ...
case Bag(Chocolate c): ...
case Bag(var x): ....
}
With my semantics, Bag(null) and Box(null) throw NPE. Or the developer
can use `deafult` multiple times to accept nulls:
switch (container) {
case Box(Frog f): ...
case Box(Chocolate c): ...
default Box(var x): ....
case Bag(Frog f): ...
case Bag(Chocolate c): ...
default Bag(var x): ....
}
What would be a useful complement to the above is a pattern that
matches anything including null but without binding, effectively an
"ignore this", allowing more control in complex cases, but this is
probably on your radar anyway:
case Triple(String a, String b, __IgnorePattern__) ...
Stephen
More information about the amber-dev
mailing list