I post this proposal with assumption that it’s not a duplication of any other proposal that I’ve overlooked somewhere in related threads, and that it’ll not make any negative impact on any further discussion about generators design. If that was wrong - just ignore or delete current thread.
Intermediate return type definition
Previously there was defined two return type options for async functions
Inner: async fn x() -> T { ... }
Outer: async fn x() -> impl Future<T> { ... }
And this proposal introduces third
Intermediate: async fn x() -> Poll<T> { ... }
Why we should use it instead
To understand that we must shift from current async/await/generators context and become more focused on ideas behind Reactive Extensions and implementation of coroutines in Kotlin.
Current concept just not allows to express all advantages of proposed syntax and it looks weird instead. Therefore, a proper concept would be revealed below and motivation for it would be described at the end of proposal where all things would become more obvious.
Where it begins
Initially I wanted to propose intermediate return type to:
- Make
@ (await proposal) more consistent with existed ? operator
- Make
async functions signatures consistent with Option, Result, Box, … returning functions
- Make
async functions more similar to Future::poll function and simplify desugaring
Introducing a new generators concept
I quickly realized that intermediate return type allows return Pending; in async functions which is unfortunate side-effect at first glance. But after thinking more about it I’ve found something really refreshing and promising: it should be possible to reuse return Pending; to suspend/resume coroutine, and return Ready(...); to finish it!
That has a lot more sense than any other sytnax:
- The meaning of
return word is perfectly suitable
-
Poll enum variants clearly and explicitly describes intention of code
-
async in function signature annotates the bidirectional return behavior inside
- We don’t need
yield construct anymore
- It’s extensible enough to implement generators in exactly the same way
First thought about generators was:
generate fn fibonacci() -> Option<i32> {
let (mut a, mut b) = (0, 1);
loop {
return Some(a);
a = b;
b = a + b;
}
None
}
Further extension
It was obvious that using a separate function modifiers for Future, Iterator, Stream generators would be completely redundant because they duplicates the information that’s also provided by intermediate return type. And I known that sufficiency of annotating function behavior by its return type was proven on practice by Reactive Extensions where Observable, Single, … in signatures is used for it.
Anyway, some uniform function modifier for generators would be still required because we should distinguish between them and regular functions. Again, in this case sufficiency of uniform modifier was proven on practice by Kotlin coroutines where suspend in function signatures is used for it.
So, I selected freed yield keyword for it:
/// Iterator
yield fn fibonacci() -> Option<i32> {
let (mut a, mut b) = (1, 1);
loop {
return Some(a);
a = b;
b = a + b;
}
None
}
/// Future
yield fn request_user(self, user_id: String) -> Poll<Result<User>> {
let url = format!("users/{}/profile", user_id);
let user = self.request(url, Method::GET, None, true)@?
.res.json::<UserResponse>()@?
.user.into();
Ready(Ok(user))
}
But what about Stream
It also was obvious that a different intermediate return type for Stream generators is required because current Poll signature would be ambiguous and as well not descriptive in this context.
I selected the following type instead:
enum Flow<T> {
Next(T),
Completed
}
/// Stream
yield fn download_images(self, max: usize) -> Flow<Result<Image>> {
for i in 0..max {
let url = format!("images/{}", i);
let img = self.request(url, Method::GET, None, true)@?.decode()?;
return Next(Ok(img));
}
Completed
}
A general concept definition
We can think that actually returned Iterator,Future,Stream trait and state machine to handle it are generated based on information from intermediate return type.
I can imagine that yield function parser may seek for some kind of proc macro specified as attribute on top of intermediate return type definition:
#[generator(path::to::proc_macro)]
enum Poll<T> {
Ready(T),
Pending
}
Summary
- This may be the most consistent with all other Rust features proposal
- The final result could be also the simplest to explain for end users
- It may be extensible enough to allow implementing custom generators in future
Limitations
- This proposal don’t addresses any low level concepts and is more focused on appearance instead
- Some lint would be required to enforce termination return type variant at the end
- The syntax is completely different than regular generate/async/await
- Using enums for control flow may be surprising for some users
- Naming may be not descriptive enough but could change
- Overall, it’s more verbose than in previous concept
Prior art
-
Resolve
await syntax thread was from where it originates from
-
Generators I blog post was helpful to identify problems, constrains, and to organize knowledge
-
Inner and outer return types comparison was very inspiring
- RxJava and Kotlin coroutines provided a real-world experience
- Many arguments from async/await threads contained a lot of relevant information