MoonBit
MoonBit是一个用于云计算和边缘计算的WebAssembly端到端编程语言工具链。您可以在 https://try.moonbitlang.com 获得IDE环境,无需安装任何软件,也不依赖任何服务器。
状态
MoonBit目前处于Pre-alpha阶段,是实验性质的。我们期望明年达到beta阶段。
主要优势
- 生成与现有解决方案相比显著更小的WASM文件。
- 更高的运行时性能。
- 先进的编译时性能。
- 简单但实用的数据导向语言设计。
概述
一个月兔程序由类型定义,函数定义和变量绑定组成。每个包的入口点是一个特殊的init
函数。init
函数特殊在以下两个方面:
- 在同一个包中可以有多个
init
函数。 init
函数不能被显式地调用或被其他函数引用。相反,在一个包初始化时,所有的init
函数都将被隐式调用。因此,init
函数只应该包含语句。
func init {
"Hello world!".print() // OK
}
func init {
let x = 1
// x // fail
x.print() // success
}
MoonBit区分语句和表达式。在一个函数体中,只有最后一句应该是作为返回值的表达式。例如:
func foo() -> Int {
let x = 1
x + 1 // OK
}
func bar() -> Int {
let x = 1
x + 1 // fail
x + 2
}
func init {
foo().print()
bar().print()
}
表达式和语句
表达式包括:
- 值字面量(例如布尔值、数字、字符、字符串、数组、元组、结构体)
- 算术、逻辑或比较操作
- 访问数组元素(例如
a[0]
)或结构体字段(例如r.x
)或元组组成部分(例如t.0
) - 变量和(大写字母开头的)枚举构造器
- 匿名局部函数定义
match
和if
表达式
语句包括:
- 命名局部函数定义
- 局部变量绑定
- 赋值
- While循环和相关的控制流结构(
break
和continue
) return
语句- 返回类型为
unit
的任何表达式
函数
函数接受参数并产生结果。在MoonBit中,函数是一等公民,这意味着函数可以作为其他函数的参数或返回值。
顶层函数
Functions can be defined as top-level or local.函数可以被定义为顶层或局部。我们可以使用func
关键字定义一个顶层函数,例如以下求三个整数之和并返回结果的函数:
func add3(x: Int, y: Int, z: Int)-> Int {
x + y + z
}
注意顶层函数的参数和返回值类型需要显式标注。如果返回类型被省略,函数将被视为返回unit
类型。
局部函数
局部函数使用fn
关键字定义。局部函数可以是命名或匿名的。在大多数情况下,局部函数的类型注解可以省略,因为编译器可以自动推断。例如:
func foo() -> Int {
fn inc(x) { x + 1 } // named as `inc`
fn (x) { x + inc(2) } (6) // anonymous, instantly applied to integer literal 6
}
func init {
foo().print()
}
无论是命名的还是匿名的,函数都是 词法闭包:任何没有局部绑定的标识符必须引用来自周围词法范围的绑定
let y = 3
func foo(x: Int) {
fn inc() { x + 1 } // OK, will return x + 1
fn four() { y + 1 } // Ok, will return 4
inc().print()
four().print()
}
func init {
foo(2)
}
函数调用
函数可以用圆括号内的参数列表进行调用:
add3(1, 2, 7)
这适用于命名函数(如前面的例子)和绑定到函数值的变量,如下所示:
func init {
var add3 = fn(x, y, z) { x + y + z }
add3(1, 2, 7).print()
}
表达式add3(1, 2, 7)
返回10
。任何求值为函数值的表达式都可以被调用:
func init {
let f = fn (x) { x + 1 }
let g = fn (x) { x + 2 }
(if true { f } else { g })(3).print() // OK
}
控制结构
条件表达式
条件表达式由条件、结果和一个可选的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
子句的返回值类型必须相同。
循环
MoonBit中的主要循环语句是while循环:
while x == y {
expr1
}
while语句不返回任何值;它只求值成unit
类型的()
MoonBit还提供break
和continue
语句来控制循环流。
内置数据结构
元组
元组是一个有限值的集合,使用圆括号()
构造,其中元素由逗号,
分隔。元素的顺序很重要,例如(1, true)
和(true, 1)
是不同的类型。以下是一个例子:
func pack(a: Bool, b: Int, c: String, d: Float) -> (Bool, Int, String, Float) {
(a, b, c, d)
}
func init {
let quad = pack(false, 100, "text", 3.14)
let (bool_val, int_val, str, float_val) = quad
}
可以通过模式匹配或索引来访问元组:
func f(t : (Int, Int)) {
let (x1, y1) = t // access via pattern matching
// access via index
let x2 = t.0
let y2 = t.1
if (x1 == x2 && y1 == y2) {
"yes".print()
} else {
"no".print()
}
}
func init {
f((1, 2))
}
数组
数组是由方括号[]
构造的有限值序列,其中元素由逗号,
分隔。例如:
let array = [1, 2, 3, 4]
可以使用array[x]
来引用第x个元素。索引从零开始。
func init {
let array = [1, 2, 3, 4]
let a = array[2]
array[3] = 5
let b = a + array[3]
b.print() // prints 8
}
变量绑定
变量可以使用关键字var
或let
分别声明为可变或不可变。可变变量可以重新赋值,不可变变量则不能。
let zero = 0
func init {
var i = 10
i = 20
(i + zero).print()
}
对于局部不可变绑定,还可以使用:=
的简写语法糖。
func init {
a := 3
b := "hello"
a.print()
b.print()
}
数据类型
创建新数据类型的方法有两种:struct
和enum
。
结构
在MoonBit中,结构与元组类似,但是它们的字段由字段名索引。结构体可以使用结构体字面量构造,结构体字面量由一组带有标签的值组成,并用花括号括起来。如果结构体字面量的字段完全匹配类型定义,则其类型可以被自动推断。使用点语法s.f可以访问结构体字段。如果一个字段使用关键字mut标记为可变,那么可以给它赋予新的值。
struct User {
id: Int
name: String
mut email: String
}
func init {
let u = { id: 0, name: "John Doe", email: "john@doe.com" }
u.email = "john@doe.name"
u.id.print()
u.name.print()
u.email.print()
}
注意,您还可以在结构类型中包含与之关联的方法,例如:
struct Stack {
mut elems: List[Int]
push: (Int) -> Unit
pop: () -> Int
}
枚举
枚举类型和函数式语言中的代数数据类型类似。枚举可以有一组情况,并且每个情况和元组类似,可以指定不同类型的关联值。每个情况的标签必须大写,称为数据构造函数。可以通过调用带有指定类型参数的数据构造函数来构造枚举。枚举的构造必须使用标明类型。可以通过模式匹配来解构枚举,并且可以将关联值绑定到每个模式中指定的变量。
enum List {
Nil
Cons (Int, List)
}
func print(l: List) {
match l {
Nil => "nil".print()
Cons(x, xs) => {
x.print();
",".print();
print(xs)
}
}
}
func init {
let l: List = Cons(1, Cons(2, Nil))
print(l)
}
模式匹配
我们已经展示了对枚举进行模式匹配的用例,但是模式匹配不局限于枚举。例如,我们也可以对布尔值、数字、字符、字符串、元组、数组和结构体字面量进行模式匹配。由于这些类型和枚举不同,只有一种情况,我们可以使用let
/var
绑定而不是match
表达式来对它们进行模式匹配。需要注意的是,在match
中绑定的变量的作用域仅限于引入该变量的分支,而let
/var
绑定将引入每个变量到当前作用域。此外,我们可以使用下划线 \_
作为我们不关心的值的通配符。
let id = match u {
{ id: id, name: _, email: _ } => id
}
// is equivalent to
let { id: id, name: _, email: _ } = u
在模式匹配中还有一些其他有用的构造。例如,我们可以使用as
为某个模式指定一个名称,并且可以使用|
同时匹配多个情况。
match expr {
e as Lit(n) => ...
Add(e1, e2) | Mul(e1, e2) => ...
_ => ...
}
泛型
您可以在顶层的函数和数据结构定义中使用泛型。类型参数可以由方括号引入。我们可以重写前面提到的数据类型List
,添加类型参数T
,以获得一个泛型版本的列表。然后,我们可以定义泛型函数map
和reduce
,用于对列表进行操作。
enum List[T] {
Nil
Cons(T, List[T])
}
func map[S, T](self: List[S], f: (S) => T) -> List[T] {
match self {
Nil => Nil
Cons(x, xs) => Cons(f(x), map(xs, f))
}
}
func reduce[S, T](self: List[S], op: (T, S) => T, init: T) -> T {
match self {
Nil => init
Cons(x, xs) => reduce(xs, op, op(init, x))
}
}
统一函数调用语法
MoonBit支持与传统面向对象语言不同的方法(method)。一个方法被定义为self
作为其第一个参数的顶层函数。self
参数将成为方法调用的主体。例如,l.map(f)
等同于map(l, f)
。这种语法使得方法链而不是嵌套的函数调用成为可能。例如,我们可以使用这样的语法将先前定义的map
和reduce
与from\_array
和```
方法链在一起,执行列表操作。
func map[S, T](self: List[S], f: (S) -> T) -> List[T] {
match self {
Nil => Nil
Cons(x, xs) => Cons(f(x), map(xs, f))
}
}
func reduce[S, T](self: List[S], op: (T, S) -> T, init: T) -> T {
match self {
Nil => init
Cons(x, xs) => reduce(xs, op, op(init, x))
}
}
func from_array[T](self: Array[T]) -> List[T] {
var res: List[T] = Nil
var i = self.length() - 1
while (i >= 0) {
res = Cons(self[i], res)
i = i - 1
}
res
}
func init {
[1, 2, 3, 4, 5].from_array().map(fn(x) { x * 2 }).reduce(fn(x, y) { x + y }, 0).print()
}
方法和普通函数的另一个区别是,只有方法支持重载。例如,我们可能需要多个输出函数:
用于不同类型的输出。但使用方法\\_int</code>和<code>
\_float```
,可以识别主体的类型,并选择适当的重载版本,例如1.print()和1.0.print()。
运算符重载
MoonBit支持重载内置运算符。与运算符
相对应的方法名称是op\_
。例如:
pub struct T {
x:Int
}
pub func op_add(self: T, other: T) -> T {
{ x: self.x + other.x }
}
func init {
let a = { x:0, }
let b = { x:2, }
(a + b).x.print()
}
目前,以下运算符可以被重载:
operator name | method name |
---|---|
\+ | op\add |
\- | op\_sub |
\* | op\_mul |
/ | op\_div |
% | op\_mod |
\- (unary) | op\_neg |
\[\] (get item) | op\_get |
\[\] = \ (set item) | op\_set |
访问控制
默认情况下,所有函数定义和变量绑定对其他包是 不可见 的;没有修饰符的类型是抽象数据类型,其名称被导出,但内部是不可见的。这种设计防止了意外暴露实现细节。您可以在type
/func
/let
前使用pub
修饰符使其完全可见,或在type
前使用priv
修饰符使其对其他包完全不可见。您还可以在字段名前使用pub
或priv
获得更细粒度的访问控制。但是,请注意:
- 在抽象或私有结构体内,所有字段都不能被定义为
pub
,因为这样没有意义。 - 枚举类型的构造器没有单独的可见性,所以不能在它们前面使用
pub
或priv
struct R1 { // abstract data type by default
x: Int // implicitly private field
pub y: Int // ERROR: `pub` field found in a abstract type!
priv z: Int // WARNING: `priv` is redundant!
}
pub struct R2 { // explicitly public struct
x: Int // implicitly public field
pub y: Int // WARNING: `pub` is redundant!
priv z: Int // explicitly private field
}
priv struct R3 { // explicitly private struct
x: Int // implicitly private field
pub y: Int // ERROR: `pub` field found in a private type!
priv z: Int // WARNING: `priv` is redundant!
}
enum T1 { // abstract data type by default
A(Int) // implicitly private variant
pub B(Int) // ERROR: no individual visibility!
priv C(Int) // ERROR: no individual visibility!
}
pub enum T2 { // explicitly public enum
A(Int) // implicitly public variant
pub B(Int) // ERROR: no individual visibility!
priv C(Int) // ERROR: no individual visibility!
}
priv enum T3 { // explicitly private enum
A(Int) // implicitly private variant
pub B(Int) // ERROR: no individual visibility!
priv C(Int) // ERROR: no individual visibility!
}
MoonBit中另一个有用的特性是 pub(readonly)
类型,其受到了 OCaml private types的启发。简而言之,pub(readonly)
类型的值可以使用模式匹配或点语法析构,但在其他包中,不能被构造或改变。注意到在pub(readonly)
类型定义的同一个包中,它没有任何限制。
// Package A
pub(readonly) struct RO {
field: Int
}
func init {
let r = { field: 4 } // OK
let r = { ..r, field: 8 } // OK
}
// Package B
func print(r : RO) {
"{ field: ".print()
r.field.print() // OK
" }".print()
}
func init {
let r : RO = { field: 4 } // ERROR: Cannot create values of the public read-only type RO!
let r = { ..r, field: 8 } // ERROR: Cannot mutate a public read-only field!
}
MoonBit中的访问控制遵循这样一个原则:pub
类型、函数或变量不能基于私有类型进行定义。这是因为私有类型可能不是在使用pub
实体的所有地方都可以被访问。MoonBit内建了一些检查,以防止违反这一原则的用例。
pub struct s {
x: T1 // OK
y: T2 // OK
z: T3 // ERROR: public field has private type `T3`!
}
// ERROR: public function has private parameter type `T3`!
pub func f1(_x: T3) -> T1 { T1::A(0) }
// ERROR: public function has private return type `T3`!
pub func f2(_x: T1) -> T3 { T3::A(0) }
// OK
pub func f3(_x: T1) -> T1 { T1::A(0) }
pub let a: T3 // ERROR: public variable has private type `T3`!
字符串插值
字符串插值是MoonBit的一个强大特性,它允许您在插值字符串中替换变量。这个特性通过直接将变量值嵌入文本中,简化了构造动态字符串的过程。
x := 42
"The answer is \\(x)".print()
用于字符串插值的变量必须支持to\_string
方法。