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.0
) - 变量和(大写字母开头的)枚举构造器
- 匿名局部函数定义
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]
}
自动填充的参数
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]
}
自动填充的参数可以用于编写调试和测试用的工具函数。
控制结构
条件表达式
条件表达式由条件、结果和一个可选的 else
子句组成。
if x == y {
expr1
} else {
expr2
}
if x == y {
expr1
}
else
子句也可以包含另一个 if-else
表达式:
if x == y {
expr1
} else if z == k {
expr2
}
花括号用于在结果或 else
子句中组合表达式。
注意,在 MoonBit 中,条件表达式总是返回一个值,其结果和 else
子句的返回值类型必须相同。
一个配合条件表达式使用let
绑定的例子:
let initial = if size < 1 { 1 } else { size }
While 循环
MoonBit 中支持while
循环。while
后的循环条件会在循环体之前执行,当循环条件为真时, 执行循环体:
let mut i = 5
while i > 0 {
println(i)
i = i - 1
}
循环体内支持break
和continue
。使用break
能够跳出当前循环;使用continue
跳过本次循环的剩余部分,提前进入下一次循环。
fn main {
let mut i = 5
while i > 0 {
i = i - 1
if i == 4 { continue }
if i == 1 { break }
println(i)
}
}
while
循环也支持可选的else
子句。当循环条件转变为假时,将会执行else
子句,然后循环结束。
fn main {
let mut i = 2
while i > 0 {
println(i)
i = i - 1
} else {
println(i)
}
}
当存在 else
子句时,while
循环也可以返回一个值,返回值是 else
子句语句块的求值结果。此时如果使用break
跳出循环,需要在break
后提供一个返回值,类型与else
子句的返回值类型一致:
let mut i = 10
let r1 =
while i > 0 {
i = i - 1
if i % 2 == 0 { break 5 } // 跳出循环并返回 5
} else {
7
}
println(r1) //output: 5
let mut i = 10
let r2 =
while i > 0 {
i = i - 1
} else {
7
}
println(r2) //output: 7
For 循环
MoonBit 也支持 C 风格的 For 循环。关键字for
后依次跟随以分号间隔的变量初始化子句、循环条件和更新子句。三者不需要使用圆括号包裹。
例如下面的代码创建了一个新的变量绑定i
, 它的作用域在整个循环中,且是不可变的。这更利于编写清晰的代码和推理:
for i = 0; i < 5; i = i + 1 {
println(i)
}
// output:
// 0
// 1
// 2
变量初始化子句中可以创建多个绑定:
for i = 0, j = 0; i + j < 100; i = i + 1, j = j + 1 {
println(i)
}
需要注意的是在更新子句中,对于多个绑定变量具有同时更新的语义。也就是说上面的例子中,更新子句并不是顺序执行i = i + 1
、j = j + 1
,而是同时令i
、j
自增。因此,在更新子句中读取绑定变量得到的值永远是上一次循环更新后的值。
变量初始化子句、循环条件和更新子句都是可选的。例如下面两个无限循环:
for i=1;; i=i+1 {
println(i) // loop forever!
}
for {
println("loop forever!")
}
for
循环同样支持continue
、break
和else
子句。和while
循环一样,for
循环同样
支持使用break
和else
子句使for
语句返回一个值。
使用continue
语句将跳过for
本次循环的剩余部分(包括更新子句)提前进入下次循环。continue
语句
也支持同时更新for
循环的绑定变量,只要在continue
后面跟随和绑定变量数量一致的表达式,多个表达式使用逗号分隔。
例如,下面的程序计算数字 1 到 6 中的偶数的和:
fn main {
let sum =
for i = 1, acc = 0; i <= 6; i = i + 1 {
if i % 2 == 0 {
println("even: \{i}")
continue i + 1, acc + i
}
} else {
acc
}
println(sum)
}
输出:
even: 2
even: 4
even: 6
12
for .. in
循环
MoonBit 使用 for .. in
循环语法来遍历各种数据结构和序列:
for x in [ 1, 2, 3 ] {
println(x)
}
for .. in
循环会被翻译成 MoonBit 标准库中的迭代器类型 Iter
。只要一个类型有方法 .iter() : Iter[T]
,就可以使 用 for .. in
循环来遍历其中的元素。如果想了解迭代器的更多信息,可以阅读本文档的 迭代器 一节。
除了单个元素的序列,MoonBit 还能使用标准库中的 Iter2
类型来遍历有两个元素的序列,例如字典 Map
。
如果一个类型有方法 .iter2() : Iter2[A, B]
,就可以使用有两个循环变量的 for .. in
循环来遍历它:
for k, v in { "x": 1, "y": 2, "z": 3 } {
println("\{k} => \{v}")
}
下面是另一个有两个循环变量的 for .. in
的例子,在遍历一个数组的同时追踪这是数组中的第几个元素:
for index, elem in [ 4, 5, 6 ] {
let i = index + 1
println("数组的第 \{i} 个元素是 \{elem}")
}
for .. in
的循环体中可以使用 return
,break
和错误处理等控制流操作:
test "map test" {
let map = { "x": 1, "y": 2, "z": 3 }
for k, v in map {
assert_eq!(map[k], Some(v))
}
}
最后,如果循环变量没有被使用到,可以用 _
来忽略它。
函数式循环
函数式循环是 MoonBit 中一个强大的特性,它能让您以函数式风格编写循环。
函数式循环接受参数并返回一个值。它使用 loop
关键字定义,后跟其参数和循环体。
循环体是一系列子句,每个子句由模式和表达式组成。
与输入匹配的模式会被执行,并且循环将返回表达式的值。如果没有匹配的模式,循环将抛出异常。
可以使用 continue
关键字和参数开始下一次循环迭代,使用 break
关键字和参数来从循环中返回一个值。
如果值是循环体中的最后一个表达式,则可以省略 break
关键字。
fn sum(xs: @immut/list.T[Int]) -> Int {
loop xs, 0 {
Nil, acc => break acc // break 可以省略
Cons(x, rest), acc => continue rest, x + acc
}
}
fn main {
println(sum(Cons(1, Cons(2, Cons(3, Nil)))))
}
卫语句
卫语句用于检查指定的不变量。如果不变量的条件满足,程序继续执行后续的语句并返回。
如果条件不满足(即为假),则执行 else
块中的代码并返回它的求值结果(后续的语句会被跳过)。
guard index >= 0 && index < len else {
abort("Index out of range")
}
guard
语句也支持模式匹配:下面的例子中getProcessedText
假设输入的path
指向的都是纯文本的资源,
它使用卫语句保证这一不变量。相比于直接使用match
语句,后续对text
的处理过程可以少一层缩进。
enum Resource {
Folder(Array[String])
PlainText(String)
JsonConfig(Json)
}
fn getProcessedText(resources : Map[String, Resource], path : String) -> String!Error {
guard let Some(PlainText(text)) = resources[path] else {
None => fail!("\{path} not found")
Some(Folder(_)) => fail!("\{path} is a folder")
Some(JsonConfig(_)) => fail!("\{path} is a json config")
}
...
process(text)
}
当省略else
的部分时,卫语句指定的条件不为真或者无法匹配时,程序终止。
guard condition // 相当于 guard condition else { panic() }
guard let Some(x) = expr // 相当于 guard let Some(x) = expr else { _ => panic() }
迭代器
迭代器(Iterator)是一个用来遍历访问某个序列的元素的对象。传统面向对象语言(例如 Java),使用 Iterator<T>
和 next()
hasNext()
来步进一个迭代过程;而函数式语言(例如 JavaScript 的 forEach
,Lisp 的
mapcar
)则是通过接收某个操作和序列,并在遍历过程中将操作应用于该序列的高阶函数来实现迭代器。
前者叫做外部迭代器(对用户可见);后者称为内部迭代器(对用户不可见)。
MoonBit 的内置类型 Iter[T]
提供了迭代器支持。基本上所有的内置序列结构都实现了 Iter:
fn filter_even(l : Array[Int]) -> Array[Int] {
let l_iter : Iter[Int] = l.iter()
l_iter.filter(fn { x => (x & 1) == 1 }).collect()
}
fn fact(n : Int) -> Int {
let start = 1
start.until(n).fold(Int::op_mul, init=start)
}
常用的方法包括:
-
each
:遍历迭代器的每个元素,并将接收的函数应用于每个元素上 -
fold
:用给定的函数和一个初始值折叠(归约)某个迭代器 -
collect
:将迭代器中的元素收集到一个Array
中 -
filter
:(惰性)用某个函数(谓词)过滤迭代器的元素 -
map
:(惰性)用某个函数转化迭代器中的元素 -
concat
:(惰性)将一个迭代器中的元素全部加到另一个的尾部
类似 filter
map
这样的方法在序列结构上很常见。但是 Iter 特别的地方在于任何构造一个新 Iter 的方法都是惰性的(即调用方法不会立即执行迭代,因为套了一层函数),这种性质是 Iter 不产生中间值的体现。这就是在遍历序列上 Iter 的优势:没有额外开销。MoonBit 鼓励用户使用 Iter 在函数间传参,而不是使用序列本身。
例如 Array
这样预定义的序列结构和其自带的迭代器应当足够使用。但要让上面的方法也适用于自定义的序列结构,就需要手动实现 Iter,以 Bytes
为例:
fn iter(data : Bytes) -> Iter[Byte] {
Iter::new(
fn(yield) {
// The code that actually does the iteration
/////////////////////////////////////////////
for i = 0, len = data.length(); i < len; i = i + 1 {
if yield(data[i]) == IterEnd {
break IterEnd
}
/////////////////////////////////////////////
} else {
IterContinue
}
},
)
}
基本上所有的 Iter
实现都和上述 Bytes
的相似,唯一的不同点在于实际用于迭代的代码部分。
实现细节
Iter[T]
实际上是 ((T) -> IterResult)->IterResult
的类型别名,即一个接收某个操作的高阶函数。
IterResult
是一个记录迭代过程状态的 enum
对象,其包含两个迭代状态:
IterEnd
:表示迭代终点IterContinue
:表示迭代尚未到达终点,即迭代在这个状态下会进行下去。
简单来说,Iter[T]
接收一个函数 (T) -> IterResult
并利用其转换自身的状态(IterResult
),转换后的状态是两者中的哪一个则由这个函数决定。
迭代器为我们提供了一个统一的方法用于序列结构的迭代,
且构造这样的迭代器是几乎没有额外开销的:只要 fn(yield)
没有执行,那么迭代就不会开始。
在内部实现中 Iter::run()
是用于触发迭代的。串接各种 Iter 的方法可能在写法上显得十分优雅,但也需要注意方法抽象之下的迭代过程。
与外部迭代器不同,内部迭代器只要迭代过程一开始就无法停止,除非到达迭代终点。
类似 count()
(返回某个迭代器元素数目)之类的方法看上去是 O(1)
,
实际上却是线性复杂度。因此对于内部迭代器需要小心使用,否则可能产生性能问题。