dart-lang / language

Design of the Dart language
Other
2.68k stars 205 forks source link

To-non-nullable indicator for type parameters like `T!` #4160

Open HosseinYousefi opened 3 weeks ago

HosseinYousefi commented 3 weeks ago

We can currently have a type parameter like class Foo<T extends Object> {} and specify a method to return T?:

class Foo<T extends Object> {
  T? foo() => null;
}

However we can't do this in the other way, meaning we have a T extends Object? and we want to specify a method to return a non-nullable version of T, I propose we add a T! variation so we can have:

class Foo<T extends Object?> {
  T! foo() => ...;
}

This is useful in the context of Java interop where the type parameter itself can be nullable but a single method be annotated with @NonNull.

lrhn commented 3 weeks ago

I recommend doing:

class Foo<T extends Object> {
 T foo() => ...

  // use T? everywhere being nullable is allowed.
}

Even if we had a ! operator that could be applied to type variables, it would not be able to make every type non-nullable. For example Foo<FutureOr<int?>> would have a foo() that returns NonNull(FutureOr<int?>) which is FutureOr<int?> again. That may not be a problem for the cases where it does work.

The ! would be a general operator on types. You probably can't write int?!, but only because it's redundant, so the only real use will be on type variables. (Again FutureOr<T>! will be FutureOr<T>, not FutureOr<T!>.)

HosseinYousefi commented 3 weeks ago

I recommend doing:

class Foo<T extends Object> {
 T foo() => ...

  // use T? everywhere being nullable is allowed.
}

That's not an accurate representation. Foo<String> and Foo<String?> might indeed be different types. To truly differentiate them I need to add a FooOfNullable<T extends Object> and Foo<T extends Object> extends FooOfNullable<T> and override the methods previously defined as T? to now return T. But this is ugly and for K type parameters, you'd need 2^K different classes!

lrhn commented 3 weeks ago

It's not the same, and nothing is because the functionality your asking for doesn't exist. I often find that the rewrite gives a better design, because the type T! isn't what is really needed. (If T is generic in a way that allows a user to include null in it's values, why does the API remove null. That's not being generic over the type T!)

Looking at it again, if the construct is going to work, the ! probably has to be reified. The static and runtime type systems both contain types of the shape T!, just like they contain types of the shape T?.

Normalization would convert T!! and T?! to Norm(T!), Null! to Never, and T! to Object if T is a supertype of Object and to T if T is a subtype of Object. (And T!? to Norm(T?).)

Subtyping would treat T! like the intersection type T & Object (which it is, it's the dual of T? being the union type T | Null, making a type non-nullable where ? makes it nullable).

The interface signature of T! is the interface signature of NonNull(T).

It introduces yet another special-cased type, like the union types ? and Future Or. It does fit with the ? type, it's not completely arbitrary.