MoonBit
MoonBit 是一个用于云计算和边缘计算的 WebAssembly 端到端的编程语言工具链。 您可以访问 https://try.moonbitlang.cn 获得 IDE 环境,无需安装任何软件,也不依赖任何服务器。
状态
MoonBit 目前处于 Pre-alpha 阶段,是实验性质的。我们期望今年能达到 beta 阶段。
主要优势
- 生成比现有解决方案明显更小的 WASM 文件。
- 更高的运行时性能。
- 先进的编译时性能。
- 简单且实用的数据导向语言设计。
概述
一个月兔程序由类型定义,函数定义和变量绑定组成。
程序入口
有一个特殊的函数: init
函数,它有以下两个特点:
- 同一个包中可以有多个
init
函数。 init
函数不能被显式调用或被其他函数引用。相反,在一个包初始化时,所有的init
函数都将被隐式地调用。因此,init
函数中只能包含语句。
fn init {
print("Hello world!") // OK
}
fn init {
let x = 1
// x // 失败
print(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
}
fn init {
print(foo())
print(bar())
}
表达式包括:
- 值字面量(例如布尔值、数字、字符、字符串、数组、元组、结构体)
- 算术、逻辑和比较运算
- 访问数组元素(例如
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 init {
print(foo())
}
无论是命名的还是匿名的,函数都是 词法闭包:任何没有局部绑定的标识符, 必须引用来自周围词法作用域的绑定:
let y = 3
fn foo(x: Int) -> Unit {
fn inc() { x + 1 } // OK,返回 x + 1
fn four() { y + 1 } // Ok,返回 4
print(inc())
print(four())
}
fn init {
foo(2)
}
函数调用
函数可通过向圆括号内传入参数列表进行调用:
add3(1, 2, 7)
这适用于命名函数(如前面的例子)和绑定到函数值的变量,如下所示:
fn init {
let add3 = fn(x, y, z) { x + y + z }
print(add3(1, 2, 7))
}
表达式 add3(1, 2, 7)
返回 10
。任何求值为函数值的表达式都可以被调用:
fn init {
let f = fn (x) { x + 1 }
let g = fn (x) { x + 2 }
print((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 init {
println(optional()) // 42
println(optional(opt=0)) // 0
}
每次使用默认参数调用一个函数时,都会重新求值默认值的表达式,也会被重新触发其中的副作用。例如:
fn incr(~counter : Ref[Int] = { val: 0 }) -> Ref[Int] {
counter.val = counter.val + 1
counter
}
fn init {
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 init {
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 init {
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 init {
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
子句的返回值类型必须相同。
While 循环
MoonBit中支持while
循环。while
后的循环条件会在循环体 之前执行,当循环条件为真时, 执行循环体:
let mut i = 5
while i > 0 {
println(i)
i = i - 1
}
循环体内支持break
和continue
。使用break
能够跳出当前循环;使用continue
跳过本次循环的剩余部分,提前进入下一次循环。
let mut i = 5
while i > 0 {
i = i - 1
if i == 4 { continue }
if i == 1 { break }
println(i)
}
输出:
3
2
while
循环也支持可选的else
子句。当循环条件转变为假时,将会执行else
子句,然后循环结束。
let mut i = 2
while i > 0 {
println(i)
i = i - 1
} else {
println(i)
}
输出:
2
1
0
当存在 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
函数式循环
函数式循环是 MoonBit 中一个强大的特性,它能让您以函数式风格编写循环。
函数式循环接受参数并返回一个值。它使用 loop
关键字定义,后跟其参数和循环体。
循环体是一系列子句,每个子句由模式和表达式组成。
与输入匹配的模式会被执行,并且循环将返回表达式的值。如果没有匹配的模式,循环将抛出异常。
可以使用 continue
关键字和参数开始下一次循环迭代,使用 break
关键字和参数来从循环中返回一个值。
如果值是循环体中的最后一个表达式,则可以省略 break
关键字。
fn sum(xs: List[Int]) -> Int {
loop xs, 0 {
Nil, acc => break acc // break 可以省略
Cons(x, rest), acc => continue rest, x + acc
}
}
fn init {
println(sum(Cons(1, Cons(2, Cons(3, Nil)))))
}
内置数据结构
布尔值
MoonBit 内置了布尔类型,它有两个值:true
和 false
。布尔类型用于条件表达式和控制结构。
let a = true
let b = false
let c = a && b
let d = a || b
let e = not(a)
数字
MoonBit 支持整 型和浮点类型:
类型 | 描述 | 例子 |
---|---|---|
Int | 32 位有符号整数 | 42 |
Int64 | 64 位有符号整数 | 1000L |
UInt | 32 位无符号整数 | 14U |
UInt64 | 64 位无符号整数 | 14UL |
Double | 64 位浮点数,由 IEEE754 定义 | 3.14 |
MoonBit 支持的数字字面量,包括十进制、二进制、八进制和十六进制。
为了提升可读性,你可以在数字字面量内插入下划线,例如 1_000_000
。
注意,下划线可以插入到数字之间的任何位置,而非只能在每三个数字之间。
-
十进制数和往常一样。
let a = 1234
let b = 1_000_000 + a
let large_num = 9_223_372_036_854_775_807L // Int64 类型的整数必须后缀“L”
let unsigned_num = 4_294_967_295U // UInt 类型的整数必须有后缀”U“ -
八进制数的前缀是 0 后接字母 O,也就是
0o
或0O
。注意在0o
或0O
之后出现的数字只能在0
到7
之间。let octal = 0o1234
let another_octal = 0O1234 -
十六进制数的前缀是 0 后接字母 X,也就是
0x
或0X
。注意在0x
或0X
之后出现的数字只能是0123456789ABCDEF
之一。let hex = 0XA
let another_hex = 0xA
字符串
字符串String
内部保存了 UTF-16 编码单元序列。可以使用双引号来表示一个字符串,或者通过#|
来书写多行字符串。
let a = "兔rabbit"
println(a[0]) // output: 兔
println(a[1]) // output: r
let b =
#| Hello
#| MoonBit
#|
在双引号包围的字符串之间支持使用\
表示特殊字符转义:
转义序列 | 解释 |
---|---|
\n ,\r ,\t ,\b | 换行、回车、水平制表符、退格 |
\\ | 反斜杠 |
\x41 | 16 进制转义序列 |
\o102 | 8 进制转义序列 |
\u5154 ,\u{1F600} | Unicode 字符转义序列 |
MoonBit 支持字符串插值,它可以把字符串中内插的变量替换为变量具体的值。 这个特性能够简化动态拼接字符串的过程。
fn init {
let x = 42
print("The answer is \(x)")
}
用于字符串内插的变量必须支持 to_string
方法。
字符
字符Char
是表示一个 Unicode 字符的整数。
let a : Char = 'A'
let b = '\x41'
let c = '🐰'
let zero = '\u{30}'
let zero = '\u0030'