dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.04k stars 1.55k forks source link

Conditionals and switch expressions never unify to FutureOr even with explicit result type #55337

Open dgreensp opened 5 months ago

dgreensp commented 5 months ago

This is becoming a pain point in code I am writing. Would it be possible to make FutureOr something that can be found as a LUB?

import "dart:async" show FutureOr;

final a = "str";
final b = Future(() => "str");
final c = true;
final FutureOr<String> d = c ? a : b; // error, Object is not FutureOr
final FutureOr<String> e = switch (c) { // same error
  true => a,
  false => b
};

Is there any way to annotate or hint this code further without changing the conditional expression or switch expression into statements? It seems like it is maximally annotated; the type checker would need to do the rest.

lrhn commented 5 months ago

It's correct that the UP algorithm, which is what unifies two types, never give a FutureOr type as result unless one or both of the original types is a FutureOr type, and it doesn't (yet) depend on the context type.

@stereotype441 is currently working on making it take the context type into account, which will likely help here. (At that point, it won't introduce a FutureOr type unless at least one of the types, or the context type, is a FutureOr type.)

Until then, cast one of the two branches to the type you want.

dgreensp commented 5 months ago

That sounds great!

On Fri, Mar 29, 2024 at 11:10 AM Lasse R.H. Nielsen < @.***> wrote:

Correct. The UP algorithm, which is what unifies two types, never give a FutureOr type as result unless one or both of the original types is a FutureOr type, and it doesn't (yet) depend on the context type.

@stereotype441 https://github.com/stereotype441 is currently working on making it take the context type into account, which will likely help here. (At that point, it won't introduce a FutureOr type unless at least one of the types, or the context type, is a FutureOr type.)

— Reply to this email directly, view it on GitHub https://github.com/dart-lang/sdk/issues/55337#issuecomment-2027563526, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABGBJVGJDRLVRQSIWHJ3G3Y2WOBDAVCNFSM6AAAAABFOT6N6WVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDAMRXGU3DGNJSGY . You are receiving this because you authored the thread.Message ID: @.***>

dgreensp commented 5 months ago

There's also this case, probably similar, in which the bound of a type parameter comes into play:

import "dart:async";

// intent is for argument to be a map of Strings to a mixture of
// String, Future<String>, and (statically) FutureOr<String>.
// It would be nice if the implementation can check if T == String
// as a quick way to know if all the keys are statically known to be
// Strings.
void foo<T extends FutureOr<String>>(Map<String, T> map) {
  print(T);
}

void main() {
  // all these work fine
  foo({"hello": "world"});
  foo({"hi": Future(() => "there")});
  final FutureOr<String> hey = "hey";
  foo({"also": hey});
  foo({"hello": "world", "also": hey});
  foo({"hi": Future(() => "there"), "also": hey});

  // mixing String and Future<String> doesn't work, whether or not there
  // is a FutureOr<String> involved.
  // error: Couldn't infer type parameter 'T'
  foo({"hello": "world", "hi": Future(() => "there")});
  // error: Couldn't infer type parameter 'T'
  foo({"hello": "world", "hi": Future(() => "there"), "also": hey});
}
dgreensp commented 5 months ago

Ah, and in particular, because of the way Dart tracks collection type parameters at runtime, there is another advantage to the argument type of a call like foo({"asdf": "asdf"}) being Map<String, T> where T extends FututureOr<String>, because it actually allows the type of the map to be Map<String, String>. Whereas if foo simply takes a Map<String, FutureOr<String>>, so that becomes the inferred type of {"asdf": "asdf"}, now you have a Map whose values are all strings, but that's not reflected in the (runtime) type of the Map, so if you want to convert it into a Map<String, String>, you actually have to copy it or use .cast() which allocates a wrapper object.

dgreensp commented 5 months ago

I'm not sure if this should be a separate issue, but here's another FutureOr quirk: it prevents inference of function arguments during analysis:

import "dart:async";

FutureOr<void Function(int)> foo() {
  return (n) => n + 1; // n is dynamic, not int
}
dgreensp commented 4 months ago

@stereotype441 Here is a case where unification doesn't work as expected, simply by replacing Future with a subclass of Future:

import "dart:async";
import "package:async/async.dart";

FutureOr<T> func<T>(Future<T>? a, FutureOr<T> b) {
  return a ?? b; // works
}

FutureOr<T> func2<T>(ResultFuture<T>? a, FutureOr<T> b) {
  return a ?? b; // error: a value of type FutureOr<Object?> can't be returned
}