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