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:
async fn my_async_function() -> Unit {
...
}
// anonymous/local function
test {
let async_lambda = async fn () {
...
}
async fn local_async_function() {
...
}
}
Async functions must be called with the !!
operator:
async fn some_async_function() -> Unit! {
...
}
async fn another_async_function() -> Unit! {
// error will be rethrowed by `!!`
some_async_function!!()
}
If the async function may throw error, !!
will also rethrow the error.
Async functions can only be called in async functions. Currently, async functions cannot be called in the body of for .. in
loops.
Async primitives for suspension#
MoonBit provides two core primitives for %async.suspend
and %async.run
:
// `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 = "%async.suspend"
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 coroutine, 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 thereset
operator in delimited continuation, and%async.suspend
is theshift
operator in delimited continuation
Here's an example of how these two primitives work:
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 below should print:
//
// the worker finishes
// the end of the coroutine
// after the first coroutine finishes
// caught MyError
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("caught: \{err}")
}
})
println("after the first coroutine finishes")
run_async(fn () {
try {
async_worker!!(throw_error=true)
println("the worker finishes")
} catch {
err => println("caught: \{err}")
}
})
}
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 operations of Javascript. Here's an example of integrating MoonBit's async programming support with JS's callback based API:
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 _ = js_set_timeout(fn () { resume_ok(()) }, duration)
})
}
test {
run_async(fn () {
try {
sleep!!(500)
println("timer 1 tick")
sleep!!(1000)
println("timer 1 tick")
sleep!!(1500)
println("timer 1 tick")
} catch { _ => panic() }
})
run_async(fn () {
try {
sleep!!(600)
println("timer 2 tick")
sleep!!(600)
println("timer 2 tick")
sleep!!(600)
println("timer 2 tick")
} catch { _ => panic() }
})
}
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.