MoonBit

MoonBit是一个用于云计算和边缘计算的WebAssembly端到端编程语言工具链。您可以在 https://try.moonbitlang.com 获得IDE环境,无需安装任何软件,也不依赖任何服务器。

状态

MoonBit目前处于Pre-alpha阶段,是实验性质的。我们期望明年达到beta阶段。

主要优势

  • 生成与现有解决方案相比显著更小的WASM文件。
  • 更高的运行时性能。
  • 先进的编译时性能。
  • 简单但实用的数据导向语言设计。

概述

一个月兔程序由类型定义,函数定义和变量绑定组成。每个包的入口点是一个特殊的init函数。init函数特殊在以下两个方面:

  • 在同一个包中可以有多个 init 函数。
  • init 函数不能被显式地调用或被其他函数引用。相反,在一个包初始化时,所有的init函数都将被隐式调用。因此,init函数只应该包含语句。
  1. func init {
  2. "Hello world!".print() // OK
  3. }
  4. func init {
  5. let x = 1
  6. // x // fail
  7. x.print() // success
  8. }

MoonBit区分语句和表达式。在一个函数体中,只有最后一句应该是作为返回值的表达式。例如:

  1. func foo() -> Int {
  2. let x = 1
  3. x + 1 // OK
  4. }
  5. func bar() -> Int {
  6. let x = 1
  7. x + 1 // fail
  8. x + 2
  9. }
  10. func init {
  11. foo().print()
  12. bar().print()
  13. }

表达式和语句

表达式包括:

  • 值字面量(例如布尔值、数字、字符、字符串、数组、元组、结构体)
  • 算术、逻辑或比较操作
  • 访问数组元素(例如a[0])或结构体字段(例如r.x)或元组组成部分(例如t.0
  • 变量和(大写字母开头的)枚举构造器
  • 匿名局部函数定义
  • matchif表达式

语句包括:

  • 命名局部函数定义
  • 局部变量绑定
  • 赋值
  • While循环和相关的控制流结构(breakcontinue
  • return语句
  • 返回类型为unit的任何表达式

函数

函数接受参数并产生结果。在MoonBit中,函数是一等公民,这意味着函数可以作为其他函数的参数或返回值。

顶层函数

Functions can be defined as top-level or local.函数可以被定义为顶层或局部。我们可以使用func关键字定义一个顶层函数,例如以下求三个整数之和并返回结果的函数:

  1. func add3(x: Int, y: Int, z: Int)-> Int {
    x + y + z
    }

注意顶层函数的参数和返回值类型需要显式标注。如果返回类型被省略,函数将被视为返回unit类型。

局部函数

局部函数使用fn关键字定义。局部函数可以是命名或匿名的。在大多数情况下,局部函数的类型注解可以省略,因为编译器可以自动推断。例如:

  1. func foo() -> Int {
  2. fn inc(x) { x + 1 } // named as `inc`
  3. fn (x) { x + inc(2) } (6) // anonymous, instantly applied to integer literal 6
  4. }
  5. func init {
  6. foo().print()
  7. }

无论是命名的还是匿名的,函数都是 词法闭包:任何没有局部绑定的标识符必须引用来自周围词法范围的绑定

  1. let y = 3
  2. func foo(x: Int) {
  3. fn inc() { x + 1 } // OK, will return x + 1
  4. fn four() { y + 1 } // Ok, will return 4
  5. inc().print()
  6. four().print()
  7. }
  8. func init {
  9. foo(2)
  10. }

函数调用

函数可以用圆括号内的参数列表进行调用:

  1. add3(1, 2, 7)

这适用于命名函数(如前面的例子)和绑定到函数值的变量,如下所示:

  1. func init {
  2. var add3 = fn(x, y, z) { x + y + z }
  3. add3(1, 2, 7).print()
  4. }

表达式add3(1, 2, 7)返回10。任何求值为函数值的表达式都可以被调用:

  1. func init {
  2. let f = fn (x) { x + 1 }
  3. let g = fn (x) { x + 2 }
  4. (if true { f } else { g })(3).print() // OK
  5. }

控制结构

条件表达式

条件表达式由条件、结果和一个可选的else子句组成。

  1. if x == y {
    expr1
    } else {
    expr2
    }

    if x == y {
    expr1
    }

else子句也可以包含另一个if\-else表达式:

  1. if x == y {
    expr1
    } else if z == k {
    expr2
    }

花括号在结果或 else 子句中用于组合表达式

需要注意的是,在MoonBit中,条件表达式总是返回一个值,结果和else子句的返回值类型必须相同。

循环

MoonBit中的主要循环语句是while循环:

  1. while x == y {
    expr1
    }

while语句不返回任何值;它只求值成unit类型的()MoonBit还提供breakcontinue语句来控制循环流。

内置数据结构

元组

元组是一个有限值的集合,使用圆括号()构造,其中元素由逗号,分隔。元素的顺序很重要,例如(1, true)(true, 1)是不同的类型。以下是一个例子:

  1. func pack(a: Bool, b: Int, c: String, d: Float) -> (Bool, Int, String, Float) {
  2. (a, b, c, d)
  3. }
  4. func init {
  5. let quad = pack(false, 100, "text", 3.14)
  6. let (bool_val, int_val, str, float_val) = quad
  7. }

可以通过模式匹配或索引来访问元组:

  1. func f(t : (Int, Int)) {
  2. let (x1, y1) = t // access via pattern matching
  3. // access via index
  4. let x2 = t.0
  5. let y2 = t.1
  6. if (x1 == x2 && y1 == y2) {
  7. "yes".print()
  8. } else {
  9. "no".print()
  10. }
  11. }
  12. func init {
  13. f((1, 2))
  14. }

数组

数组是由方括号[]构造的有限值序列,其中元素由逗号,分隔。例如:

  1. let array = [1, 2, 3, 4]

可以使用array[x]来引用第x个元素。索引从零开始。

  1. func init {
  2. let array = [1, 2, 3, 4]
  3. let a = array[2]
  4. array[3] = 5
  5. let b = a + array[3]
  6. b.print() // prints 8
  7. }

变量绑定

变量可以使用关键字varlet分别声明为可变或不可变。可变变量可以重新赋值,不可变变量则不能。

  1. let zero = 0
  2. func init {
  3. var i = 10
  4. i = 20
  5. (i + zero).print()
  6. }

对于局部不可变绑定,还可以使用:=的简写语法糖。

  1. func init {
  2. a := 3
  3. b := "hello"
  4. a.print()
  5. b.print()
  6. }

数据类型

创建新数据类型的方法有两种:structenum

结构

在MoonBit中,结构与元组类似,但是它们的字段由字段名索引。结构体可以使用结构体字面量构造,结构体字面量由一组带有标签的值组成,并用花括号括起来。如果结构体字面量的字段完全匹配类型定义,则其类型可以被自动推断。使用点语法s.f可以访问结构体字段。如果一个字段使用关键字mut标记为可变,那么可以给它赋予新的值。

  1. struct User {
  2. id: Int
  3. name: String
  4. mut email: String
  5. }
  6. func init {
  7. let u = { id: 0, name: "John Doe", email: "john@doe.com" }
  8. u.email = "john@doe.name"
  9. u.id.print()
  10. u.name.print()
  11. u.email.print()
  12. }

注意,您还可以在结构类型中包含与之关联的方法,例如:

  1. struct Stack {
    mut elems: List[Int]
    push: (Int) -> Unit
    pop: () -> Int
    }

枚举

枚举类型和函数式语言中的代数数据类型类似。枚举可以有一组情况,并且每个情况和元组类似,可以指定不同类型的关联值。每个情况的标签必须大写,称为数据构造函数。可以通过调用带有指定类型参数的数据构造函数来构造枚举。枚举的构造必须使用标明类型。可以通过模式匹配来解构枚举,并且可以将关联值绑定到每个模式中指定的变量。

  1. enum List {
  2. Nil
  3. Cons (Int, List)
  4. }
  5. func print(l: List) {
  6. match l {
  7. Nil => "nil".print()
  8. Cons(x, xs) => {
  9. x.print();
  10. ",".print();
  11. print(xs)
  12. }
  13. }
  14. }
  15. func init {
  16. let l: List = Cons(1, Cons(2, Nil))
  17. print(l)
  18. }

模式匹配

我们已经展示了对枚举进行模式匹配的用例,但是模式匹配不局限于枚举。例如,我们也可以对布尔值、数字、字符、字符串、元组、数组和结构体字面量进行模式匹配。由于这些类型和枚举不同,只有一种情况,我们可以使用let/var绑定而不是match表达式来对它们进行模式匹配。需要注意的是,在match中绑定的变量的作用域仅限于引入该变量的分支,而let/var绑定将引入每个变量到当前作用域。此外,我们可以使用下划线 \_ 作为我们不关心的值的通配符。

  1. let id = match u {
    { id: id, name: _, email: _ } => id
    }
    // is equivalent to
    let { id: id, name: _, email: _ } = u

在模式匹配中还有一些其他有用的构造。例如,我们可以使用as为某个模式指定一个名称,并且可以使用|同时匹配多个情况。

  1. match expr {
    e as Lit(n) => ...
    Add(e1, e2) | Mul(e1, e2) => ...
    _ => ...
    }

泛型

您可以在顶层的函数和数据结构定义中使用泛型。类型参数可以由方括号引入。我们可以重写前面提到的数据类型List,添加类型参数T,以获得一个泛型版本的列表。然后,我们可以定义泛型函数mapreduce,用于对列表进行操作。

  1. enum List[T] {
  2. Nil
  3. Cons(T, List[T])
  4. }
  5. func map[S, T](self: List[S], f: (S) => T) -> List[T] {
  6. match self {
  7. Nil => Nil
  8. Cons(x, xs) => Cons(f(x), map(xs, f))
  9. }
  10. }
  11. func reduce[S, T](self: List[S], op: (T, S) => T, init: T) -> T {
  12. match self {
  13. Nil => init
  14. Cons(x, xs) => reduce(xs, op, op(init, x))
  15. }
  16. }

统一函数调用语法

MoonBit支持与传统面向对象语言不同的方法(method)。一个方法被定义为self作为其第一个参数的顶层函数。self参数将成为方法调用的主体。例如,l.map(f)等同于map(l, f)。这种语法使得方法链而不是嵌套的函数调用成为可能。例如,我们可以使用这样的语法将先前定义的mapreducefrom\_array```方法链在一起,执行列表操作。

  1. func map[S, T](self: List[S], f: (S) -> T) -> List[T] {
  2. match self {
  3. Nil => Nil
  4. Cons(x, xs) => Cons(f(x), map(xs, f))
  5. }
  6. }
  7. func reduce[S, T](self: List[S], op: (T, S) -> T, init: T) -> T {
  8. match self {
  9. Nil => init
  10. Cons(x, xs) => reduce(xs, op, op(init, x))
  11. }
  12. }
  13. func from_array[T](self: Array[T]) -> List[T] {
  14. var res: List[T] = Nil
  15. var i = self.length() - 1
  16. while (i >= 0) {
  17. res = Cons(self[i], res)
  18. i = i - 1
  19. }
  20. res
  21. }
  22. func init {
  23. [1, 2, 3, 4, 5].from_array().map(fn(x) { x * 2 }).reduce(fn(x, y) { x + y }, 0).print()
  24. }

方法和普通函数的另一个区别是,只有方法支持重载。例如,我们可能需要多个输出函数:\\_int</code>和<code>\_float用于不同类型的输出。但使用方法```,可以识别主体的类型,并选择适当的重载版本,例如1.print()和1.0.print()。

运算符重载

MoonBit支持重载内置运算符。与运算符相对应的方法名称是op\_。例如:

  1. pub struct T {
  2. x:Int
  3. }
  4. pub func op_add(self: T, other: T) -&gt; T {
  5. { x: self.x + other.x }
  6. }
  7. func init {
  8. let a = { x:0, }
  9. let b = { x:2, }
  10. (a + b).x.print()
  11. }

目前,以下运算符可以被重载:

operator namemethod 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修饰符使其对其他包完全不可见。您还可以在字段名前使用pubpriv获得更细粒度的访问控制。但是,请注意:

  • 在抽象或私有结构体内,所有字段都不能被定义为pub,因为这样没有意义。
  • 枚举类型的构造器没有单独的可见性,所以不能在它们前面使用 pubpriv
  1. 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)类型定义的同一个包中,它没有任何限制。

  1. // 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内建了一些检查,以防止违反这一原则的用例。

  1. 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的一个强大特性,它允许您在插值字符串中替换变量。这个特性通过直接将变量值嵌入文本中,简化了构造动态字符串的过程。

  1. x := 42
    "The answer is \\(x)".print()

用于字符串插值的变量必须支持to\_string方法。