Open elkSal opened 6 months ago
There's no way you can make a function run synchronously if it has a body which is marked async
. When the body is async
the function will always return a future, and there's no way to obtain the object that was used to complete that future other than awaiting it (which can be done using an actual await
expression, or via Future
members like then
).
So you need to write code that allows for a purely synchronous execution, and you may use the type FutureOr
to indicate that it might also initiate an asynchronous computation.
import 'dart:async';
FutureOr<int> get valueA => 45;
FutureOr<int> get valueB => 5;
FutureOr<int> get differenceAB {
var a = valueA, b = valueB;
switch ((a, b)) {
case (int aInt, int bInt): return aInt - bInt;
}
return (() async => await valueA - await valueB)();
}
void main() {
print("Start");
var difference = differenceAB;
if (difference is int) {
print("The difference is $difference");
} else {
print("Oh, we'll have to wait for that!");
difference.then((difference) => print("Here it comes: $difference"));
}
print("Done");
}
You can add async
here and there, and see that you will then have to wait for the result. So the point is that you need to keep the computation completely free from async
function bodies in order to preserve the possibility that the computation proceeds synchronously throughout.
FutureOr is just a special union type, there is no actual class associated with it, it has no instance methods other than what are on Object.
However, you can make a then
extension on it, https://dartpad.dev/?id=de531414b6bac53863806691ae0918b6 (in reality you would want to do something a bit more complicated than this to support the other typical then
arguments).
The main issue is this won't work with async/await.
There's no way you can make a function run synchronously if it has a body which is marked async. When the body is async the function will always return a future, and there's no way to obtain the object that was used to complete that future other than awaiting it (which can be done using an actual await expression, or via Future members like then).
One thing that was requested in a similar issue is to have either:
async
return a FutureOr if the returnType of the function is explicitly typed FutureOr.asyncOr
, with a similar behaviorUltimately the problem with FutureOr is that we lose the ability to use await
unless we treat the FutureOr as a Future, and ignore the "value synchronously available" bit.
Fwiw, I like the idea of asyncOr
, which returns a FutureOr
by default, doesn't implicitly convert return values to futures, and where await
continues synchronously when awaiting non-Futures. I am not sure about the feasibility/complexity of implementation though.
The asyncOr
idea is https://github.com/dart-lang/language/issues/2033
The arguments made there apply here to.
I can't see how this request differs from that, other than possibly syntax.
It is "sync evaluation if possible, including await
" + "return future only if necessary from async
".
If we only make await
pass values through synchronously, which is an option, then it would still get wrapped in a future at the end of the async function.
If we make an async function able to return a FutureOr
, returning a synchronous result as a non future, then it's #2033.
And not an option I'm particularly keen on. It would mean doing an extra type test at everything at every await
of such a FutureOr
, but on the other hand it feels like it should make some code possibly faster, so I'll expect authors to make every async function return FutureOr
, because they can.
(Because with this, even if the function always does an await
before return
, that might now be a synchronous await, if the function it's awaiting has begun returning FutureOr
.)
So everybody uses the feature everywhere, but the hoped-for performance improvement gets lost to the extra type checks, and it also increases code size.
So everybody uses the feature everywhere, but the hoped-for performance improvement gets lost to the extra type checks, and it also increases code size.
"Just" add then
to Object and rely on dynamic dispatch? 🤣
I don't think this is an issue of performance. To me this is an issue of timing.
I don't care about how hard it is on a CPU. It's about making the UI immediately show a value when it is available, instead of delaying its rendering by a few frames. This avoids flickers
Fwiw, the utility I have written in the past to handle this does incur a type check but still showed some appreciable speedup. I think in the case where you have a FutureOr
already, await
is probably already doing a similar check?
I'd consider making my own future wrapper, like:
extension type EagerFuture<T>._(FutureOr<T> _) {
static final _cachedResults = Expando<_FutureResult>();
EagerFuture(FutureOr<T> futureOr) : _ = futureOr {
if (futureOr is Future<T>) {
_cachedResults[futureOr] ??= _FutureResult<T>.future(futureOr);
}
}
Future<T>? asFuture() => _ is Future<T> ? _ : null;
T? asValue() => _ is Future<T> ? null : _;
bool get hasResult =>
_ is Future<T> ? _cachedResults[_]?.result != null : true;
T get result => _ is Future<T>
? (_cachedResults[_]?.result?.value ?? (throw StateError("No result")))
: _;
T? get tryResult => _ is Future<T> ? (_cachedResults[_]?.result?.value) : _;
}
class _FutureResult<T> {
_FutureResult.future(Future<T> f) {
f.then((v) {
result = Result.value(v);
}, onError: (Object e, StackTrace s) {
result = Result.error(e, s);
});
}
Result<T>? result;
}
extension <T> on Result<T> {
T get value {
if (this case ValueResult(:var value)) return value;
var error = this.asError!;
Error.throwWithStackTrace(error.error, error.stackTrace);
}
}
void main() async {
var f = EagerFuture(Future.delayed(Duration(seconds: 1), () => 42));
while (!f.hasResult) {
print("...tick");
await Future.delayed(Duration(milliseconds: 250));
}
print("Result: ${f.result}");
}
(Not tested much.)
I am worried that you're talking frames here. Awaiting a FutureOr
which is a value should never introduce more than a microtask delay. It should complete within the same top-level event.
The microtask model was designed in the browser so that UI updates are top-level events, so waiting for a microtask should not cause any frames to be drawn.
We've already talked about this before.
Your wrapper isn't representative of how folks want to use FutureOrs.
We're looking for something comparable to await future
in terms of usability, but where await 42
does not introduce any delay yet await Future.value(42)
does.
And there's the issue of chaining too. To use await
, you had to use async
. And async
returns a Future, not a FutureOr.
Meaning we cannot compose FutureOr functions easily using await
The only real solutions out there are:
.then
method on FutureOrs. But the syntax sucks compared to await
await SynchronousFuture(42)
is synchronous.
But that doesn't work with many SDK APIs (like how Future.wait([SynchronousFuture()])
will fail)
And this does not solve the composition issue, due to async
always returning Future, never a SynchronousFuture.I am worried that you're talking frames here
Microtasks are way too late already. If scheduled at the start of a frame, they'll trigger at the end of it. Yet we want those values immediately instead of at the end of the frame.
The issue can be represented using the following Flutter app:
import 'package:flutter/material.dart';
void main() {
runApp(MyWidget());
}
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
int? a;
int? b;
int? c;
@override
void initState() {
super.initState();
Future<int> fn() async {
setState(() {
a = 42;
});
return 42;
}
Future<int> fn2() async {
final result = await fn();
setState(() {
b = result * 2;
});
return result * 2;
}
fn2().then((value) {
setState(() {
c = value * 2;
});
});
}
@override
Widget build(BuildContext context) {
print('a $a b $b c $c');
return const Placeholder();
}
}
This app will print to console the following:
a 42 b null c null
a 42 b 84 c 168
But that a: 42 b: null c: null
state is undesired. That's a frame where if we didn't use async/await
, we could've gotten the value already.
This has an impact on the rendering. Because when the widget renders, its content appears over multiple frames instead of all at once.
If we only go with await
not introducing a delay for a non-Future
, then I'm much more comfortable allowing that, than changing how async
functions work.
That change would be:
Evaluation of
await e
, wheree
as static typeS
and flatten(S
) =T
, proceeds as follows:
- Evaluate
e
to value v.- If the runtime type of v has
Future<U>
as super-interface, withU
\<:T
, then
- Suspend the current
async
orasync*
function (with all that that entails).- Invoke the
then
instance method of v with a type argument ofvoid
, a function onValue as positional argument and a function onErroras named argument with name
onError`. The runtime type of onValue isvoid Function(T)
, and the runtime type of onError* isvoid Function(Object, StackTrace)
.- If the onValue function is called with a value w, then as a later microtask event, the surrounding function resumes, and
await e
evaluates to w.- If the onError function is called with an object x and stack trace s, then as a later microtask event, the surrounding function resumes, and
await e
throws x with stack trace s.- If both functions are called, or either function is called more than once, runtime behavior is unspecified.
- Otherwise, when v is not a
Future<T>
,eval e
evaluates to v.
If we want a short way to introduce a delay, like await null;
today, we can just make the expression optional, and make (await)
introduce a microtask delay before it evaluates to null
at type void
.
That's doable. Heck, even preferable IMO.
It won't make an async
function be able to return a non-Future
. That's something I don't want, because that's where all the incentives are wrong.
That's a frame where if we didn't use async/await, we could've gotten the value already.
That has nothing to do with await
though. You will see exactly the same behavior if you simply use built-in Future
:
@override
void initState() {
super.initState();
setState(() {
a = 42;
});
Future.value(42).then((result) {
setState(() {
b = result * 2;
});
return result * 2;
}).then((value) {
setState(() {
c = value * 2;
});
});
}
Future
listener chains when stack is empty. You get build
invoked after initState
but before control unwinds all the way to the event loop - consequently you only see a
value which is set synchronously in initState
. There is another invariant: if propagation has started values will propagate through as far as they can go while microtask queue is being drained. You can see this on b
and c
"arriving" at the same time despite being separate from each other with a level of Future.then
.
So I think the fundamental question we should be asking is: are invariants around value propagation guaranteed by Future
worth the inconvenience they cause in cases like this?.
TBH I am not really sure if they are worth it: I kinda understand that they are added so that it is easier to reason about interleaving of callbacks (e.g. "then
callback never interleaved with other synchronous code" or consequently "async function execution is never interleaved"). But is this invariant really worth maintaining? We even begrudgingly allow developers to violate the invariant around await
by implementing Future.then
in an incompatible way:
print('before');
(() async {
print(await SynchronousFuture(10));
});
print('after');
Will actually print
before
10
after
We probably can't break existing Future
and async
, but what if this worked:
// library dart:async
final class SynchronousFuture<T> {
// ...
}
extension ToSync on Future<T> {
external SynchronousFuture<T> get synchronous;
}
// User code
SynchronousFuture<int> foo() async { // Return type serves as marker.
await bar(); // equivalent of `await bar().synchronous`.
}
If we have a taste for something even more ambitious we could go even further and have a separate modifier and bring our async story closer to Kotlin and Swift with "auto awaiting":
extension on Future<T> {
/// If the value available on the Future will return its value
/// synchronously without suspending the current coroutine.
T get value coro;
}
int bar() coro {
return fut.value + 1;
}
int foo() coro {
return bar() + 2;
}
You will see exactly the same behavior if you simply use built-in Future:
Indeed. But that's why this discussion is about FutureOr. I'm only showcasing how a delay impacts Flutter.
The goal here is to be able to replace Futures in my snippet with FutureOrs somehow, keep an "await"-like syntax, while also removing that frame with null
values.
Ultimately, this boils down to convenience. There are lots of possible workarounds, but none of them are very convenient.
I've personally been playing around with implementing a custom return
keyword, using a callback that returns Never
; combined with an extension on FutureOr
to convert values into SynchronousFutures
:
extension<T> on FutureOr<T> {
Future<T> get asSync {
final that = this;
if (that is! Future<T>) return SynchronousFuture(that);
return that;
}
}
/// A custom `return` keywords that works by throwing an exception, later caught by [asyncOr]
typedef Returns<T> = Never Function(T value);
/// A custom `async` modifier that returns a FutureOr.
FutureOr<T> asyncOr<T>(
Future<T> Function(Returns<T> returns) cb,
)
Then used as:
FutureOr<int> getValue() => 42;
FutureOr<int> multiplyBy2() => asyncOr((returns) async {
final value = await getValue().asSync;
returns(value * 2);
});
But I think something's broken in Dart at the moment. Even though the return value appears to be correct, I am seemingly unable to do print(await multiplyBy2())
... even though doing print(multiplyBy2())
works fine.
We even begrudgingly allow ...[
SynchronousFuture
]
and
But I think something's broken in Dart at the moment ...
are probably related.
We don't allow SynchronousFuture
, begrudgingly or otherwise, and there are certainly some parts of our async implementation that chokes on a synchronous future, because it's not ready to receive a value when it calls then
.
It's an accident when it works, even if that is more often than I'd have expected, but SynchronousFuture
was never supported. Nothing is broken in Dart, the broken thing is SynchronousFuture
itself.
... a consequence of the invariant which it is built around: values only propagate through
Future
listener chains when stack is empty.
That's an invariant. That's pretty much the only invariant that we guarantee, and which we also assume ourselves.
I'd be very, very worried about loosening up on that, because there is no telling how much code will simply break if calling .then
will synchronously call the callbacks if the future is already completed. (But we could check!)
There is another invariant: if propagation has started values will propagate through as far as they can go while microtask queue is being drained.
That's not an invariant, that's just an implementation detail. We do not promise that it is so, and we don't promise not to change it. (We do know that changing timing can break bad code out there, with "bad code" being defined as code which relies on async timing that was never promised.)
It's really just for efficiency and latency - when most futures have exactly one listener, it's efficient to propagate the completion eagerly through any callbacks that return a result synchronously. We can do that in one big loop, instead of scheduling another microtask, which introduces more latency and overhead. If something prevents us from continuing with such a propagation, we just give up, and continue later, with no promises broken.
I'd be very, very worried about loosening up on that, because there is no telling how much code will simply break if calling
.then
will synchronously call the callbacks if the future is already completed. (But we could check!)
Way way back when @nex3 and I were first writing pub, I remember running into some issues around this. This was before async
/await
was in Dart so we used manual Futures for everything. We had some code that was traversing a queue (I think? been a while) of arbitrary size. If we did the traversal asynchronously by having then()
in the loop, it worked fine because the stack unwound at each step. But if we accidentally moved all of the async out of the loop, we could run into stack overflows.
In short, I think it's possible that there is a lot of code out there implicitly relying on async code essentially doing tail call optimization. Much of that could break if we start synchronously calling then()
. Worse, that code probably won't break tests which have fairly small data sizes that fit within the stack size, but then could break in the wild on real-world data sizes.
To be fair, the idea of making then
synchronous would be to have it opt-in.
I think we all agree that the current behaviour is a good default. It's just that in some cases, we want the ability to have a function optionally execute synchronously ; and at the moment the syntax for that is horrifying compared to async/await
Assume we had both async
and asyncOr
functions, where asyncOr
functions returned FutureOr
and ran synchronously if possible. Let's even assume that await
would be synchronous on any non-Future
.
Which new async
function you wrote would you not make asyncOr
?
(But isn't that reason enough to give them that? Maybe. But see above for why that could end up more computationally expensive than today, because of all the extra is Future
checks. But that cost is all at the call site, not where the function is being written, so the incentives are not aligned.)
Which new async function you wrote would you not make asyncOr?
It depends on the tradeoffs between async/asyncOr.
Since the tradeoff list isn't exactly fixed due to talking about an API that doesn't exist yet, we can only speculate.
I can see a few possible tradeoffs from this conversation:
async
may be more "consistent" in terms of exception. I don't think we're decided whether:
FutureOr<T> fn() asyncOr {
throw err;
}
throws synchronously, or returns a Future.error
. If the former, async
would be safer to use due to less edge-cases to handle.
async
would be less CPU intensive than asyncOr
.asyncOr
gives more granular control over how many ticks of the event loop it takes for a function to complete.So the balance is favours pretty strongly async
for most use-cases, as few functions care about how many ticks of the event loop they take to complete.
I would expect most asyncOr
usages to be specifically related to async cache and data transformation for the sake of UI rendering.
This is what triggered me to request a synchronous await
/async
many times, and seems to be how Flutter uses its SynchronousFuture
too.
A quick Github Search of the Flutter repo shows that SynchronousFuture
is used for:
This effectively always linked to immediately updating the UI the very first frame where a UI element appears/disappears.
In this case, we don't care about performance, we care about the correctness of the UI output.
in #25, there's a proposal to introduce a suffix form of await, like
var x = foo.bar().await.baz().await;
This "suffix await" can be defined to optionally take the form of a function call:
var x = foo.bar().await().baz().await();
Which gives a chance to pass extra parameters to await().
FutureOr<int> x = ...
int y = x.await(immediateIfTheValueIsReady: true);
No need to introduce asyncOr
keyword. The behavior is controlled by the caller. The ugliness of this form guarantees that people won't use it without a reason.
Are you saying:
Future<int> fn() async => 42;
Future<void> fn2() async {
print(fn().await(sync: true));
}
void main() {
print('a');
fn2();
print('b');
}
Would print:
a
42
b
?
Yes, this is exactly what I'm saying :-) Except that the returned value of fn should be FutureOr<int>
to allow sync:true
parameter to take effect.
This "suffix await" can be defined to optionally take the form of a function call:
That would conflict with Future<String Function()> asyncFactory = ...; print(asyncFactory.await());
.
Just sayin'. (All the good syntaxes are taken!)
How about
This "suffix await" can be defined to optionally take the form of a function call.
To me, it looks more natural with the parens anyway. (you can also pass a "timeout" or something, so it's not only good for "sync")
On a different thought:
What about exposing Future.value
?
Folks could then do:
extension<T> on Future<T> {
Future<T> get asSync {
if (value case final T value) return SynchronousFuture(value);
return this;
}
}
For this, you don't need to expose "value" or write an extension. Assuming that runtime knows how to differentiate between a real Future and a Future wrapped around the value, Future can provide a method like "tryAsSync" out of the box. The details of implementation can remain hidden.
if (value case final T value) return SynchronousFuture(value);
Won't work if T
is Object?
.
If we have something like extension <T> on Future<T> { Result<T>? get tryResult => ... }
, then we can distinguishe a value from an error, and both from not being done yet.
But you can do that yourself with a wrapper, like EagerFuture
above, no changes needed.
The one thing you can't do today is to have an async
function produce a value synchronously, which is available when the function returns. (And there are no plans to change that.)
The one thing you can't do today is to have an async function produce a value synchronously, which is available when the function returns. (And there are no plans to change that.)
Yes, but a Future.value
or whatever equivalent we have could be immediately available
We could have:
Future<int> fn() async => 42;
void main() {
print(fn().value); // 42
}
Since fn
returns immediately, the .value
should already be set
But you can do that yourself with a wrapper, like EagerFuture above, no changes needed.
No EagerFuture doesn't solve the problem like counter-argued above. It has a too poor usability when compared to async/await.
Have you tried to return a tuple (Future, value)? The idea is to NOT mark the function as async.
(Future<int>?, int?) foo(int n) /* NO "async"! */ {
return n==0? (null, 0) : (Future.value(n), null);
}
var x = switch (foo(42)) { // passing 0 will return an immediate value
(Future<int> f, _)=> await f,
(_, int? v) => v!
};
(I couldn't find a way to verify it works as expected though).
EDIT: turns out, rust follows essentially the same template. The following code is produced by Gemini
enum DataResult {
FutureVariant(futures::future::Ready<i32>),
ValueVariant(i32),
}
fn get_data() -> DataResult {
if some_condition() {
DataResult::FutureVariant(futures::future::ready(42))
} else {
DataResult::ValueVariant(100)
}
}
fn main() {
let result = get_data();
match result {
DataResult::FutureVariant(future) => {
// Need to wait for the future to complete
let value = future.await;
println!("Got value from Future: {}", value);
},
DataResult::ValueVariant(value) => {
println!("Got immediate value: {}", value);
},
}
}
The problem is that these either-or functions are potentially contagious, leading to a third color. The same is true for "Result or error" functions. And combinations of those. The program turns into a mess where you can't see the forest for the trees.
Have you tried to return a tuple (Future, value)? The idea is to NOT mark the function as async.
That's no different from using FutureOr, which is horrible in terms of usability. It's error prone and verbose, for something we can to many many times.
It's a problem users of my packages can commonly face. I want to offer a proper solution that's usable my many.
I agree. On top of that, it doesn't fit in dart's laconic style IMO. f.await(sync:true)
looks like a much better syntax candidate.
I had a discussion about this issue with @lrhn and I have also looked a bit into Flutter sources to get the context.
(@lrhn could you read this and tell me if I wrote something incorrectly?)
Flutter intentionally builds the widget tree only once per frame. This is quite clearly stated in documentation for Element.markNeedsBuild
:
/// Marks the element as dirty and adds it to the global list of widgets to
/// rebuild in the next frame.
///
/// Since it is inefficient to build an element twice in one frame,
/// applications and widgets should be structured so as to only mark
/// widgets dirty during event handlers before the frame begins, not during
/// the build itself.
This decision effectively means that you can only create widget tree from synchronously available data, and any data hidden within Future
s will need to wait the whole frame before you can do something with it. Flutter does drain microtasks queue between frames, but by that time the tree is already built and it is too late to do anything.
Flutter attempts to offer a work-around for this problem by providing SynchronousFuture
, which has a slightly misleading documentation:
/// This is similar to [Future.value], except that the value is available in
/// the same event-loop iteration.
This documentation is misleading because the result of Future.value
is also available in the same event-loop iteration, because propagation of value from Future.value
to the callback will happen as a microtask, not an event. What distinguishes Future.value
and SynchronousFuture
is whether then
callback is invoked immediately (synchronously) or deferred to a micro-task.
Yes, it probably could. One approach I could envision is recognizing that some elements have an asynchronous component and requiring microtask flush before finishing their build:
rebuildDirtyElements();
do {
drainMicrotasks();
rebuildDirtyAsyncElements();
} while (microtasks.isNotEmpty || delayedAsyncElements.isNotEmpty);
In this model FutureBuilder
will not immediately build its child, but wait until microtasks are flushed and data is available (or it is known that the data will not be available this event loop turn).
This model does the same amount of work as the current synchronous build: you don't rebuild the tree multiple times, but simply delay building parts of it until after you drained the microtasks which produces data for these builds. It is more aligned with microtasks based Future
resolution model.
The overall structure of widget tree building becomes much more complicated then it is now, and it will probably come with some amount of overhead which might be a strong enough argument against doing that.
Lets assume that solving this in Flutter is not going to happen. What could we do on the Dart level? Well, we could completely change the semantics of await
:)
await f
does not require a suspension:
f
is a not a Future
, then await f
completes immediately with value f
;f
is a completed internal implementation of Future
, then await f
completes immediately with value (or an error) of f
.await null
expression (with literal null
as an argument) is a special case: it requires a microtask even though v = null; await v;
does not. Future<T>
which allow synchronous access to the underlying value, e.g. Future<T>.isCompleted
and Future<T>.value
(or maybe just FutureState<T> get value
which incapsulates not-completed, completed with value and completed with error states as a sealed class family?).SynchronousFuture<T>
and tell people to just migrate away. It can just become a wrapper around core Future
. Eventually we delete SynchronousFuture<T>
and mark Future<T>
final
.We are open to changing semantics of await
, but not semantics of Future.then
(or any other Future
APIs which take callbacks). This means the code which currently uses then
(e.g. FutureBuilder
) will need to be updated to benefit from synchronous value propagation.
The big open question is whether we will be able to roll such a change out: our previous experiences with touching async timing was that it requires somehow fixing hundreds if not thousands of poorly written tests in the internal monorepo. If breakage is too big and not toolable then we need an alternative rollout strategy.
While I was writing this I realized that maybe we could package this together with some other changes into a new type of async
function. Having special keyword (maybeAwait
) seems wrong, but an async
function that does not require any await
might be actually the path forward? Consider for example:
Future<int> foo() async {
return await bar() + await baz();
}
extension on Future<T> {
suspending T get value;
}
suspending int foo2() {
return bar().value + baz().value;
}
I am more or less lifting suspending name from Kotlin, for the lack of better vocabulary.
[^1]: If we change semantics of await
we can just as well provide these methods because they can be implemented in terms of await
anyway.
FutureState<T> get state {
var result = FutureState<T>.incomplete();
(() async {
try {
result = FutureState<T>.value(await this);
} catch (e, st) {
result = FutureState<T>.error(e, st);
}
})();
return result;
}
What if we just... added a ?
? await? future;
(or async?
which could return FutureOr
or a Future
who's .value
exists)
That way it suspends if it isnt complete, and doesn't if it is complete.
Honestly, having synchronous access to future values could be super useful, as it would become trivial to determine if it is completed without needing to indirectly check though .then((_) => _completed = true)
which obviously is less ergonomic.
Hell, this might even make FutureOr
almost obsolete, since you can just... access the completed value directly, or await if its not done, or just await?
to do both. It's only use would become for function signatures to not need async
, but then it could just become a special-cased typedef for just that.
await?
/async?
would also avoid breaking anything.
That being said, it also wouldnt allow existing code to automatically become sync when there's no actual async occurring, but it may be the most logical solution for anything that expects normal await behavior.
That being said, it may limit the usefulness some - so we should see if we can just reuse await
itself
This could be really neat for eager initialization of futures while keeping access to the future so that both sync and async tasks can use it in whatever manner makes sense.
We could also choose to have await?
change the semantics of await
to match it in the function we're calling, such that
Future<bool> get() async => await Future.value(true);
Future<void> doThing() async? {
final res = await? get(); // as if it were Future<bool> get() async? => await? Future.value(true);
assert(res.value == true) // or whatever the API is
}
The problem is that as soon as await?
becomes available, there will be no justification for a raw await
except in the rare cases where the distinction is important. It would be more logical to change the legacy await
to await!
then, and interpret await
as await?
by default.
The problem is that as soon as
await?
becomes available, there will be no justification for a rawawait
except in the rare cases where the distinction is important. It would be more logical to change the legacyawait
toawait!
then, and interpretawait
asawait?
by default.
Makes sense! perhaps this is a good point to apply versioning to.
Basically, just inject an await null;
into the font of any async function before this
version, and we're probably good.
@mraleph
While await
is problematic, there is a currently available workaround (SynchronousFuture). It's not ideal, but "works".
IMO the fundamental problem is async
. We can't have async
return a SynchronousFuture
if we wanted to.
That's the gist of the issue what what forces users to stop using async/await.
And although arguably less important, async* has similar issues. The function always starts asynchronously instead of immediately when the stream is listened. And yield
has the same issue as await
afaik.
The main problem here is Flutter's behavior, where it renders something, and then changes it after a change happening during the same microtask queue.
The Dart microtask queue is modeled after the browser microtask queue, because that's what it needed to be compatible with, in order to work with browser resources that have a lifetime of "one event" ... meaning the recent and the microtasks belonging to that event. The event isn't done until all the microtasks are done. It's like microtasks are synchronous events that happen right now, not arbitrarily later.
In the browser, UI updates happen as top level events. Change events are microtasks. That means that a cascade of changes will have all completed and stabilized before the UI is updated.
It Flutter followed that model, rather than choosing what to render before all microtasks had been run, the problem wouldn't exist here. Using a FutureBuilder
with an already completed future would schedule a microtask, that microtask would run, and the model would have its final state before anything got rendered.
In the browser, UI updates happen as top level events. Change events are microtasks. That means that a cascade of changes will have all completed and stabilized before the UI is updated.
I want to point out that this applies to raw DOM APIs. Frameworks like React behave similar to Flutter (as far as I can tell in my quick experiments). If you update state as a result of microtasks running you will only see updated UI when event loop turns around, because VDOM is not going to be rebuilt immediately.
I've been looking through the Future code, and... as far as I can tell, it looks like the value already exists as a private variable. var _resultOrListeners;
which, for some reason, is pulling double/triple duty.
I honestly don't see much reason why we couldn't add a T? get valueOrNull;
+ (AsyncError? errorOrNull
) to put the final resolved value into. after all, if it's completed, there's no reason we shouldn't be able to get the value immediately. hell, an _isComplete
already exists too! just un-private it.
alternatively, a T get value;
that just throws either the completed error or a "future not completed" error
It's incredibly odd to me that these are already available, and yet arent exposed. the utility of having access to those two alone is incredible
Even if all we got was sync access to Future values when the future is known to be completed, it would be a major step forwards. Changing how async+await works could honestly wait till later (in the - ahem - Future). It would let us freely pass futures around, which would then let the callers handle the data how they please.
Hell, it makes the FutureOr thing easier, as you can just do Future.value(futureOr).valueOrNull;
or something like that, (an extension on FutureOr would make this trivial) which would also allow you to synchronously access the value even if its actually a completed future.
Honestly, what are we missing to make this much happen? it seems almost trivial. Frankly, unless I'm missing something, the only issue is that its technically a breaking change for anything that implements Future
, but that's hopefully just Flutter's own SynchronousFuture
which would either suddenly become unnecessary, or just get updated by the relevant team trivially.
So... to reiterate
_isCompleted
as bool get isCompleted;
T get value;
(that throws) or T? get valueOrNull
(that doesn't throw, disambiguated by isCompleted
)
SynchronousFuture
break?
SynchronousFuture
to be deprecated, in favor of checking the completed value instead of an unusual .then (which could open the possibility for an extension that exposes a .thenSync that does the same, but for any future)await
semantics to act sync when the future is already completed
await null;
(or a blank await;
) could let you manually opt out of that, if your terrible code relies on timing so muchawait;
to the front of any async function that doesn't support this version, now does anything break?What are we actually missing? What roadblocks are there? Is there any reason we cant ship #.1 "now" and get to async+await semantics later?
It has always been a non-goal to allow synchronous access to future results. You get the result "when it's ready", as a push event, and you have no control over the scheduling of synchronous events.
If you have two ways to access the result, it encourages writing two code paths, where it's hard to ensure that both code paths are tested.
Or writing code that assumes that a future is always already completed, which makes it harder to optimize the asynchronous implementation code in a way that may change timing. For example, the distinction between setting the result on the future and asynchronously scheduling callbacks, or asynchronously scheduling setting the result and then immediately propagating to callbacks, which is an implementation detail today, would become observable.
You should treat microtasks as being "nearly synchronous" and happening as a unit. Listening to a completed future gets you notified in the same microtask queue, that's part of the same synchronous execution of the microtask queue loop, no real asynchronous timing gap will happen. Asking for a result sooner is just changing the order of code that is all going to get executed "synchronously" during the same event.
I'd argue that already completed futures dont need timing at all. There's already ways to extract a value when completed if the future is awaited somewhere, at some point, by setting it to a variable.
But then, why cant futures just have a variable on them that gets set via .then?
That way, timing becomes irrelevant, as its effectively just avoiding a wrapper object.
Something like this:
class Future<T> {
Future() {
then((v) {
isComplete = true;
valueOrNull = v;
);
}
bool isComplete = false;
T? valueOrNull;
}
I don't think anything breaks this way. additionally, the future itself decides when that value is set. If its completed, the value is there. But it may not be considered completed until after that microtask loop completes (and the value isn't there until it does)
This effectively removes the need for indirection and wrapper objects in a number of cases, and still trivializes FutureOr usage. It also decreases the burden of needing to remember to go and do that, which can be especially useful if some wrapper object you don't control does await the future, but doesn't expose the resulting value, even though you really should be able to use that value sync by now. after all, the value isn't "in the future" anymore is it? its here. its right there even, but we just cant use it, even though we know its there.
As far as I can tell, this effectively removes the timing concern doesn't it? It just makes Future more usable. .then
still acts the same, its just that it might have already happened.
-- we might want a lint that prefers await
over valueOrNull
in an async method though.
It's definitely possible to wrap a future in something that stores the value when the future calls its callbacks. That wrapper can even implement Future
.
By all means do that, if you really want to. It's effectively the same as caching the result and the future together. But it's caching that you choose to do when you know that you want to.
If someone gives you a plain future, you shouldn't be wondering whether it may already be completed. You should assume it's not, because futures are rarely shared, and should be listened to immediately when they are created, to ensure that errors are handled in time.
Adding the caching to the future type itself is changing its intended use-cases, from being the eventual result of a, most likely ongoing, computation, to being a way to store that result in perpetuity. That is not its intent today, you want to store the result, you do that in the callback. Needing to store the result is a specific use-case that you can program for, but it shouldn't be baked into the Future
type,ø.
Or just await the future. If it comes back within the same microtask queue, during the same event, that should be soon enough.
I'll throw in a wrapper, for good measure:
Copyright 2024 Google LLC.
SPDX-License-Identifier: BSD-3-Clause
import 'dart:async';
/// A future and its eventual result.
///
/// Contains a [future] and, when that future has completed,
/// the future's result.
///
/// The result can be accessed synchronously using [result], which must
/// only be used when the result is available as specified by [hasResult].
/// Do notice that reading [result] will throw an error if the future
/// completed with that error.
///
/// The result can be accessed optimistically using [tryResult] which provides
/// the synchronous result if it's available, and otherwise the [future] which
/// is still the most precise representation of the eventual result.
final class FutureCache<T> {
Future<T>? _future;
_Result<T>? _result;
/// Creates cache with existing result.
FutureCache._result(this._result);
/// Creates a cache for the eventual result of [future].
///
/// The cache will not be filled with a result immediately, even if the
/// future has already been completed. The result will only be available when
/// the future has responded to a [Future.then] call, which mush be done
/// asynchronously.
FutureCache(Future<T> future) : _future = future {
future.then<void>((T v) {
_result = _Value<T>(v);
}, onError: (Object error, StackTrace stack) {
_result = _Error(error, stack);
});
}
/// Creates a cache that is pre-filled with the given [value].
///
/// Also creates a [future] which complete with [value].
FutureCache.value(T value) : this._result(_Value<T>(value));
/// Creates a cache that is pre-filled with [error] and [stack] trace.
///
/// Also creates a [future] which completes with [error] and [stack]
/// as an error.
FutureCache.error(Object error, StackTrace stack)
: this._result(_Error(error, stack));
/// A future that completes with [result].
///
/// If no result is available yet, as reported by [hasResult], then
/// cache is waiting for this future to notify of its completion.
///
/// If a result is available, then the future will provide the same
/// result if awaited.
Future<T> get future => _future ??= switch (_result!) {
_Value<T>(:var value) => Future<T>.value(value),
_Error(:var error, :var stack) => Future<T>.error(error, stack)
..ignore(),
};
/// Creates a cache for the value or future of [result].
///
/// If [result] is a `Future<T>`, the created cache waits for that
/// future to complete before the result is valid.
///
/// Otherwise the created cache is already filled with the provided
/// value, and its [future] is created as a future completing with that
/// value if necessary.
factory FutureCache.of(FutureOr<T> result) {
if (result is Future<T>) {
return FutureCache<T>(result);
}
return FutureCache<T>.value(result);
}
/// Captures the result of a synchronous or asynchronous computation.
///
/// Runs [computation] and returns a cache that is either filled with
/// an available synchronous value or error results, or that waits
/// for the result of the future returned by [computation].
factory FutureCache.sync(FutureOr<T> Function() computation) {
try {
return FutureCache<T>.of(computation());
} catch (e, s) {
return FutureCache<T>.error(e, s);
}
}
/// Whether a result is available.
///
/// A result is available after the [future] has completed invoked its
/// listeners, which happens asynchronously at some point after the
/// future completed.
///
/// Until there is an available result, the [result] getter must not be
/// used, and the [tryResult] getter will return the [future].
///
/// After a result is available, both [result] and [tryResult]
/// synchronously provides that result, as a value or by throwing an error.
bool get hasResult => _result != null;
/// The result of [future] provided synchronously, when available.
///
/// Must not be used until a result is available, as reported by [hasResult].
///
/// When the result of [future] is available, evaluating `result`
/// will either return the result value, or (re-)throw an error result.
T get result => (_result ?? (throw StateError("Not completed"))).value;
/// The result of [future] provided as synchronously as possible.
///
/// If a result is available, as reported by [hasResult], this getter
/// provides that result, just like [result].
/// Until a result is available, it instead evaluates to [future].
/// Notice that in the former case it may synchronously throw an error.
///
/// Awaiting the returned value is equivalent to awaiting [future],
/// but can also be checked for whether the value is available synchronously.
FutureOr<T> get tryResult {
var result = _result;
if (result != null) return result.value;
return _future!;
}
}
/// The result of a computation.
sealed class _Result<T> {
/// Read to reify the result.
T get value;
}
/// A value result.
final class _Value<T> implements _Result<T> {
final T value;
_Value(this.value);
}
/// An error result.
final class _Error implements _Result<Never> {
final Object error;
final StackTrace stack;
_Error(this.error, this.stack);
Never get value {
Error.throwWithStackTrace(error, stack);
}
}
Being able to synchronously access already resolved futures doesn't really help anyway.
await
and async
would still add introduce a delay regardless.
@rrousselGit Correct. That's a tangent for this issue's actual requests: A synchronous await of FutureOr
which is not a future, and an ability to return a FutureOr
synchronous value from an async
-like function.
I'm very positive towards allowing an await
of a call to an async
function to return a result synchronously, if the result is available synchronously.
In practice, an await asyncFunction(args)
can already be implemented to continue synchronously (and run on the same stack if it wants to) if the runtime recognizes that the function being called is declared as async
. It doesn't even have to allocate a Future
, it can just return directly to the caller if it manages to complete synchronously, and if not, then it can create a future (or some other internal signal) and suspend the caller until it's actually done.
The behavior of await
and async
-function invocation are deliberately designed so that this optimization is possible, because all await
promises is that it won't continue until the asynchronous computation is done.
(For example, an await
invocation of an async function could pass in onValue
and onError
functions to the async
function, and if it has those and completes synchronously, it can just call one of those functions instead of creating a future, and if it's async, create a future and add those as listeners. A normal (non-await
) call of the function wouldn't pass those functions, and would get a future back in all cases.)
So it's allowed by the spec, it's just an optimization.
I'm possibly willing to allow await
to continue "immediately" (synchronously or as the very next microtask, which should be indistinguishable if all code is well-behaved) on an already completed system future.
It has to be a system future, we don't have a way to see if user futures are already completed, because they only expose the Future
interface.
As @mraleph points out, if we have this feature, we can detect whether the result is available (and get it if it is).
Not super happy about it, but it's not completely outrageous.
I don't consider that breaking the API of Future
because any magic happens at a point where the code would otherwise be suspended at the await
. It's not detectable what happens between the await
and the resume, and we don't make any promises about how soon it will resume. (We probably do promise to pause await for
stream subscriptions before the await
, even if it continues synchronously.)
That is, it's still just a matter of which interleaving we give asynchronous callbacks. (Except that's not entirely true, because calling an async
function without await
in front starts code running synchronously, and ends at an await future
, which doesn't suspend the current execution thread. Performing that await
synchronously runs it not as a new microtask, but in the middle of other synchronous computation. (I guess I'd want to only allow this optimization if the current execution thread is actually suspended at the await
.)
I'm not at all positive about allowing synchronous interospective access to a completed future's result from just any code.
The Future
is designed to make that impossible, because it's a clear abstraction boundary, and because it makes it clear that there is only one thing to do with a future: Wait for it.
That is not its intent today, you want to store the result, you do that in the callback. Needing to store the result is a specific use-case that you can program for, but it shouldn't be baked into the Future type,ø.
I get that, but the problem is that; depending on the use case, we can end up forced to "stutter" with a loading value even though the value may already exist.
Going from nothing to a loading state to a value state over the period of a microtask is jarring
Especially since we may have "lost the opportunity" to extract the value "synchronously" by the time we're in a sync function.
Hell, it's why SynchronousFuture exists at all, and it's not necessarily limited to Flutter use cases.
That wrapper object would always throw if you tried to use it right away, even if the future completed, unless the inner implementation happened to be a SynchronousFuture that was already processed.
I'm almost certain that people would be using FutureOr way more if it were first class, but then that still doesn't handle synchronizing the future at any point.
After all, at some point, we aren't waiting anymore right?
It doesn't make sense to me to wait for a value that's already here.
Could we alternatively say that .then does act immediately if the future is completed instead, SynchronousFuture-style? Would that break anything?
Maybe add a named parameter like then(sync: true) for that behavior? Doesn't break anything and let's us use it as we please
Hell, depending on how you possibly handle synchronous await like you're describing, it could effectively be the same solution.
Going from nothing to a loading state to a value state over the period of a microtask is jarring
Again, that wouldn't be visible if the UI didn't update in the middle of a microtask queue execution. If it just waits to the end, after all change events and their future propagations have stabilized, nobody would see the flash of loading.
Could we alternatively say that .then does act immediately if the future is completed instead, SynchronousFuture-style? Would that break anything?
Definitely. A lot of code has been written under the assumption that you can call then
, and do more initialization (synchronously) after that, possibly using the future returned by then
.
The initial implementation of Future
and Stream
actually did call immediately if a value was available, and the feedback of the first users were that that was confusing and error prone, because it was unpredictable.
It was very easy to write code that assumed a later callback, and what then broke on the occasional immediate callback.
You had to write more convoluted code to be able to defensively handle two different behaviors in the same code path.
Maybe add a named parameter like then(sync: true) for that behavior? Doesn't break anything and let's us use it as we please.
That would be a better API. You have to opt in to the otherwise surprising behavior, so you wouldn't/shouldn't be surprised.
It's hard to change the API of Future
, though. There are many classes that implement Future
, and which would be broken by any chance to the interface signature. (Giver me "Interface default methods" please! Then we could add thenSync
or something. If we had created Future
today, it would probably have been final
.)
Those implementations would also have to understand the parameter, but I guess ignoring it is still valid behavior, the value might not have been available anyway.
Sounds like we have a viable idea.
It's hard to change the API of
Future
, though. There are many classes that implementFuture
, and which would be broken by any chance to the interface signature. (Giver me "Interface default methods" please! Then we could addthenSync
or something. If we had createdFuture
today, it would probably have beenfinal
.)
I wonder if there's some extension BS we could do? The most straightforward option is to extension type it such that only _Future
gets the benefit
The fact that it was designated an interface instead of a mixin or something unfortunately does make it tricky.
Its possible we could do some funky shit like allowing subtypes to not include {bool sync}
in their signatures (effectively a noop) or doing funkier shit like injecting a thenSync => then
into everything...
the other option then would be to just... break it. then maybe force people to mix it in instead of implementing (or otherwise some compiler BS to special-case transform it...)
eh.
imo it seems like the most useful option is to permit "subtypes" of functions to exclude parameters they don't use (which, as an aside, can make the (x, y, _) thing a bit cleaner)
currently, subtypes of Functions can only add optional parameters. Maybe we can permit them to exclude named parameters they don't need? The signature and type would still be the same - it would just effectively be syntax sugar for having the parameter, and just... not using it.
effectively:
typedef Fn = Function(int, String, {double percent})
final Fn fn = (i, n, {percent}) {}; // normal
final Fn fnPlus = (i, n, {percent, String additional = ''}) {}; // normal subtyping
final Fn fn2 = (i, n) {} // special syntax. effectively identical to (i, n, {percent}) {}
abstract interface class Future<F> {
T then<T>(T Function(F) cb, {bool sync = false});
}
class CustomFuture<T> implements Future<T> {
T then<T>(T Function(F) cb) {...} // lint warning: specify all parameters in function signature. still compiles.
}
this would require language support in some way, but it could cleanly resolve it, and we could even add thenSync
as an extension method that just does then(sync: true)
we could easily then just have a lint warning you that you arent using a parameter, which should nicely wrap up any remaining concerns
It could also make adding parameters to an interface no longer a breaking change, allowing for stagnant classes (like Future
) that simply cannot change (there's extension methods yes, but we've just demonstrated why that's not always viable) to do more
Just to be clear, I'm not necessarily asking for new subtyping rules - effectively the method could just have the parameter injected or something.
Is that worth an issue? thoughts?
Having "optional optional parameters" that an implementation doesn't have to understand, can be done in two ways:
call
behavior. The former is fairly simple to implement, but invasive and error prone. It makes a superclass and to change the implementation of a subclass method, and if actually acting on the argument is necessary to satisfy the method semantics, then the subclass silently gets a wrong implementation. (Don't use the feature then?)
The latter is a big change to the type system.
It introduces a supertype to fx void Function()
and void Function(int)
, which must be called with one argument.
That supertype should probably never be the result of an upper bound operation, it must be something you opt in to.
Then a subclass method override of a method with such a function type signature can omit a parameter, which makes it not a breaking change to add such a parameter in the superclass.
(Again, unless the method must react to the parameter to behave correctly.)
(Languages with overloading and interface default methods have it easier. Looking at you Java.)
As per this request, I'm opening a new issue. In Dart, the keywords async and await will convert any function in an asynchronous one. It would be useful if for cases like FutureOr, it would be possible to access the value synchronously first if possible, and if not, the asynchronously. I saw on this topic this and this but I haven't found a resolution yet.
Ex:
Solution A: Introduce a new term for awaiting a FutureOr value or adapt async and await to allow to run code synchronously if possible
Solution B: Why doesn't FutureOr allow a then method as Future? It won't be maybe the best solution out there as it would be requiring nesting in the above
differenceAB
function but at least it works :)