异步编程支持#
MoonBit 采用类似于 Kotlin 的基于协程的异步编程方法。编译器支持和具体语法是稳定的,而异步库仍在开发中,并被视为实验性。
异步函数#
异步函数用 async
关键字,以及显式的 raise
或 noraise
:
async fn my_async_function() -> Unit noraise {
...
}
// 匿名/本地函数
test {
let async_lambda = async fn() noraise { ... }
async fn local_async_function() noraise {
...
}
}
由于 MoonBit 是一门静态类型语言,编译器会跟踪异步函数的调用,因此你可以像调用普通函数一样调用异步函数。MoonBit IDE 会用不同的样式来高亮显示异步函数的调用。
///|
async fn some_async_function() -> Unit raise {
...
}
///|
async fn another_async_function() -> Unit raise {
some_async_function() // 使用斜体渲染
}
异步函数只能在异步函数中调用。
警告
目前,异步函数还不支持在 for .. in
循环体中使用,这将在未来的版本中解决。
用于中断异步函数的原语#
MoonBit 提供了两个用于异步编程的原语:%async.suspend
和 %async.run
:
/// `run_async` 会创建一个新的协程,并在其中运行一个异步函数
fn run_async(f : async () -> Unit noraise) -> Unit = "%async.run"
/// `suspend` 会中断当前协程的运行。
/// `suspend` 会接受一个回调函数,并让这个回调函数来操作中断的协程
async fn[T, E : Error] suspend(
// `f` 是负责操作中断的协程的回调函数
f : (
// `f` 的第一个参数用于继续运行被中断的协程
(T) -> Unit,
// `f` 的第二个参数用于取消被中断的协程。
// 取消会被表示为在中断处抛出错误
(E) -> Unit,
) -> Unit
) -> T raise E = "%async.suspend"
这两个原语不应该让终端用户直接调用。但由于 MoonBit 的异步标准库仍在开发中,目前,用户需要手动绑定这两个原语,才能编写异步程序。
可以用两种不同的方式来理解这两个原语:
从协程的角度理解:
%async.run
创建一个新的协程,%async.suspend
暂停当前协程。和其他语言的协程的主要区别是:协程暂停后,不是由调用%async.run
的地方来负责恢复执行,而是通过传递给%async.suspend
的回调函数来处理协程的恢复。理解为 delimited continuation:
%async.run
是 delimited continuation 中的reset
操作符,%async.suspend
是 delimited continuationshift
操作符
以下是使用这两个原语的示例:
///|
suberror MyError derive(Show)
///|
async fn async_worker(
logger~ : &Logger,
throw_error~ : Bool,
) -> Unit raise MyError {
suspend(fn(resume_ok, resume_err) {
if throw_error {
resume_err(MyError)
} else {
resume_ok(())
logger.write_string("the end of the coroutine\n")
}
})
}
///|
test {
// 当提供匿名函数时
// 给一个期望 async 参数的高阶函数,
// 可以省略 `async` 关键字
let logger = StringBuilder::new()
fn local_test() {
run_async(() => try {
async_worker(logger~, throw_error=false)
logger.write_string("the worker finishes\n")
} catch {
err => logger.write_string("caught: \{err}\n")
})
logger.write_string("after the first coroutine finishes\n")
run_async(() => try {
async_worker(logger~, throw_error=true)
logger.write_string("the worker finishes\n")
} catch {
err => logger.write_string("caught: \{err}\n")
})
}
local_test()
inspect(
logger,
content=(
#|the worker finishes
#|the end of the coroutine
#|after the first coroutine finishes
#|caught: MyError
#|
),
)
}
在 async_worker
里,suspend
会捕获当前协程剩下的部分,并将它们表示成两个函数,传递给 suspend
的参数。在 suspend
的参数里,调用 resume_ok
会让 suspend(...)
正常返回,恢复协程的运行,一直运行到创建这个协程的 run_async(...)
为止。调用 resume_err
也会恢复协程的运行,但它会在 suspend(...)
的位置抛出一个错误。
suspend
的类型表明它可能抛出错误。但 suspend
自身不会直接产生任何错误。这一设计保证了协程在每一个的中断点都是可以取消的:调用对应的 resume_err
函数即可。
和 JS 的 Promise/回调 API 整合#
MoonBit 的异步标准库仍在开发中,因此,目前没有直接可用的事件循环和输入输出原语实现。目前,要使用 MoonBit 编写异步程序最简单的办法是使用 JS 后端,并复用 JavaScript 的事件循环和输入输出 API。下面是一个整合 MoonBit 的异步编程支持和 JS 的回调 API 的例子:
#external
type JSTimer
///|
extern "js" fn js_set_timeout(f : () -> Unit, duration~ : Int) -> JSTimer =
#| (f, duration) => setTimeout(f, duration)
///|
async fn sleep(duration : Int) -> Unit raise {
suspend(fn(resume_ok, _resume_err) {
js_set_timeout(duration~, fn() { resume_ok(()) }) |> ignore
})
}
///|
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()
}
})
}
和 JS Promise 也非常简单:只需要把 resume_ok
函数用作 Promise
的 resolve
把 resume_err
用作 Promise 的 reject
回调即可。