Skip to content

A first-class bottom type. #262

@lrhn

Description

@lrhn

Dart 1 had a bottom type, which was at the bottom of the type hierarchy, and which was the static type type of null and throw e expressions, and little else. There was no syntax for expressing the bottom type.

When Dart 1 introduced the first-class Null type, nothing changed. Dart 2 changed the Null type to be a subtype of all non-botom types, and the static type of null and throw e to Null, rather than treat Null as simply being assignable to any type. It also avoided the bottom type from being propagated by type inference into being the type of a variable or return type of a function.

With non-nullable types, and the ability to promote a nullable variable to non-nullable, will we need first class bottom type? If so, which properties will it have?

(For now, let's ignore the name used to refer to the bottom type in code, it will likely not be bottom, but maybe something like Nothing or similar names for an empty type, other than void which is already spoken for).

Do we need bottom?

If we have bottom, then bottom is a subtype of any type, and the type bottom? is equivalent (subtype-wise) to Null.

  • Example: var x = y ?? throw SomeError();.
    If y is int?, x will have type int. What happens then if y is Null?
    In practice, the type doesn't matter. If y has type Null, then we are certain that the ?? expression will throw, so any type assigned to x will be sound wrt. the program.
    Still, we'd like to assign some type to x, and the only types that are not completely unrealted are Null and bottom. The consistent choice would be bottom, because it really is the nullable type with ? removed.

  • Example: var x = [ if (false) y ?? throw SomeError() ];
    If y is int?, x will be a List<int?>. If y has type Null?, what is the type of x. Here it does matter because the expression is the only hint for infering trhe list element type, even though it will never be executed. Is the list a List<bottom> or List<Null>.
    Again, either is possible. We can treat Null as if it's'not a nullable type, because it isn't (it's not of the form something?).

  • Example: foo<T>([List<T> bar = const []}) => ....
    Code like this example currently infer const <Null>[] as the default value because it is the only type of list assignable to List<T> for all types T. With non-nullable types, that will no longer be the case because const<Null>[] does not work for a non-nullable T like int. To solve this issue, which occurs in practice, we actually need a bottom type in the type hierarchy, and without adding a bottom, we have no common subtype of Null and int.

  • Example: foo(Object Function(Null) anyUnaryFunction) => ...
    This function currently accepts any unary function as argument. Because of covariance of parameters, any function which accepts an argument is at least as permissive than the function which only accepts Null, so it's a function subtype. Without a bottom element in the entire type hierarchy, we no longer have a top element in function type hierarchies, there is no super-type of all unary functions.
    There will definitely be cases where code would need to go back to Function as their type, losing precision over what is curently used.

So, in some cases, I'm not sure we need to have bottom, we can use Null without any problems, and treat it as any other non-nullabe type, because it will only contain members that all satisfy its interface. However, there is at least two use-cases where having a least element in the type hierarchy is useful. If we don't have a bottom type, we'll have to consider whether we can still support those use-cases.

So, yes, we probably need a bottom type.

Do we want a bottom type?

One advantage of having a bottom type is that it provides a certain symmetry to the type system.
For each class type, there is a nullable version of it, which has Null as a subtype. A bottom type would provide a correpsonding subtype for all the non-nullable types.

Another advantage is that we can now express that a function always throws. The bottom type is uninhabited, no value can soundly be assigned to bottom, so if a function's type states that it returns bottom, it certainly does not return any value. It has to throw instead (or never return).
A List<bottom> must be empty, a Future<bottom> must throw or never complete.
That's something we cannot currently express.

So, we might even have wanted a bottom type, even if we didn't need it.

How does it work?

Assume that we have a bottom type, it is the minimal type in the type hierarchy, it is a subtype of all class types, so it acts as if it implements all interfaces.
A function of type bottom Function() can be used in any place where an I Function() function is required, for any interface I.

What does that mean for member access?
The bottom type implements every interface, so any member invocation can be well-typed.
The only issue is that the return type is unknown, the only type that can satisfy all interfaces (hypthetical ones included) is bottom itself. So, we can allow any member access on a bottom-typed expression, with a result type of bottom again.
(This is very reminiscent of dynamic, which is no coincidence. Dart 1's dynamic was a type that was both top and bottom, and the ability to invoke any member could be seen as coming from the bottom part).
On the other hand, any invocation on a bottom-typed expression is guaranteed dead code. We could disallow it without preventing any useful code, and maybe catch accidental misuses. That may be the more practical approach, even if it means special casing member access on the bottom type in the specification.

If a type variable is bound to bottom, code can attempt to access members on it, but again, that code can never run because an earlier check should have disqualified any value from flowing there. This is not a problem.

If we introduce static extension methods, can a static extension method be invoked on an expression of type bottom? If we special case normal member invocations on bottom to disallow them, we should do the same for static extension methods. Otherwise all static extension methods apply since bottom is a subtype of any type being extended. If static extension methods cannot shadow existing interface methods, then no extension method applies to bottom (which implements all interfaces).

Can you write e1 is bottom? It's always false (and always type promotes a variable to bottom on the true branch).
Can you write e1 as bottom? It always throws a type error if e1 evaluates to a value, so it's not useful.
Can you have an implicit downcast to bottom (if we allow implicit downcasts at all)? bottom x = 1+ 1; should work, and always fail at run-time.

All in all, I'd recommend:

  • Having a bottom type.
  • Explicitly disallow any member access on something with static type bottom, because it's dead code, so probably a mistake.
  • Disallow, or make it a warning, any time a value with static type bottom is not ignored (like void, but without the backwards compatible exceptions), again because it's dead code.
  • Disallow as bottom, because it always throws.
  • Disallow is bottom, becasue it's always false.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureProposed language feature that solves one or more problemsnnbdNNBD related issues

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions