异步编程支持#

MoonBit 采用类似于 Kotlin 的基于协程的异步编程方法。编译器支持和具体语法是稳定的,而异步库仍在开发中,并被视为实验性。

异步函数#

异步函数用 async 关键字,以及显式的 raisenoraise

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 continuation shift 操作符

以下是使用这两个原语的示例:

///|
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 函数用作 Promiseresolveresume_err 用作 Promise 的 reject 回调即可。