Thanks to @johnniwinther for bringing this up. Future is exported by dart:core since Dart 2.1, but FutureOr cannot be used unless it is imported explicitly. That seems somewhat inconsistent, and may be annoying for developers. Given that FutureOr may be in used during inference (and may be mentioned in error messages), it seems reasonable to claim that FutureOr is present implicitly even with no imports. Is it about time to treat FutureOr like Future and make it something like this?:
@Since("2.1")
export "dart:async" show Future, Stream;
@Since("2.11")
export "dart:async" show FutureOr;
Note that
@lrhn, @natebosch, @munificent, @leafpetersen, @stereotype441, @jakemac53, WDYT?
https://github.com/dart-lang/sdk/issues/26162 looks like the original issue for Future and Stream. The reasoning for adding those was that you could _create_ them using async and async* functions, so it was inconsistent that you couldn't explicitly reference the types without importing dart:async.
Does the same apply here that you could get a FutureOr type through inference without any transitive import explicitly referencing the FutureOr type? I tried some toy examples and couldn't make it happen, but if you can then I could see that as justification for adding it - you shouldn't be able to get an instance of a type that isn't defined in at least some transitive import of your program.
Jake is correct, the reason for including Future and Stream was that the language required them in order to correctly type some programs (those using async and async*). There is no such requirement for FutureOr.
If we make var x = test ? 1 : Future.value(1); infer FutureOr<int>, then we might want to reconsider FutureOr. I'm not sure that'd be sufficient reason, after all, sub<X>(Stream<X> stream) => stream.listen(null); would need StreamSubscription to be imported in order to be correctly typed too.
DBC. I was able to interact with the FutureOr type in a limited way in the following two programs that don't import anything.
Example 1
This example is built around the dynamic check on the return value in async functions: it's expected to be a FutureOr instantiation.
foo(Future<int> Function(dynamic, bool) f) async {
print(42 + await f("foo", false));
}
bar() {
return foo((dynamic x, bool b) async {
if (b) {
return 42;
} else {
return x;
}
});
}
main() async {
await bar();
}
This programs complains with a stack trace that refers to FutureOr when run.
$ tools/sdks/dart-sdk/bin/dart /tmp/asdf.dart
Unhandled exception:
type 'String' is not a subtype of type 'FutureOr<int>'
#0 bar.<anonymous closure> (file:///tmp/asdf.dart:10:7)
#1 foo (file:///tmp/asdf.dart:2:21)
#2 bar (file:///tmp/asdf.dart:6:10)
#3 main (file:///tmp/asdf.dart:16:9)
#4 _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:301:19)
#5 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:168:12)
Example2
In this example the same technique is used to pass the FutureOr instantiation as a type argument into a generic function.
foo(Future<int> Function(bool) f) async {
await f(false);
}
bar() {
return foo((bool b) async {
if (b) {
return 42;
} else {
return baz();
}
});
}
X baz<X>() {
// FutureOr is available here as X.
print(X);
}
main() async {
await bar();
}
This program prints FutureOr<int> when run.
$ tools/sdks/dart-sdk/bin/dart /tmp/asdf2.dart
FutureOr<int>
+1 to what @stefantsov said. Here's another smaller example:
T foo<T>(T x) => x;
void bar () async {
int x = await foo("hello");
}
Note the error message, which shows that T is inferred as FutureOr<int>:
Analyzing /Users/leafp/tmp/test.dart...
error • The argument type 'String' can't be assigned to the parameter type 'FutureOr<int>'. • /Users/leafp/tmp/test.dart:3:21 • argument_type_not_assignable
We have a couple of different criteria on the table, for/against providing access to a type via the implicit import of core:
@jakemac53 mentioned the ability to create a future/stream because of async and async*, but such functions can use Object or several other types as the return type (even though the typing is loose). So the criterion is not that "any use of async or async* requires an import of 'dart:async'". Similarly, it's easy to write a program where 'main.dart' imports a library _L_ such that an expression in 'main.dart' has a type which is not denotable (it could be imported by _L_ or private), and it is even possible to use the members of an interface type. So the criterion can't be "every type that an expression can have must be denotable".
I mentioned that FutureOr may come up in error messages and it may be used during type inference. That suggests a criterion like "this type should be imported because you might hear about it." ;-) That is arguably weaker than having an instance, but the difference is not that firm in any case.
Maybe we should aim at the best possible developer experience, rather than trying to find hard criteria? Would it help a developer to be able to write FutureOr<...> in a "common" programming situation, just like it's helpful to allow the return type of an async function to be Future<...>?
To me having the type show up in error messages alone is not a sufficient justification for adding it to dart:core. You are not required to explicitly reference the type in order to fix the code. That would essentially be an argument that all types from all transitively imported libraries should be implicitly exported because all of them could appear in an error message.
While yes you could put Object as the return type for an async function that is not the most accurate type and I can't think of a valid use case for doing that as the "correct" thing.
Maybe we should aim at the best possible developer experience, rather than trying to find hard criteria? Would it help a developer to be able to write
FutureOr<...>in a "common" programming situation, just like it's helpful to allow the return type of anasyncfunction to beFuture<...>?
In general FutureOr is a much less common type than Future, or I would at least be shocked if it wasn't. From my perspective if we wanted to do this I would just say deprecate dart:async entirely and export the entire thing from dart:core. Other apis like Completer, StreamController, StreamSubscription, probably have greater usage than FutureOr.
Gathered a bit of data for (case-sensitive) string matches on the cache of pub packages I have locally (>9k packages, mostly flutter):
Future: 149128
Stream: 30520
FutureOr: 619
StreamController: 4371
StreamSubscription: 2029
Completer: 3848
StreamTransformer: 196
Timer: 5411
Strong numbers indeed!
The type FutureOr may seem to be more fundamental to Dart than it is, because it comes up whenever inference is performed with such a type as the context type for e in return e;, but that doesn't mean that FutureOr is needed in order to write that program.
This sounds like, after all, there is little motivation for exporting FutureOr to every Dart program in the universe.
SGTM.
In general, user should never need to write FutureOr. If they have a function that they pass to Future.then or a result that they await, they should use either Future or a non-Future type.
Having to explicitly pass along a FutureOr really only happens (or should only happen) in framework code which need to be general over user code. User code itself should know which of the options of the union they are dealing with.
Or, put differently, you should only use FutureOr in contravariant positions. That's the general recommendation: Be strict in what you produce, but be graceful in what you accept. That means not giving a FutureOr to others who then need to handle both cases somehow, but it's fine to accept a FutureOr where you want to allow either a Future or a non-Future value. Then it's on yourself to handle both.
That makes it very rare for end-user code to need to write FutureOr anywhere.
So the numbers match the theory, and FutureOr isn't particularly needed. StreamSubscription is likely more important (especially since we often need to declare the variable before assigning it, so we can't rely on inference).
@lrhn wrote:
In general, user should never need to write
FutureOr
It is interesting that this line of argumentation would apply to union types in general, but the conclusion in general would be more like "use union types as parameter types" than "don't use union types unless you are writing a framework".
In any case, we are converging on not exporting FutureOr from core:
@leafpetersen wrote:
SGTM
I'll close this issue after the language meeting later today unless there is input to the contrary.
Closing: We will not export FutureOr from 'dart:core'.
Most helpful comment
Gathered a bit of data for (case-sensitive) string matches on the cache of pub packages I have locally (>9k packages, mostly flutter):
Future: 149128
Stream: 30520
FutureOr: 619
StreamController: 4371
StreamSubscription: 2029
Completer: 3848
StreamTransformer: 196
Timer: 5411