MoonBit
MoonBit 是一个用于云计算和边缘计算的 WebAssembly 端到端的编程语言工具链。 您可以访问 https://try.moonbitlang.cn 获得 IDE 环境,无需安装任何软件,也不依赖任何服务器。
状态
MoonBit 目前处于 Beta-Preview 阶段。我们期望能在 2024/11/22 达到 Beta 阶段,2025年内达到 1.0 阶段。
主要优势
- 生成比现有解决方案明显更小的 WASM 文件。
- 更高的运行时性能。
- 先进的编译时性能。
- 简单且实用的数据导向语言设计。
概述
一个月兔程序由类型定义,函数定义和变量绑定组成。
程序入口
有一个特殊的函数: init
函数,它有以下两个特点:
- 同一个包中可以有多个
init
函数。 init
函数不能被显式调用或被其他函数引用。相反,在一个包初始化时,所有的init
函数都将被隐式地调用。因此,init
函数中只能包含语句。
fn init {
let x = 1
// x // 失败
println(x) // 成功
}
对于 WebAssembly 后端而言,这意味着它将会在实例准备好之前被执行,也就是说,如果有 FFI 依赖实例的导出,那么将不能正常运行;对于 JavaScript 后端而言,这意味着它将会在被导入的时候执行。
另一个特殊的函数是main
函数。main
函数是程序的入口,它将会在初始化阶段之后被执行。只有是main
的包中才能定义main
函数。查看构建系统教程了解更多。
上述两个函数均需省略参数列表与返回值定义。
表达式和语句
MoonBit 区分语句和表达式。在一个函数体中,只有最后一句才能写成作为返回值的表达式。例如:
fn foo() -> Int {
let x = 1
x + 1 // OK
}
fn bar() -> Int {
let x = 1
x + 1 // 失败
x + 2
}
表达式包括:
- 值字面量(例如布尔值、数字、字符、字符串、数组、元组、结构体)
- 算术、逻辑和比较运算
- 访问数组元素(例如
a[0]
)、结构体字段(例如r.x
)或元组的元素(例如t._
) - 变量和(大写字母开头的)枚举构造器
- 匿名局部函数定义
match
和if
表达式
语句包括:
- 命名局部函数定义
- 局部变量绑定
- 赋值
return
语句- 返回类型为
Unit
的任何表达式
函数
函数接受参数并产生结果。
在 MoonBit 中,函数是一等公民,这意味着函数可以作为其他函数的参数或返回值。与下述的 enum
的类型构造子相对,MoonBit 的命名规则要求函数名不能以大写字母 (A-Z) 开头。
顶层函数
函数可以被定义为顶层或局部。
我们可以使用 fn
关键字定义一个顶层函数,
例如以下函数求三个整数之和并返回结果:
fn add3(x: Int, y: Int, z: Int)-> Int {
x + y + z
}
注意,顶层函数的参数和返回值类型需要显式标注。
局部函数
局部函数使用 fn
关键字定义。局部函数可以是命名的或匿名的。在大多数情况下,局部函数的类型注解可以省略,因为编译器可以自动推断。例如:
fn foo() -> Int {
fn inc(x) { x + 1 } // 命名为 `inc`
fn (x) { x + inc(2) } (6) // 匿名,立即应用到整数字面量 6
}
fn main {
println(foo())
}
无论是命名的还是匿名的,函数都是 词法闭包:任何没有局部绑定的标识符, 必须引用来自周围词法作用域的绑定:
let y = 3
fn foo(x: Int) -> Unit {
fn inc() { x + 1 } // OK,返回 x + 1
fn four() { y + 1 } // Ok,返回 4
println(inc())
println(four())
}
fn main {
foo(2)
}
函数调用
函数可通过向圆括号内传入参数列表进行调用:
add3(1, 2, 7)
这适用于命名函数(如前面的例子)和绑定到函数值的变量,如下所示:
fn main {
let add3 = fn(x, y, z) { x + y + z }
println(add3(1, 2, 7))
}
表达式 add3(1, 2, 7)
返回 10
。任何求值为函数值的表达式都可以被调用:
fn main {
let f = fn (x) { x + 1 }
let g = fn (x) { x + 2 }
println((if true { f } else { g })(3)) // OK
}
带标签的参数
可以用 label~ : Type
的语法为函数声明带标签的参数。函数体内参数的名字也是 label
:
fn labelled(arg1~ : Int, arg2~ : Int) -> Int {
arg1 + arg2
}
调用函数时,可以用 label=arg
的语法提供带标签的参数。label=label
可以简写成 label~
:
fn init {
let arg1 = 1
println(labelled(arg2=2, arg1~)) // 3
}
可以用任意的顺序提供带标签的参数。参数的求值顺序与函数声明中参数的顺序相同。
可选的参数
可选的参数是带有默认值的带标签参数。声明可选的参数的语法是 label~ : Type = default_expr
。调用函数时,如果没有提供这个参数,就会使用默认值作为参数:
fn optional(opt~ : Int = 42) -> Int {
opt
}
fn main {
println(optional()) // 42
println(optional(opt=0)) // 0
}
每次使用默认参数调用一个函数时,都会重新求值默认值的表达式,也会被重新触发其中的副作用。例如:
fn incr(counter~ : Ref[Int] = { val: 0 }) -> Ref[Int] {
counter.val = counter.val + 1
counter
}
fn main {
println(incr()) // 1
println(incr()) // 依然是 1,因为重新求值了默认表达式,产生了一个新的 Ref
let counter : Ref[Int] = { val: 0 }
println(incr(counter~)) // 1
println(incr(counter~)) // 2,因为两次调用使用了同一个 Ref
}
如果想要在多次不同的函数调用之间共享默认值,可以提前用 let
计算并保存默认值:
let default_counter : Ref[Int] = { val: 0 }
fn incr(counter~ : Ref[Int] = default_counter) -> Int {
counter.val = counter.val + 1
counter.val
}
fn main {
println(incr()) // 1
println(incr()) // 2
}
默认值可以依赖于前面的参数,例如:
fn sub_array[X](xs : Array[X], offset~ : Int, len~ : Int = xs.length() - offset) -> Array[X] {
... // 生成 xs 的一个从 offset 开始、长度为 len 的子数组
}
fn main {
println(sub_array([1, 2, 3], offset=1)) // [2, 3]
println(sub_array([1, 2, 3], offset=1, len=1)) // [2]
}
在提供可选参数时让编译器自动插入 Some
许多可选参数的类型是 T?
,默认值是 None
。显式提供这种参数时,需要裹一层构造器 Some
:
fn image(width~ : Int? = None, height~ : Int? = None) -> Image { ... }
fn main {
let img = image(width=Some(1920), height=Some(1080)) // 丑!
...
}
MoonBit 提供了一种特殊的可选参数来解决这个问题。可以用 label? : T
来声明一个可选参数,这个可选参数的类型是 T?
,默认值是 None
。调用者显式提供这一参数时,MoonBit 会自动在参数上插入一层 Some
:
fn image(width? : Int, height? : Int) -> Image { ... }
fn main {
let img = image(width=1920, height=1080) // 好多了!
...
}
不过,有时依然需要直接直接传递一个类型为 T?
的值,例如在转发一个可选参数时。为此,MoonBit 提供了一个语法 label?=value
,表示直接把类型为 T?
的值 value
传递给参数 label
。此外,label?=label
可以简写成 label?
:
fn image(width? : Int, height? : Int) -> Image { ... }
fn fixed_width_image(height? : Int) -> Image {
image(width=1920, height?)
}
自动填充的参数
MoonBit 能够自动在每次函数调用时填充某些特定类型的参数,例如函数调用在源码中的位置。要声明这种自动填充的参数,只需要使用 _
作为参数的默认值即可。如果在调用时没有提供这个参数,MoonBit 就会自动根据调用处的上下文填充 这个参数。
目前 MoonBit 支持两种类型的自动填充参数。代表整个函数调用在源码中位置的 SourceLoc
类型,以及包含每个参数各自的位置的 ArgsLoc
类型:
fn f(_x : Int, _y : Int, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit {
println("整个函数调用的位置:\{loc}")
println("各个参数的位置:\{args_loc}")
}
fn main {
f(1, 2)
// 整个函数调用的位置:<文件名>:7:3-7:10
// 各个参数的位置:[Some(<文件名>:7:5-7:6), Some(<文件名>:7:8-7:9), None, None]
}
自动填充的参数可以用于编写调试和测试用的工具函数。