diff --git a/x/io/README.md b/x/io/README.md index f5585c58..79d76666 100644 --- a/x/io/README.md +++ b/x/io/README.md @@ -36,9 +36,8 @@ async function asyncCall(): Promise { return `AsyncCall: ${result}`; } -async function asyncCall2(): Promise { - const result = await resolveAfter1Second(); - return `AsyncCall2: ${result}`; +function asyncCall2(): Promise { + return resolveAfter1Second(); } function asyncCall3(): void { @@ -92,9 +91,8 @@ async def async_call() -> str: result = await resolve_after_1_second() return f"AsyncCall: {result}" -async def async_call2() -> str: - result = await resolve_after_1_second() - return f"AsyncCall2: {result}" +def async_call2() -> asyncio.Task: + return resolve_after_1_second() def async_call3() -> None: asyncio.create_task(print_after_1_second()) @@ -139,7 +137,9 @@ async fn name(param0: Type) -> ReturnType { Example: ```rust -use tokio::time::{sleep, Duration}; +use std::time::Duration; +use tokio::time::sleep; +use std::future::Future; async fn resolve_after_1_second() -> String { sleep(Duration::from_secs(1)).await; @@ -151,9 +151,8 @@ async fn async_call() -> String { format!("AsyncCall: {}", result) } -async fn async_call2() -> String { - let result = resolve_after_1_second().await; - format!("AsyncCall2: {}", result) +fn async_call2() -> impl Future { + resolve_after_1_second() } fn async_call3() { @@ -177,7 +176,7 @@ async fn main() { async_call3(); // Wait for AsyncCall3 to complete - sleep(Duration::from_secs(1)).await; + sleep(Duration::from_secs(2)).await; println!("Main function completed"); } @@ -216,10 +215,9 @@ class Program return $"AsyncCall: {result}"; } - static async Task AsyncCall2() + static Task AsyncCall2() { - string result = await ResolveAfter1Second(); - return $"AsyncCall2: {result}"; + return ResolveAfter1Second(); } static void AsyncCall3() @@ -286,8 +284,7 @@ cppcoro::task asyncCall() { } cppcoro::task asyncCall2() { - auto result = co_await resolveAfter1Second(); - co_return "AsyncCall2: " + result; + return resolveAfter1Second(); } cppcoro::task asyncCall3() { @@ -323,3 +320,90 @@ int main() { return 0; } ``` + +## Common concepts + +### Promise, Future, Task, and Coroutine + +- **Promise**: An object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. It is used to produce a value that will be consumed by a `Future`. + +- **Future**: An object that represents the result of an asynchronous operation. It is used to obtain the value produced by a `Promise`. + +- **Task**: A unit of work that can be scheduled and executed asynchronously. It is a higher-level abstraction that combines a `Promise` and a `Future`. + +- **Coroutine**: A special type of function that can suspend its execution and return control to the caller without losing its state. It can be resumed later, allowing for asynchronous programming. + +### `async`, `await` and similar keywords + +- **`async`**: A keyword used to define a function that returns a `Promise` or `Task`. It allows the function to pause its execution and resume later. + +- **`await`**: A keyword used to pause the execution of an `async` function until a `Promise` or `Task` is resolved. It unwraps the value of the `Promise` or `Task` and allows the function to continue. + +- **`co_return`**: A keyword used in C++ coroutines to return a value from a coroutine. It is similar to `return` but is used in coroutines to indicate that the coroutine has completed. It's similar to `return` in `async` functions in other languages that boxes the value into a `Promise` or `Task`. + +`async/await` and similar constructs provide a more readable and synchronous-like way of writing asynchronous code, it hides the type of `Promise`/`Future`/`Task` from the user and allows them to focus on the logic of the code. + +### Executing Multiple Async Operations Concurrently + +To run multiple promises concurrently, JavaScript provides `Promise.all`, `Promise.allSettled` and `Promise.any`, Python provides `asyncio.gather`, Rust provides `tokio::try_join`, C# provides `Task.WhenAll`, and C++ provides `cppcoro::when_all`. + +In some situations, you may want to get the first result of multiple async operations. JavaScript provides `Promise.race` to get the first result of multiple promises. Python provides `asyncio.wait` to get the first result of multiple coroutines. Rust provides `tokio::select!` to get the first result of multiple futures. C# provides `Task.WhenAny` to get the first result of multiple tasks. C++ provides `cppcoro::when_any` to get the first result of multiple tasks. Those functions are very simular to `select` in Go. + +### Error Handling + +`await` commonly unwraps the value of a `Promise` or `Task`, but it also propagates errors. If the `Promise` or `Task` is rejected or throws an error, the error will be thrown in the `async` function by the `await` keyword. You can use `try/catch` blocks to handle errors in `async` functions. + +## Common patterns + +- `async` keyword hides the types of `Promise`/`Future`/`Task` in the function signature in Python and Rust, but not in JavaScript, C#, and C++. +- `await` keyword unwraps the value of a `Promise`/`Future`/`Task`. +- `return` keyword boxes the value into a `Promise`/`Future`/`Task` if it's not already. + +## Design considerations in LLGo + +- Don't introduce `async`/`await` keywords to compatible with Go compiler. +- For performance reason don't implement async functions with goroutines + +## Design + +```go +func resolveAfter1Second() Promise[string] { + return Async(func (context) { + context.ScheduleAfter(1 * time.Second, func() { + context.Resolve("Resolved after 1 second") + }) + }) +} + +func asyncCall() Promise[string] { + return Async(resolveAfter1Second().Await()) +} + +func asyncCall2() Promise[string] { + return resolveAfter1Second() +} + +func asyncCall3() { + resolveAfter1Second().Then(func(result string) { + fmt.Println("AsyncCall3: " + result) + }) +} + +func asyncMain() { + fmt.Println("Starting AsyncCall") + result1 := asyncCall().Await() + fmt.Println(result1) + + fmt.Println("Starting AsyncCall2") + result2 := asyncCall2().Await() + fmt.Println(result2) + + fmt.Println("Starting AsyncCall3") + asyncCall3() + + // Wait for AsyncCall3 to complete + time.Sleep(2 * time.Second) + + fmt.Println("Main function completed") +} +``` diff --git a/x/io/_demo/asyncdemo/async.go b/x/io/_demo/asyncdemo/async.go index 6d5362ae..ce972fe7 100644 --- a/x/io/_demo/asyncdemo/async.go +++ b/x/io/_demo/asyncdemo/async.go @@ -1,23 +1,108 @@ package main import ( + "encoding/json" "fmt" "time" "github.com/goplus/llgo/x/io" ) -var GetUser = io.Async[any](func(string) any { - panic("todo: GetUser") -}) +type Response struct { + StatusCode int -var GetScore = io.Async[float64](func() float64 { + mockBody string +} + +func (r *Response) mock(body string) { + r.mockBody = body +} + +func (r *Response) Text() *io.Promise[string] { + return io.NewPromise[string](func(resolve func(string, error)) { + resolve(r.mockBody, nil) + }) +} + +func HttpGet(url string, callback func(resp *Response, err error)) { + panic("todo: Get") +} + +func AsyncHttpGet(url string) io.AsyncCall[*Response] { + return io.NewPromise[*Response](func(resolve func(*Response, error)) { + HttpGet(url, resolve) + }) +} + +type User struct { + Name string +} + +func GetUser(uid string) io.AsyncCall[User] { + return io.NewPromise[User](func(resolve func(User, error)) { + resp, err := io.Await(AsyncHttpGet("http://example.com/user/" + uid)) + if err != nil { + resolve(User{}, err) + return + } + + if resp.StatusCode != 200 { + resolve(User{}, fmt.Errorf("http status code: %d", resp.StatusCode)) + return + } + + resp.mock(`{"name":"Alice"}`) + + body, err := io.Await[string](resp.Text()) + if err != nil { + resolve(User{}, err) + return + } + user := User{} + if err := json.Unmarshal([]byte(body), &user); err != nil { + resolve(User{}, err) + return + } + + resolve(user, nil) + }) +} + +// func GetUser1(uid string) (resolve io.AsyncCall[User]) { +// resp, err := io.Await(AsyncHttpGet("http://example.com/user/" + uid)) +// if err != nil { +// resolve(User{}, err) +// return +// } + +// if resp.StatusCode != 200 { +// resolve(User{}, fmt.Errorf("http status code: %d", resp.StatusCode)) +// return +// } + +// resp.mock(`{"name":"Alice"}`) + +// body, err := io.Await[string](resp.Text()) +// if err != nil { +// resolve(User{}, err) +// return +// } +// user := User{} +// if err := json.Unmarshal([]byte(body), &user); err != nil { +// resolve(User{}, err) +// return +// } + +// resolve(user, nil) +// } + +func GetScore() *io.Promise[float64] { panic("todo: GetScore") -}) +} -var DoUpdate = io.Async[io.Void](func(op string) io.Void { +func DoUpdate(op string) *io.Promise[io.Void] { panic("todo: DoUpdate") -}) +} func main() { user, err := GetUser("123").Await() @@ -26,15 +111,15 @@ func main() { user, err = io.Race(GetUser("123"), GetUser("456"), GetUser("789")).Await() fmt.Println(user, err) - user, score, _, err := io.Await3[any, float64, io.Void](GetUser("123"), GetScore(), DoUpdate("update sth.")) + user, score, _, err := io.Await3[User, float64, io.Void](GetUser("123"), GetScore(), DoUpdate("update sth.")) fmt.Println(user, score, err) select { - case user := <-GetUser("123"): + case user := <-GetUser("123").Chan(): fmt.Println("user:", user) - case score := <-GetScore(): + case score := <-GetScore().Chan(): fmt.Println("score:", score) - case <-io.Timeout(5 * time.Second): + case <-io.Timeout(5 * time.Second).Chan(): fmt.Println("timeout") } } diff --git a/x/io/io.go b/x/io/io.go index 8748673d..7ceb650b 100644 --- a/x/io/io.go +++ b/x/io/io.go @@ -30,10 +30,13 @@ type Void = [0]byte // ----------------------------------------------------------------------------- -type AsyncCall[OutT any] chan OutT +type AsyncCall[OutT any] interface { + Await(timeout ...time.Duration) (ret OutT, err error) + Chan() <-chan OutT +} // llgo:link AsyncCall.Await llgo.await -func (AsyncCall[OutT]) Await(timeout ...time.Duration) (ret OutT, err error) { +func Await[OutT any](call AsyncCall[OutT], timeout ...time.Duration) (ret OutT, err error) { return } @@ -61,7 +64,27 @@ func Await3[OutT1, OutT2, OutT3 any]( // ----------------------------------------------------------------------------- -type Promise[OutT any] func(...any) AsyncCall[OutT] +type Promise[OutT any] struct { +} + +func NewPromise[OutT any](fn func(resolve func(OutT, error))) (ret *Promise[OutT]) { + ret = &Promise[OutT]{} + return +} + +func NewPromiseFromValue[OutT any](value OutT) (ret *Promise[OutT]) { + return NewPromise[OutT](func(resolve func(OutT, error)) { + resolve(value, nil) + }) +} + +func (p *Promise[OutT]) Await(timeout ...time.Duration) (ret OutT, err error) { + return +} + +func (p *Promise[OutT]) Chan() <-chan OutT { + return nil +} // llgo:link Async llgo.async func Async[OutT any](fn any) (ret Promise[OutT]) {