dart-lang / language

Design of the Dart language
Other
2.65k stars 202 forks source link

Add "asyncOr", similar to "async", for FutureOr #2033

Open rrousselGit opened 2 years ago

rrousselGit commented 2 years ago

A common UI problem is to want to make potentially asynchronous code synchronous if possible, to avoid flickers where progress indicators show only for a frame or two.

While there are solutions to this problem, the issue is, those solutions are strictly incompatible with async/await – decreasing the code readability quite a bit.

Proposal

To solve the problem of async/await being incompatible with code-paths that are potentially synchronous, we could introduce an async variant that returns a FutureOr

Meaning we could write:

FutureOr<int> multiplyByTwo() keyword {
  FutureOr<int> futureOr;
  T value = await futureOr;
  return value * 2;
}

With this variant, await may execute synchronously (when it currently would always be async). Meaning that the previous snippet would be equivalent to doing:

FutureOr<int> multiplyByTwo() {
  FutureOr<int> futureOr;
  if (futureOr is Futureint>)
    return futureOr.then((value) => value * 2);
  return value * 2;
}
rrousselGit commented 2 years ago

Note that this doesn't have to return a FutureOr specifically.

The return type can change, as long as:

jakemac53 commented 2 years ago

Note that this doesn't have to return a FutureOr specifically.

What other return types would you have in mind?

  • this return type is backward compatible with "normal" async/await

Can you clarify what you mean by this? FutureOr is not a subtype of Future so it isn't a compatible return type with the equivalent async function.

  • the return type is different from Future<T> so that it is statically analyzable.

What makes Future<T> not statically analyzable?

I think it could be useful to allow a return type of Future<T>, for the case where you want to handle internally FutureOr in a potentially synchronous way, but still always return a Future (maybe you call some other apis that always return Futures).

rrousselGit commented 2 years ago

What other return types would you have in mind?

A custom type that would be made for the occasion, likely a subclass of Future.

So:

MaybeSynchronousFuture<T> fn() asyncOr {

}

Can you clarify what you mean by this? FutureOr is not a subtype of Future so it isn't a compatible return type with the equivalent async function.

It's about existing APIs that rely on FutureOr, such as the await keyword and other code like .then & some stream operators

What makes Future not statically analyzable?

Future<T> does not express that this future is potentially synchronous.

This prevents making lint-rules such as "consider using asyncOr instead of async because your function is using potentially synchronous code" – which I think is valuable.

jakemac53 commented 2 years ago

Imo FutureOr<T> is the idiomatic way of representing a potentially synchronous future as opposed to some subclass of future that immediately invokes the then callback if it has a synchronous value. I get that dealing with FutureOr kind of sucks, but that is exactly because it is statically expressing the fact that it could be a synchronous value (and forcing you to deal with that).

What it seems to me like you really want here is a better way of easily dealing with FutureOr<T>, making it as simple as await but allowing the program to continue synchronously if the value is a T, and where the function also can return a FutureOr<T> and won't automatically create a future out of the result if it has a synchronous value.

That could come in the form of some new type of async function, or possibly just modifications to the existing spec, something like:

Future<T> does not express that this future is potentially synchronous.

Ya this is exactly why FutureOr is better than some SynchronousFuture implementation - it statically expresses this :).

I would want to be able to get the value of potentially synchronous awaits even if my function always returns a Future still.

rrousselGit commented 2 years ago

Imo FutureOr is the idiomatic way of representing a potentially synchronous future as opposed to some subclass of future that immediately invokes the then callback if it has a synchronous value.

I wholeheartedly agree. But my understanding is that not all members of the Dart/Flutter team agree, hence why I mentioned using an alternative

lrhn commented 2 years ago

Imo FutureOr<T> is the idiomatic way of representing a potentially synchronous future as opposed to some subclass of future that immediately invokes the then callback if it has a synchronous value.

I wholeheartedly agree. But my understanding is that not all members of the Dart/Flutter team agree, hence why I mentioned using an alternative

And I am one of those who disagree. 😄

There is no idiomatic way to return a potentially synchronous/potentially asynchronous result because you should never do that. (There must also not be any future which synchronously invokes the callback in then. That's just plain breaking the contract of Future and its effect on asynchronous code is undefined-to-unpredictable).

Any code which receivers something typed as FutureOr must be able to handle a Future and is itself inherently asynchronous. That's why any platform code using FutureOr<T> takes it as an argument (allowing users of the API to pass in either a T or a Future<T>), but always returns a Future itself.

Providing a way to have the same code act as both synchronous and asynchronous is not just not a goal, it's something I'd try to actively discourage.

So, back to the original proposal: It's not impossible. It'd be like an async function, except that await is synchronous on non-futures, and if all awaits executed by an invocation ends up synchronous (including the implicit await in the return statement), the returned value is returned synchronously as a non-future.

I just don't think it's a good idea. Make two functions instead, one which allows FutureOr as arguments (anywhere contravariant, including return types of argument functions) and which is asynchronous, and one which doesn't allow futures as arguments and is synchronous. If the caller doesn't know which one to call, it's probably going to be asynchronous.

jakemac53 commented 2 years ago

Any code which receivers something typed as FutureOr must be able to handle a Future and is itself inherently asynchronous.

It is inherently potentially asynchronous which is a very different thing.

Providing a way to have the same code act as both synchronous and asynchronous is not just not a goal, it's something I'd try to actively discourage.

This just isn't always practical though. In performance critical code that has to deal with potentially async things, you really want to avoid thrashing the event loop. In fact the entire existance of FutureOr as a thing is an indication that its important to be able to handle this type of code.

A common example of this is a cached async operation. You have to deal with the fact that it might need to do some async work, but usually the value is cached. And you may read this value repeatedly and often, and want that operation to continue synchronously if the cached value is available.

As long as the API statically signals (via FutureOr) that it is sometimes synchronous, and sometimes asynchronous, I see no problem with it. It is statically enforced that people deal with it somehow.

I just don't think it's a good idea. Make two functions instead, one which allows FutureOr as arguments (anywhere contravariant, including return types of argument functions) and which is asynchronous, and one which doesn't allow futures as arguments and is synchronous. If the caller doesn't know which one to call, it's probably going to be asynchronous.

The problem is you want the function that accepts a FutureOr to also be able to return a FutureOr, and still be able to use async/await. The only way to do that really is to make async/await compatible with FutureOr in that way, or new keywords.

lrhn commented 2 years ago

In fact the entire existance of FutureOr as a thing is an indication that its important to be able to handle this type of code.

That's not why FutureOr exists. It was introduced solely to support the existing use-cases from classes like Future where then accepted a function returning a future, and as a convenience also a function returning a non-future value, so you wouldn't have to wrap that function in something that makes the value into a future. It was not intended as a way for the same function to return either a value or a future.

That's why every platform API or language feature which recognizes a FutureOr will treat it as if it had been a future.

If you create an API which is sometimes synchronous and sometimes not, it won't interact well with the platform library. You'll have to split the code paths before calling platform code, otherwise you end up with a definitely asynchronous result.

(I'll admit that I don't know what "thrashing the event loop" means. It does sound bad.)

rrousselGit commented 2 years ago

The issue with being against code that can be both synchronous and asynchronous is, there are legitimate use cases. SynchronousFuture and FutureOr exist for a reason after all.

Splitting the code into two function, one sync and one async, is not something reasonable.

In many cases this distinction is needed when some of the computation is cached. It is the same code being invoked, but we don't need to load an asset or do a network request because that was done before.

Forcing the code to be async here would cause a flicker on the UI. Which is where Flutter instead relies on SynchronousFuture

That's an important problem. We can't simply dismiss it with "you should never do that", because that's simply not true

lrhn commented 2 years ago

The existence of FutureOr is not evidence for a single function being either synchronous or asynchronous being legitimate. That was not its intent.

SynchronousFuture is evidence, but it's just fundamentally broken because it claims to be asynchronous, and then isn't.

The problem is that if you don't know whether something will provide its value synchronously or asynchronously, you still risk flickering the UI.

So, presumably you are seeding the value in a way which ensures that it's going to be synchronous when it matters. That's a perfectly reasonable design, because you know whether it's synchronous or not. It's just trying to fit this into a design which wasn't made for that.

The direct approach to the problem would be a class like:

import "package:async/async.dart";
class AsyncCache<T> {
  final Future<T> computation;
  Result? _result;
  AsyncCache(this.computation) {
    Result.capture(computation).then((result) {
      _result = result;
    });
  }
  bool get hasResult => _result != null;
  T get result {
    var result = _result;
    if (result == null) throw StateError("No value");
    ValueResult? value = result.asValue;
    if (value != null) return value.value;
    ErrorResult error = result.asError!;
    Error.throwWithStackTrace(error.error, error.stackTrace);
  }
}

That'll allow you to access a future's result synchronously (cache.result) if it's available, or access the future (cache.computation) when you prefer that.

It doesn't try to shoehorn the two approaches into the same code path (which then has to get at least twice as complex, and requires a type check - one which 95% of people will get wrong for a Future<Future<int>>, only saved by the fact that they'll never see one).

I might be more worried about the complexity we'll see people write themselves into in order to make potentially synchronous computations, than about the feature itself.

I'd be totally down for making await nonFuture complete synchronously, independently of this feature. (I'd also be totally from disallowing await on non-futures completely. It's the current middle-ground that I'm not too fond of.).

It has some consequences: We currently complete the returned future asynchronously in returns that happen during the initial synchronous part of an async function call, but complete it synchronously in later parts of the computation. We now won't be able to know that an await futureOrValue definitely introduces an asynchronous await. I think that's a minor point, though. We already have to deal with if (something) await future; return e;.

If we say that an async function with a FutureOr<T> as declared return type will synchronously return a non-future return value for a return that happens during the initial synchronous part of the function call, then ... it's probably mostly safe.

Or, we could introduce a different function body marker, like asyncOr, which allows you to use the functionality safely in function literals too, where you can't write the return type.

My main worry is then why should every async function not want to be asyncOr? And if they do, then maybe it should be the default behavior of every async function. But that will put the onus of handling the FutureOr on the caller. If you just use await, that's easy. If you need to pass the result to something expecting a Future, then you either need to wrap the result (Future.value(result), again 95% chance of making a mistake if ever encountering a Future<Future<int>>), or you have to split the code paths.

Returning a FutureOr is a cost on the receiver. It's also a potential boon for the receiver, if they want the result synchronously. That means that it can be done, judiciously, in situations where you know it's an advantage, but I'd be very worried about making it a default, or even an easily supported language feature, because I'd expect a lot of asyncOr markers added "for speed" without actually considering the receivers of the API.

If it's easy to misuse, I don't think it's a good language feature. I fear this feature, which is otherwise quite technically possible, would be easy to misuse.

jakemac53 commented 2 years ago

If we say that an async function with a FutureOr<T> as declared return type will synchronously return a non-future return value for a return that happens during the initial synchronous part of the function call, then ... it's probably mostly safe.

Ya this would be I think the best solution, combined with making await continue synchronously for synchronous values.

Or, we could introduce a different function body marker, like asyncOr, which allows you to use the functionality safely in function literals too, where you can't write the return type.

That is a good case for a special keyword, I hadn't considered that. Having both options seems a bit weird though.

Returning a FutureOr is a cost on the receiver. It's also a potential boon for the receiver, if they want the result synchronously.

It is only really a cost on the receiver because we don't have the feature being asked for here. These features are really all about making it easier to handle FutureOr easily (in the same way you handle futures), which removes most of that burden.

If you need to pass the result to something expecting a Future, then you either need to wrap the result (Future.value(result), again 95% chance of making a mistake if ever encountering a Future<Future<int>>), or you have to split the code paths.

I would hypothesize that functions which accept futures as parameters are relatively rare compared to ones that return futures. They certainly exist (FutureBuilder, etc), and may exist more than I realize in UI code, but many of the UI use cases probably should actually accept a FutureOr anyways.

If it's easy to misuse, I don't think it's a good language feature. I fear this feature, which is otherwise quite technically possible, would be easy to misuse.

I guess I am not quite understanding exactly what the concerns are? Are you concerned about the cost of the type checks, or something else?

rrousselGit commented 2 years ago

I don't quite see what can be easy to misuse here either.

I could see the problem if we were to make .then synchronous. But making await synchronous seems harmless

lrhn commented 2 years ago

The infinite loop is a valid worry. Say, you call an async method until some variable changes, and that async method becomes asyncOr and starts caching results for up to five seconds instead of repeatedly querying the backend. And it returns the cached result synchronously. Then you will busy-wait for five seconds and block all other execution, including the one which would change the variable you're waiting for.

  var oldValue = variable;
  do {
    await checkServerUp(); // Used to give other code time to run, not anymore.
  } while (oldValue == variable);

The misuse I'm worried about is people making functions be asyncOr instead of async for no good reason.

Such authors will likely think that they are helping their users, with a comment like "// asyncOr for speed!". Because, why not. It costs you nothing, other than changing the return type to FutureOr<T> instead of Future<T>, and most of the time it returns a Future anyway, and all you ever envision your users doing is awaiting the result, so it should be safe and potentially faster. What's not to like?

What's not to like is the cases where you don't await the result, but either pass it to something else instead, which expects a Future (like Future.wait or similar helpers) or you want to call then on the future yourself (or whenComplete or ignore or asStream). All those cases gets harder to handle. Not that much, I'd probably just do Future.sync(yourFunction) to wrap the future (or make an extension method to do it for me). But still, you have to do that. Or you might think that preserving synchonousness is a valid goal, and rewrite your function to have two paths, which is unnecessary complexity that you don't know if anyone actually wants.

And it possibly turns off the unawaited_futures lint, because that doesn't trigger on FutureOr.

When I look at a feature, I try to look at the incentives to use or not use that feature. Who benefits? Who does it cost? When an author is in a position to use the feature, should they do it always, never, or sometimes? If "sometimes", do they have the information needed to make that choice? If "always" or "never", why do we make it a choice at all? If they make the wrong choice, what does it cost them, or others?

Here, I fear the feature might be used too much, because the incentives are there to change async to asyncOr "for speed", and not considering the down-stream cost of having to deal with a FutureOr. The person who'd make that change doesn't have the necessary information to make that choice.

I don't expect users to make rational and well thought out choices about every little thing in their program. They'll learn some rules-of-thumb and "best practices" and apply them blindly. If the language is well structured and designed, that will usually work. The quick message for the feature is that "making the function asyncOr will potentially make some things faster". I'm not sure I can explain the down-sides of using asyncOr with that few and memorable words, so what are people going to do? They're going to make every async function asyncOr, because why not?

That's what this design encourages, and I don't think it's a good outcome.

If we think it is a good, or reasonable, outcome, and that we want to fundamentally change the Dart async/await story to always be "synchronous if possible", then that is an option. The design would be different then. It would be on by default (so changing the current async meaning). Our platform libraries would change to accommodate it, returning FutureOr in many places where it currently returns Future (subclasses implementing that by returning Future would stay valid, code using the future using await would stay valid, but code using .then would need to be migrated). We might want to add extension methods on FutureOr<T>, like FutureOr<R> then(FutureOr<R> Function(T)), to allow old-style code, even though FutureOr has no methods.

That's a valid design choice for Dart, if done properly. I think the proposed asyncOr here would push us towards that end too, but would not provide the underlying support needed to do it properly. It's half a solution, which will leave us with 1.5 valid features, and people gravitation towards the 0.5 part.

That's my worry. Feel free to disagree.

lrhn commented 2 years ago

Interestingly, I count ~100 existing functions with FutureOr return type and async body in the code I have easy access to. Some are implementing the same interface with that signature, but some are just stand-alone functions. Those make no sense.

That also suggests to me where this is coming from.

I generally say that a function in your public API should not return FutureOr, but it's fine to accept FutureOr in input, because that puts the work on yourself, not your API clients.

These interfaces are input objects to a framework. A method on such an object can be treated like a function argument, an input to the framework code, and as such, allowing FutureOr as return is fine. Framework clients can choose to implement that function either synchronously or asynchronously, either is fine. They don't get a FutureOr that they need to handle, only the framework does.

It worries me a little that being synchronous is such a big advantage that it's worth worrying about whether the code ends up being synchronous or asynchronous, that suggests that asynchrony is still too expensive. Accepting both is fine, preferring synhronous over asynchronous is at least a little problematic.

rrousselGit commented 2 years ago
  var oldValue = variable;
  do {
    await checkServerUp(); // Used to give other code time to run, not anymore.
  } while (oldValue == variable);

That's something which would be caught in development immediately, and it'd be a matter of adding an await Future.delayed.
From there on, users would likely be aware of this and avoid making the mistake next time

We could possibly have a lint for that too. That's something that the analyzer can catch

The misuse I'm worried about is people making functions be asyncOr instead of async for no good reason.

I'd say that's quite unlikely:

We could (and likely should) have a lint that says "asyncOr unused, the function can never return synchronously" such that we have:

FutureOr<T> fn() asyncOr { // warning, http.get returns a Future not FutureOr
  return http.get('...');
}

FutureOr<T> fn() asyncOr { // ok
  if (cache != null) return cache;
  return cache = await http.get('...');
}

What's not to like is the cases where you don't await the result, but either pass it to something else instead, which expects a Future (like Future.wait or similar helpers) or you want to call then on the future yourself (or whenComplete or ignore or asStream). All those cases gets harder to handle. Not that much, I'd probably just do Future.sync(yourFunction) to wrap the future (or make an extension method to do it for me). But still, you have to do that.

That sounds a bit exaggerated to me. To begin with, these are likely fairly rare. And we could solve all of those with one single extension:

extension<T> on FutureOr<T> {
  Future<T> get asFuture => this is future? this : null;
}

which allows

FutureOr<T> futureOr;
futureOr.asFuture?.ignore()
rrousselGit commented 2 years ago

@tatumizer

Just curious: do you have any practical (meaning: not artificial) example of the situation where, using the idea of a workaround illustrated by multiplyByTwo, you could demonstrate a non-trivial performance impact on UI?

Performance impact is not what we're after. The impact this feature has is on user experience and how easy it is to reach that user experience.

The problem at hand is avoiding flickers when the UI tries to render data that is readily available yet locked behind an unnecessary await.
It's a recurring issue in Flutter, which is why Flutter has this SyncronousFuture class. Navigator/Image/... are all examples of production code using fancy workarounds around Future to avoid delaying the rendering by a frame and causing a blink. A NetworkImage will be asynchronous on the first render, yet synchronous on subsequent renders.

So it's a known issue with known workarounds. The case I'm making is that the code needed to solve this problem is not pretty. That's fine when this code is internal to Fluter. But this problem is a common issue in business logic too.

rrousselGit commented 2 years ago

It's not specific to caching data. Some examples would be Localization and Router APIs. They have a few APIs that have to handle both synchronous and asynchronous cases and ask users to "return a SynchronousFuture instance when possible"

rrousselGit commented 2 years ago

I don't agree

FutureOr is not a base class that objects have to implement. Everything is a FutureOr

cedvdb commented 2 years ago

What's stopping NetworkImage from returning a dynamic instead Future with SyncchronousFuture sometimes ? The base class interfaces the method Future<T> obtainKey(ImageConfiguration configuration); but it could be dynamic or even FutureOr.

I agree it would be easier to await on the other side if it's maybe a future though wouldn't the other proposal to remove the await from async methods solve this ?

Levi-Lesches commented 2 years ago

The status of this function is the same as unawaited: it doesn't do much by itself, but acts more as an annotation, controlling other features (e.g. unawaited affects the reaction of the linter). This way, you get the behavior you want without a new keyword.

Well, at that point, it might as well just be a keyword. I don't think special-casing a feature that goes this deep into the mechanics and philosophies of asynchrony is a good idea. If it impacts a language mechanic, it should be a language feature.

It seems that the discussion has otherwise come to the consensus of modifying the behavior of async/await to handle FutureOr while still being intuitive. From @jakemac53's (https://github.com/dart-lang/language/issues/2033#issuecomment-998082739):

That could come in the form of some new type of async function, or possibly just modifications to the existing spec, something like:

  • An async function with a return type of FutureOr will not wrap non-Future return types in a Future.
  • Change await to continue synchronously if it gets a non-Future value (this would be more breaking and potentially harder, may want some different keyword).

And from @lrhn (https://github.com/dart-lang/language/issues/2033#issuecomment-998637736):

If we say that an async function with a FutureOr<T> as declared return type will synchronously return a non-future return value for a return that happens during the initial synchronous part of the function call, then ... it's probably mostly safe.

I'd be totally down for making await nonFuture complete synchronously

Seems to me like you two agree on the approach.

Levi-Lesches commented 2 years ago

Right, I agree with you. That's why I quoted -- twice -- that it would be better to change the existing mechanics than to introduce a whole new keyword that does basically the same thing.

rrousselGit commented 2 years ago

To come back to this, personally I think a real problem with this issue is, packages cannot fix it themselves. We can't make a function that does the "await" logic for users

That's in part why I wanted statement-level macros for metaprogramming, so that packages could implement their own await-like keywords if they wanted to. This way even if we had no native language support for it, we'd still be able to make a reasonable workaround

After-all there are numerous use-cases for custom await-like keywords, such as:

rrousselGit commented 2 years ago

The macro "asyncExtensions" can find all locations where awaitOr is "called" and replace awaitOr with the workaround. If the user forgets to add @asyncExtensions annotation, the compiler will raise an error (the argument type int cannot be assigned to the parameter type Never"). Will it work?

That won't work AFAIK. Unless a new proposal has been made with different rules, macros are not able to modify user-defined code. So there's no way to replace the "awaitOr" expression

rrousselGit commented 2 years ago

But that would effectively break breakpoints I believe.

My understanding is that the augment proposal relies on a new "super" feature, where generated code can call super to invoke user-defined code, such that we have:

@macro
void fn() {
  print('hello');
}

// generated augment library
void fn() {
  print('before');
  super();
  print('after');
}

We could theoretically not use super and generate:

// generated augment library
void fn() {
  print('before');
  print('hello');
  print('after');
}

but I believe that super is necessary for breakpoints to work, as otherwise, folks would have to place their breakpoints in the generated code

rrousselGit commented 2 years ago

No, you don't have to call super. We discussed that in another thread.

Do you have a link to that thread? I don't know which one you're referring to

rrousselGit commented 2 years ago

Oh, you're referring to hashCode uses-cases, I see. But forking the user's code to apply minor tweaks is a different beast. We can already do things like these with build_runner, and this does break breakpoints.

Considering the amount of pushback I got when suggesting various solutions to this issue (sourcemaps, expression macro, ..), I'm almost certain that your proposal would indeed break breakpoints and that would be considered as "expected behavior"

rrousselGit commented 2 years ago

If you set a breakpoint on "foo" in class A, it won't be hit. This doesn't mean that the breakpoints are "broken" :-)

That's a marginally different use-case. We need to differentiate generating new code from transpiling existing code

If tomorrow await was instead implemented as a macro such that we have:

@async
Future<int> fn() {
  int result = @await fetch(...);
  int result2 = @await fetch2(...);
  return result + result2;
}

// augment
Future<int> fn() {
  return Future(() {
    return fetch(...).then((result) {
      return fetch2(...).then((result) => result + result2);
    });
  });
}

then this would be a significant degradation of the user experience to expect folks to place their breakpoints in the generated code.

That's why the JS land with Babel/TS/... uses source-maps. It is a critical piece to have a good developer experience. Without that, we can't really consider the problem solved. As otherwise, users would still want this feature to be built in the language, such that we have a better user experience.

rrousselGit commented 2 years ago

We can't use the async keyword, because this would make the return type become a Future instead of FutureOr. As such, we can't use await either and have to use .then

So the generated code will be more complex because of that.

I'd expect:

@asyncOr
FutureOr<int> fn() {
  print('hello');
  int value = wait(someFutureOrExpression(...));
  int value2 = wait(another());
  return value * value2;
}

to generate something among the lines of:

FutureOr<int> fn() {
  print('hello');
  final _$tmp1 = someFutureOrExpression(...);
  FutureOr<int> _$body1(int value) {
    final _$tmp2 = another();
    FutureOr<int> _$body2(int value2) {
      return value * value2;
    }

    return _$tmp2 is Future<int> ? _$tmp2.then(_$body2) : _$body2(_$tmp2 as int); 
  }

  return _$tmp1 is Future<int> ? _$tmp1.then(_$body1) : _$body1(_$tmp1 as int); 
}
rrousselGit commented 2 years ago

Since you are generating the code by a macro, you are rewriting the whole foo, so you should be able to explicitly change the return type to FutureOr, no?

No. Macros cannot change the prototype of a function, so the return type has to stay the same.

Then maybe this is a problem? In other words, had it returned FutureOr instead of a Future, then the solution would work?

Right. But then that's a language change, at which point we might as well implement the entire feature instead of only changing one part of relying on macros for the await. Right?

rrousselGit commented 2 years ago

Using build_runner, this trick can only be applied to class methods, not static functions.

But like I mentioned before the problem is the developer experience. This trick is not something that packages can reasonably use.
It may pass for internal code. But that's not something the documentation of a package should promote as a solution to a certain problem. At least not without sourcemaps

rrousselGit commented 2 years ago

I'm not sure what your proposal solves. We can already use FutureOr for the same result, while not losing type safety

FutureOr<String> getWeatherForecast() {
   if (weatherForecast != null && getForecastAgeInSeconds() < 60) {
     return weatherForecast;
   }
   return Future(() async {
      weatherForecast = parseResponse(await http.get(... request to Weather Service));
      timestamp = DateTime.now();
      return weatherForecast;
   });
}

methodUsingWeatherForecast() async {
   var f = getWeatherForecast();
   String forecast = f is Weather ? f : await f;
   // use forecast
}