Skip to content

Desire for a way to indicate to the type system that the return value can't be nullable if some arguments are null #1166

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Hixie opened this issue Aug 20, 2020 · 7 comments

Comments

@Hixie
Copy link

Hixie commented Aug 20, 2020

Consider this method from dart:ui:

double? lerpDouble(num? a, num? b, double t) {
  if (a == null && b == null)
    return null;
  a ??= 0.0;
  b ??= 0.0;
  return a + (b - a) * t as double;
}

When used, it is very common to know for a fact that a and b are not null, e.g. because they are literals. It is unfortunate that one nonetheless has to ceremoniously accompany these calls with a trailing !, as in lerpDouble(0.0, 100.0, t)!.

It would be nice if there was a way to say that if a and b are known to be statically non-null, then the return value can also be guaranteed to be statically non-null, and the ! can be dropped.

@lrhn lrhn transferred this issue from dart-lang/sdk Aug 20, 2020
@rrousselGit
Copy link

This looks closely related to #836

@lrhn
Copy link
Member

lrhn commented Aug 20, 2020

The general case where the nullability of the result depends on the nullability of the arguments, but where the type doesn't follow the arguments (like here where a num or int argument still becomes a double result), is very hard to capture in a purely subtype-based type system like Dart's.

You need a type system which either allows dependent types in general, or a language with method overloading, or some kind of type-metadata-system where you can abstract over the nullability, say:

double* lerpDouble<* extends ?>(num* a, num* b, double t) {
   if (a == null && b == null) return null as double*;
   ...
}

(This is still bad for typing this particular function because you'd want a non-nullable result unless both arguments are nullable, and then I think you're in full dependent type mode, and you can probably implement Turing machines in the type system.)

I'd probably just make it an error to have both a and b be null in this particular example.

@Hixie
Copy link
Author

Hixie commented Aug 20, 2020

There are lots of these examples and it is very common for a and b to both be null (in many cases it's the default situation that 99% of usage will encounter). Typically a and b are expensive to build. Here's another example:

  static FractionalOffset? lerp(FractionalOffset? a, FractionalOffset? b, double t) {
    assert(t != null);
    if (a == null && b == null)
      return null;
    if (a == null)
      return FractionalOffset(ui.lerpDouble(0.5, b!.dx, t)!, ui.lerpDouble(0.5, b.dy, t)!);
    if (b == null)
      return FractionalOffset(ui.lerpDouble(a.dx, 0.5, t)!, ui.lerpDouble(a.dy, 0.5, t)!);
    return FractionalOffset(ui.lerpDouble(a.dx, b.dx, t)!, ui.lerpDouble(a.dy, b.dy, t)!);
  }

Here's another:

  static Border? lerp(Border? a, Border? b, double t) {
    assert(t != null);
    if (a == null && b == null)
      return null;
    if (a == null)
      return b!.scale(t);
    if (b == null)
      return a.scale(1.0 - t);
    return Border(
      top: BorderSide.lerp(a.top, b.top, t),
      right: BorderSide.lerp(a.right, b.right, t),
      bottom: BorderSide.lerp(a.bottom, b.bottom, t),
      left: BorderSide.lerp(a.left, b.left, t),
    );
  }

@eernstg
Copy link
Member

eernstg commented Aug 20, 2020

We could do it with case functions, #148:

  static FractionalOffset? lerp(FractionalOffset? a, FractionalOffset? b, double t) {
    FractionalOffset case(FractionalOffset a, FractionalOffset b, double t) {
      assert(t != null);
      return FractionalOffset(ui.lerpDouble(a.dx, b.dx, t)!, ui.lerpDouble(a.dy, b.dy, t)!);
    }
    FractionalOffset case(FractionalOffset a, Null b, double t) {
      assert(t != null);
      return FractionalOffset(ui.lerpDouble(a.dx, 0.5, t)!, ui.lerpDouble(a.dy, 0.5, t)!);
    }
    FractionalOffset case(Null a, FractionalOffset b, double t) { 
      assert(t != null);
      return FractionalOffset(ui.lerpDouble(0.5, b!.dx, t)!, ui.lerpDouble(0.5, b.dy, t)!);
    }
    Null default => null;
  }

The point is that the cases are visible during static analysis at call sites, and if a case is statically guaranteed to apply then the compiler generates a direct invocation of that case, and hence the return type may be better. If the static information is insufficient to make the choice then the function as a whole is invoked, and that works like an if-chain.

@lrhn
Copy link
Member

lrhn commented Aug 24, 2020

Or we could do full overloading. It's non-trivial to figure out what that would mean in a language like Dart, but I don't think it's completely insurmountable. It's a major change to the language and the object model, though.

@Hixie
Copy link
Author

Hixie commented Aug 24, 2020

I used to be in favour of overloading, but my recent experiences with Kotlin and Swift have converted me. The developer experience is significantly better when there's no ambiguity as to what version of a method you are intending to call.

@Hixie
Copy link
Author

Hixie commented Aug 24, 2020

I think this should probably be WONTFIX. Having done more code migration it really doesn't come up that much.

@Hixie Hixie closed this as completed Aug 24, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants