Skip to content

Commit

Permalink
add document for experimental async programming support
Browse files Browse the repository at this point in the history
  • Loading branch information
Guest0x0 committed Jan 10, 2025
1 parent 72ece7e commit 63e1cba
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/next-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ jobs:
for dir in next/sources/*; do
if [ -d "$dir" ]; then
echo "Processing $dir"
if [ "$dir" = next/sources/async && ${{ matrix.backend }} != "js" ]; then
continue
fi
if ! (cd "$dir" && moon install && moon check --deny-warn --target ${{ matrix.backend }} && moon test --target ${{ matrix.backend }}); then
echo "Failed in $dir"
failed_directories+=("$dir")
Expand Down Expand Up @@ -77,6 +80,9 @@ jobs:
$failed_directories = @()
Get-ChildItem -Path ".\next\sources" -Directory | ForEach-Object {
Write-Output "Processing $($_.FullName)"
if ($_.Name -eq "async" && ${{ matrix.backend }} -ne "js") {
return
}
Set-Location $_.FullName
moon install && moon check --deny-warn --target ${{ matrix.backend }} && moon test --target ${{ matrix.backend }}
if (!$?) {
Expand Down
80 changes: 80 additions & 0 deletions next/language/async-experimental.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Experimental async programming support

MoonBit is providing experimental support for async programming.
But the design and API is still highly unstable, and may receive big breaking change in the future.
This page documents the current design, and we highly appreciate any feedback or experiment with current design.

## Async function
Async functions can be declared with the `async` keyword:

```{literalinclude} /sources/language/src/async/async.mbt
:language: moonbit
:start-after: start async function declaration
:end-before: end async function declaration
```

Async functions must be called with the `!!` operator:

```{literalinclude} /sources/language/src/async/async.mbt
:language: moonbit
:start-after: start async function call syntax
:end-before: end async function syntax
```

If the async function may throw error, `!!` will also rethrow the error.

## Async primitives for suspension
MoonBit provides two core primitives for `%async.suspend` and `%async.run`:

```{literalinclude} /sources/async/src/async.mbt
:language: moonbit
:start-after: start async primitive
:end-before: end async primitive
```

There two primitives are not intended for direct use by end users.
However, since MoonBit's standard library for async programming is still under development,
currently users need to bind these two primitives manually to do async programming.

There are two ways of reading these primitives:

- the coroutine reading: `%async.run` spawn a new corourtine,
and `%async.suspend` suspend current coroutine.
The main difference with other languages here is:
instead of yielding all the way to the caller of `%async.run`,
resumption of the coroutine is handled by the callback passed to `%async.suspend`
- the delimited continuation reading: `%async.run` is the `reset` operator in delimited continuation,
and `%async.suspend` is the `shift` operator in delimited continuation

Here's an example of how these two primitives work:

```{literalinclude} /sources/async/src/async.mbt
:language: moonbit
:start-after: start async example
:end-before: end async example
```

In `async_worker`, `suspend` will capture the rest of the current coroutine as two "continuation" functions, and pass them to a callback.
In the callback, calling `resume_ok` will resume execution at the point of `suspend!!(...)`,
all the way until the `run_async` call that start this coroutine.
calling `resume_err` will also resume execution of current coroutine,
but it will make `suspend!!(...)` throw an error instead of returning normally.

Notice that `suspend` type may throw error, even if `suspend` itself never throw an error directly.
This design makes coroutines cancellable at every `suspend` call: just call the corresponding `resume_err` callback.

## Integrating with JS Promise/callback based API
Since MoonBit's standard async library is still under development,
so there is no ready-to-use implementation for event loop and IO operations yet.
So the easiest way to write some async program is to use MoonBit's Javascript backend,
and reuse the event loop and IO operation sof Javascript.
Here's an example of integrating MoonBit's async programming support wtih JS's callback based API:

Check warning on line 71 in next/language/async-experimental.md

View workflow job for this annotation

GitHub Actions / typo-check

"wtih" should be "with".

```{literalinclude} /sources/async/src/async.mbt
:language: moonbit
:start-after: start async timer example
:end-before: end async timer example
```

Integrating with JS Promise is easy too:
just pass `resume_ok` as the `resolve` callback and `resume_err` as the `reject` callback to a JS promise.
1 change: 1 addition & 0 deletions next/language/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ tests
docs
ffi-and-wasm-host
derive
async-experimental
```
10 changes: 10 additions & 0 deletions next/sources/async/moon.mod.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "moonbit-community/async-doc",
"version": "0.1.0",
"readme": "README.md",
"repository": "",
"license": "Apache-2.0",
"keywords": [],
"description": "",
"source": "src"
}
120 changes: 120 additions & 0 deletions next/sources/async/src/async.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// start async function declaration
async fn my_async_function() -> Unit {
...
}

// anonymous/local function
test {
let async_lambda = async fn () {
...
}
async fn local_async_function() {
...
}
}
// end async function declaration

// start async function call syntax
async fn my_async_function() -> Unit! {
...
}

fn another_async_function() -> Unit! {
// error will be rethrowed by `!!`
my_async_function!!()
}
// end async function call syntax

// start async primitive

// `run_async` spawn a new coroutine and execute an async function in it
fn run_async(f : async () -> Unit) -> Unit = "%async.run"

// `suspend` will suspend the execution of the current coroutine.
// The suspension will be handled by a callback passed to `suspend`
async fn suspend[T, E : Error](
// `f` is a callback for handling suspension
f : (
// the first parameter of `f` is used to resume the execution of the coroutine normally
(T) -> Unit,
// the second parameter of `f` is used to cancel the execution of the current coroutine
// by throwing an error at suspension point
(E) -> Unit
) -> Unit
) -> T!E
// end async primitive

// start async example
type! MyError derive(Show)

async fn async_worker(throw_error~ : Bool) -> Unit!MyError {
suspend!!(fn (resume_ok, resume_err) {
if throw_error {
resume_err(MyError)
} else {
resume_ok(())
println("the end of the coroutine")
}
})
}

// the program above should print:
//
// the worker finishes
// the end of the coroutine
// after the first coroutine finishes
// catched MyError

Check warning on line 66 in next/sources/async/src/async.mbt

View workflow job for this annotation

GitHub Actions / typo-check

"catched" should be "caught".
test {
// when supplying an anonymous function
// to a higher order function that expects async parameter,
// the `async` keyword can be omitted
run_async(fn () {
try {
async_worker!!(throw_error=false)
println("the worker finishes")
} catch {
err => println("catched: \{err}")

Check warning on line 76 in next/sources/async/src/async.mbt

View workflow job for this annotation

GitHub Actions / typo-check

"catched" should be "caught".
}
})
println("after the first coroutine finishes")
run_async(fn () {
try {
async_worker!!(throw_error=true)
println("this message should be printed after the worker finishes")
} catch {
err => println("catched: \{err}")

Check warning on line 85 in next/sources/async/src/async.mbt

View workflow job for this annotation

GitHub Actions / typo-check

"catched" should be "caught".
}
})
}
// end async example

// start async timer example
type JSTimer
extern "js" fn js_set_timeout(f : () -> Unit, duration : Int) -> JSTimer =
#| (f, duration) => setTimeout(f, duration)

async fn sleep(duration : Int) -> Unit! {
suspend!!(fn (resume_ok, _resume_err) {
let _ = setTimeout(resume_ok, duration)
})
}

test {
run_async(fn () {
sleep!!(500)
println("timer 1 tick")
sleep!!(1000)
println("timer 1 tick")
sleep!!(1500)
println("timer 1 tick")
})
run_async(fn () {
sleep!!(600)
println("timer 2 tick")
sleep!!(600)
println("timer 2 tick")
sleep!!(600)
println("timer 2 tick")
})
}
// end async timer example
3 changes: 3 additions & 0 deletions next/sources/async/src/moon.pkg.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"warn-list": "-6"
}
91 changes: 91 additions & 0 deletions next/sources/language/src/async/async.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// start async function declaration
async fn my_async_function() -> Unit {
...
}

// anonymous/local function
fn main {
let async_lambda = async fn () {
...
}
async fn local_async_function() {
...
}
}
// end async function declaration

// start async function call syntax
async fn my_async_function() -> Unit! {
...
}

fn another_async_function() -> Unit! {
// error will be rethrowed by `!!`
my_async_function!!()
}
// end async function call syntax

// start async primitive

// `run_async` spawn a new coroutine and execute an async function in it
fn run_async(f : async () -> Unit) -> Unit = "%async.run"

// `suspend` will suspend the execution of the current coroutine.
// The suspension will be handled by a callback passed to `suspend`
async fn suspend[T, E : Error](
// `f` is a callback for handling suspension
f : (
// the first parameter of `f` is used to resume the execution of the coroutine normally
(T) -> Unit,
// the second parameter of `f` is used to cancel the execution of the current coroutine
// by throwing an error at suspension point
(E) -> Unit
) -> Unit
) -> T!E
// end async primitive

// start async example
fn run_async(f : async () -> Unit) -> Unit = "%async.run"
async fn suspend[T, E : Error](
f : ((T) -> Unit, (E) -> Unit) -> Unit
) -> T!E

type! MyError derive(Show)

async fn async_worker(throw_error~ : Bool) -> Unit!MyError {
suspend!!(fn (resume_ok, resume_err) {
if throw_error {
resume_err(MyError)
} else {
resume_ok(())
println("the end of the coroutine")
}
})
}

// the program above should print:
//
// the worker finishes
// the end of the coroutine
// after the first coroutine finishes
// catched MyError

Check warning on line 71 in next/sources/language/src/async/async.mbt

View workflow job for this annotation

GitHub Actions / typo-check

"catched" should be "caught".
fn main {
run_async(fn () {
try {
async_worker!!(throw_error=false)
println("the worker finishes")
} catch {
err => println("catched: \{err}")

Check warning on line 78 in next/sources/language/src/async/async.mbt

View workflow job for this annotation

GitHub Actions / typo-check

"catched" should be "caught".
}
})
println("after the first coroutine finishes")
run_async(fn () {
try {
async_worker!!(throw_error=true)
println("this message should be printed after the worker finishes")
} catch {
err => println("catched: \{err}")

Check warning on line 87 in next/sources/language/src/async/async.mbt

View workflow job for this annotation

GitHub Actions / typo-check

"catched" should be "caught".
}
})
}
// end async example

0 comments on commit 63e1cba

Please sign in to comment.