Skip to content

Asynchronous Constructors #782

New issue

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

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

Already on GitHub? Sign in to your account

Open
lrhn opened this issue Jan 16, 2020 · 9 comments
Open

Asynchronous Constructors #782

lrhn opened this issue Jan 16, 2020 · 9 comments
Labels
request Requests to resolve a particular developer problem

Comments

@lrhn
Copy link
Member

lrhn commented Jan 16, 2020

Users, especially new users, keep running into situations where they would like to create objects where part of the initialziation is asynchronous. They then ask for asynchronous constructors. The answer has, so far, been: Use a static factory function instead.
(dart-lang/sdk#23115, https://stackoverflow.com/questions/38933801/calling-an-async-method-from-component-constructor-in-dart, )

However, there isn't anything inherently impossible about asynchronous constructors.

Generative constructors

Consider the following:

class MyClass<T extends num> {
  final T value1;
  final T value2;
  T _sumCache;
  async MyClass.fromFutures(FutureOr<T> value1, FutureOr<T> value2)
      : value1 = await value1, value2 = await value2 {
    _sumCache = value1 + value2 + await somethingMore;
  }
}

The could define an asynchronous generative constructor. It can be redirecting only if it redirects to another asynchronous generative constructor.

Such a constructor has an implicit return type of Future<MyClass<T>>, and it allows using await in both the initializer list and the constructor body. The async is written up front because it modifies both the implicit return type and the initializer list, not just the body as for normal async functions.

It can only be used as the super-constructor of another asynchronous generative constructor.

As usual, the instance is not available to anyone before the body starts running, and at that point the object state is sound. You can leak the object if you want to, but if not, the caller only gets it when the body completes and the returned future is completed with that instance.

Factory constructors

The same way, we can introduce async factory MyClass(...) for an asynchronous factory constructor. It can either redirect to another async constructor or it can have a body which can then user await.

The real question here is whether it's worth doing since a static factory function works. It has to be backed by a generative constructor, though, which makes it more work to implement than if you could combine everything into just the constructor.
It is a recurring issue for new users, and as such a stumbling block. It's typically solved when the user goes to stackoverflow or our forums, but it's still a negative first/early impression.

@lrhn lrhn added the request Requests to resolve a particular developer problem label Jan 16, 2020
@rrousselGit
Copy link

rrousselGit commented Jan 16, 2020

As far as I can understand it, the main benefit is the "super" constructor.

It shouldn't be too much of an issue if the class has an init async protected method though:

class Base {
  @protected
  Future<void> init() {...}
}

class Subclass extends Base {
  @override
  Future<void> init() async {
    await super.init();
    // ...
  }
}

As far I as know, no other language has such syntax. So I'm not sure that newcomers would necessarily stop having to go on StackOverflow for such thing.

@munificent
Copy link
Member

The async is written up front because it modifies both the implicit return type and the initializer list, not just the body as for normal async functions.

This, to me, is the tricky part of this feature. As you note, unlike using async elsewhere, using it here affects the public API of the constructor, not just its body. I worry that if we give people this, they will place it on constructors just because they want to use await inside the body, and then get very confused when users can no longer call Foo() and synchronously get back an instance of Foo.

Given that potential source of confusion, and the possible implementation complexity of chaining to generative superclass constructors asynchronously, my hunch is that this feature doesn't carry its weight.

@lrhn
Copy link
Member Author

lrhn commented Jan 17, 2020

There is also the option of allowing you to write return types on constructors.

If that is allowed, then it might also be possible to write a Future<X> return type which makes it very clear that the constructor does not return an X.

For generative constructors, it would still require the body to be async and implicitly wrap the created object in a future, because there is no return statement that would allow you to return a Future<X> directly. That means that you would probably still need to write async somewhere. Probably before the : of the initializer list, if any, and before the body if there is no initializer list.

None of this is pretty.

I sortof like the idea of writing async early, so you can write

async int foo(int x) => await asyncOp(x);

instead of

Future<int> foo(int x) async => await asyncOp(x);

It would likely reduce the number of accidental

void foo() async { ... }

which should have returned Future<void>.

@HemilTheRebel
Copy link

I have this use case where I want to fetch parts of config from my back end because the client wants to be able to change those parameters from their admin panel. The static method workaround is hacky at best. I agree that it would mean a change in the API of the language. But at least factory constructors could be allowed ;)

@lcrocker
Copy link

lcrocker commented Nov 3, 2021

Just want to point out that a benefit of async constructors is the elimination of runtime null checks. Currently, if your class has members that need expensive initialization, you have to make them nullable or late, as the constructor must return before they can be initialized. But with an async constructor, you could have a non-nullable member initialized, say, from a file, and the compiler would know that no instance of the object could exist until the Future returned by the constructor completes.

@lrhn
Copy link
Member Author

lrhn commented Nov 4, 2021

@lcrocker
The alternative is an async static factory function which calls a private generative constructor to initialize fields with values once all the values are available. I'd recommend doing that today anyway. Don't make things nullable or mutable unnecessarily.

If you don't write new anyway (and we recommend you don't), the difference between an async Foo.constructorName(args) and an async Foo.staticFunctionName(args) is purely philosophical, and I'd allow naming it like a constructor too, if that is its role.

@munificent
Copy link
Member

the difference between an async Foo.constructorName(args) and an async Foo.staticFunctionName(args) is purely philosophical

Insert usual grumblings about generic classes and class type parameter scope here.

@lrhn
Copy link
Member Author

lrhn commented Nov 10, 2021

@munificent Well, philosophical, if you consider it an entirely philosophical issue where the type arguments go 😁.

So, good point.

Actually, we could allow you to write async in front of any function, which then changes its return type X to mean Future<X> and makes the body async (because I don't want two "async"s!)

Then it's

async int foo() => await bar() + await baz();

instead of

Future<int> foo() async => await bar() + await baz();

If we had done that from the start, it would probably have been nice. Doing it now is going to be very confusing when the two styles mix.

@Levi-Lesches
Copy link

Doing it now is going to be very confusing when the two styles mix.

Seeing as both cases have the async before the {} or the =>, I don't think it will be that confusing.

class MyClass<T extends num> {
  final T value1;
  final T value2;
  T _sumCache;
  async MyClass.fromFutures(FutureOr<T> value1, FutureOr<T> value2)
      : value1 = await value1, value2 = await value2 {
    _sumCache = value1 + value2 + await somethingMore;
  }
}

...The async is written up front because it modifies both the implicit return type and the initializer list, not just the body as for normal async functions.

I see the logic, but at the same time, I don't see anything inherently wrong or confusing with:

class MyClass<T extends num> {
  final T value1;
  final T value2;
  T _sumCache;
  MyClass.fromFutures(FutureOr<T> value1, FutureOr<T> value2) async 
      : value1 = await value1, value2 = await value2 {
    _sumCache = value1 + value2 + await somethingMore;
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests

6 participants