MoonBit每周工作动态——0617

MoonBit 更新

  • 支持了错误处理机制
  1. 函数返回值类型可以用 Int!String 来标识这个函数正常情况下返回 Int ,错误情况下会抛出类型为 String 的错误值,比如

fn div(x: Int, y: Int) -> Int!String { .. }

  1. raise 关键字用于中断当前控制流,直接抛出错误,比如

fn div(x: Int, y: Int) -> Int!String {

  if y == 0 { raise "divide by 0" }

  x / y

}

  1. try { expr0 } catch { pattern1 => expr1; pattern2 => expr2; .. } 表达式可以用于捕获 expr0 中抛出的错误,并对其进行模式匹配来处理,比如下面这个函数调用上面的 div 函数,并在 div 函数抛出错误的时候将错误信息打印,并返回默认值

fn div_with_default(x: Int, y: Int, default: Int) -> Int {

  try {

    div(x, y)!

  } catch {

    s => { println(s); default }

  }

}

  1. 此外,可以用后缀运算符 !!! 进行错误处理,这些后缀运算符只能应用于函数调用,其中:

f(x)! 将调用 f 的过程中发生的错误立即重新抛出。

f(x)!! 则会在 f 发生错误的情况下直接 panic,其等价于


try { f(x)! } catch { _ => panic() }

函数调用的形式包括方法调用,中缀运算符和管道运算符的调用,比如


fn init {

  let _ = x.f()!!

  let _ = (x + y)!!

  let _ = (x |> f)!!

}

  1. 最后,对可能会抛出错误的函数如果没有使用上述任何错误处理,那么则会报 unhandled error 的错误
  • 支持 Map 字面量语法:

fn init {

  // 键必须是字面量

  let m1 : Map[String, Int] = { "x": 1, "y": 2 }

  let m2 : Map[Int, String] = { 1: "x", 2: "y" }

}

IDE更新

  • 修复了 IDE 在补全过程中 builtin package 中的方法会重复出现两次的 bug

  • 修复了 IDE 中缺少 Byte 相关的补全功能

构建系统更新

  • 添加对 internal 包的支持,这些包被放在名为 internal 的目录中。internal 包只能被以 internal 的父目录为根的包导入。

例如,如果有一个包的路径为 username/hello/x/internal/a,该 internal 包的父目录为 username/hello/x,那么只有包username/hello/x 或其子包(例如 username/hello/x/a)能够导入username/hello/x/internal/a,而username/hello/y则不能导入该包。

3 个赞

这种错误处理比用Result[T,E]好在哪?
我尝试了新的错误处理方式,发现如果要抛出错误,调用链上每个函数签名都要加上错误类型,标准库很多接口的回调函数参数都不支持抛出错误,例如iter,map等。
另外(js环境)!!Result::unwrap报错没有打印错误值,开发调试不方便。

1 个赞

能给出具体的例子和原来Result对比吗?

实际上我期待有比Result更好的方案,目前两者在遍历容器上都有些繁琐,可能等标准库完善后表现会更好?
这是一个转换Map[String,String]Map[String,Int]的对比例子,类似代码在使用JSON时可能遇到:

fn parse_int_map1(m : Map[String, String]) -> Result[Map[String, Int], String] {
  m.as_iter().fold(
    fn(acc, kv) {
      acc.bind(
        fn(m2) {
          @strconv.parse_int(kv.1).map(
            fn(nv) {
              m2.set(kv.0, nv)
              m2
            },
          )
        },
      )
    },
    Ok(Map::new()),
  )
}

fn parse_int_map2(m : Map[String, String]) -> Map[String, Int]!String {
  let mut err : String? = None
  let m2 = Map::new()
  m.iter(
    fn(k, v) {
      if err.is_empty().not() {
        return
      }
      try {
        m2.set(k, parse_int(v)!)
      } catch {
        s => err = Some(s)
      }
    },
  )
  match err {
    Some(s) => {
      raise s
    }
    None => m2
  }
}

fn parse_int(s : String) -> Int!String {
  match @strconv.parse_int(s) {
    Ok(v) => v
    Err(e) => {
      raise e
    }
  }
}

我补充一点我在 js/ts 里用 try/catch 的烦恼,那就是 try 后代码块限定了变量作用域,供参考。

烦恼1. 不得不在try代码块之前先声明一个可变变量,这让我感觉很无聊,也没有必要。特别是在一种情况,就是只需要一个局部变量获取函数返回值,没有出错的话,后面一直使用该值,不会去改变该局部变量的值。

烦恼2. 当在一个函数(A)重调用多个可能存在返回Err的其它函数时,好的期待时,A函数内部不需要使用try/catch,而是在调用A的调用方处理,或者在最终业务流程中的才使用try/catch处理,但我觉得就是一句废话,光打日志或者埋点的需求,我就不得不在A函数内部使用try/catch 处理出错。

以上2点经历多了,try/catch 一点好感都没。错误处理,不严重的直接层层上报提示用户或者漠视;严重的,直接记录日志,然后程序挂掉,是最好的。

MoonBit的try {…} catch {…}是个表达式。与其他语言的三元运算符类似,可以避免使用可变变量:

fn request_resource(name : String) -> Int!String {
  match name {
    "res1" => 90
    _ => raise "error"
  }
}

let fallback = 1

fn main {
  let r1 = try { request_resource("res1")! } catch { _ => fallback }
  let r2 = try { request_resource("res2")! } catch { _ => fallback }
  println(r1) // 90
  println(r2) // 1
}