基础#

内置数据结构#

Unit#

Unit 是 MoonBit 中的一个内置类型,它表示没有有意义的值。它只有一个值,写作 ()Unit 类似于 C/C++/Java 中的 void,但与 void 不同的是,它是一个真正的类型,可以在任何需要类型的地方使用。

Unit 类型通常用作执行某些操作但不产生有意义结果的函数的返回类型:

fn print_hello() -> Unit {
  println("Hello, world!")
}

与其他一些语言不同,MoonBit 将 Unit 视为一等类型,允许它在泛型中使用、存储在数据结构中以及作为函数参数传递。

布尔值#

MoonBit 有一个内置的布尔类型,它有两个值:truefalse。布尔类型用于条件表达式和控制结构。可以使用 ! 对布尔值取反;not(x) 等价。

let a = true
let b = false
let c = a && b
let d = a || b
let e = !a
let f = !(a && b)

数字#

MoonBit 有整数类型和浮点类型:

类型

描述

示例

Int16

16 位有符号整数

(42 : Int16)

Int

32 位有符号整数

42

Int64

64 位有符号整数

1000L

UInt16

16 位无符号整数

(14 : UInt16)

UInt

32 位无符号整数

14U

UInt64

64 位无符号整数

14UL

Double

64 位浮点数,由 IEEE754 定义

3.14

Float

32 位浮点数

(3.14 : Float)

BigInt

表示比其他类型更大的数值

10000000000000000000000N

MoonBit 还支持数字字面量,包括十进制、二进制、八进制和十六进制数字。

为了提高可读性,您可以在数字字面量中间放置下划线,例如 1_000_000。请注意,下划线可以放在数字中的任何位置,而不仅仅是每三位数字。

  • 十进制数之间可以有下划线。

    默认情况下,整数字面量是有符号的 32 位数字。对于无符号数字,需要后缀 U;对于 64 位数字,需要后缀 L

    let a = 1234
    let b : Int = 1_000_000 + a
    let unsigned_num       : UInt   = 4_294_967_295U
    let large_num          : Int64  = 9_223_372_036_854_775_807L
    let unsigned_large_num : UInt64 = 18_446_744_073_709_551_615UL
    
  • 二进制数有一个前导零,后跟字母 “B”,即 0b/0B。请注意,0b/0B 后的数字必须是 01

    let bin = 0b110010
    let another_bin = 0B110010
    
  • 八进制数有一个前导零,后跟字母 “O”,即 0o/0O。请注意,0o/0O 后的数字必须在 07 的范围内:

    let octal = 0o1234
    let another_octal = 0O1234
    
  • 十六进制数有一个前导零,后跟字母 “X”,即 0x/0X。请注意,0x/0X 后的数字必须在 0123456789ABCDEF 范围内。

    let hex = 0XA
    let another_hex = 0xA_B_C
    
  • 浮点数字面量是 64 位浮点数。要定义一个浮点数,需要类型注释。

    let double = 3.14 // Double
    let float : Float = 3.14
    let float2 = (3.14 : Float)
    

    64 位浮点数也可以使用十六进制格式定义:

    let hex_double = 0x1.2P3 // (1.0 + 2 / 16) * 2^(+3) == 9
    

当期望的类型已知时,MoonBit 可以自动重载字面量,无需通过字母后缀指定数字的类型:

let int : Int = 42
let uint : UInt = 42
let int64 : Int64 = 42
let double : Double = 42
let float : Float = 42
let bigint : BigInt = 42

字符串#

String 包含一系列 UTF-16 码点。您可以使用双引号创建字符串,或使用 #| 编写多行字符串。

let a = "兔rabbit"
debug_inspect(a.code_unit_at(0).to_char(), content="Some('兔')")
debug_inspect(a.code_unit_at(1).to_char(), content="Some('r')")
let b =
  #| Hello
  #| MoonBit\n
  #|
println(b)
输出#
 Hello
 MoonBit\n

在双引号字符串中,反斜杠后跟某些特殊字符形成转义序列:

转义序列

描述

\n, \r, \t, \b

换行,回车,水平制表符,退格

\\

反斜杠

\u5154 , \u{1F600}

Unicode 转义序列

MoonBit 支持字符串插值。它允许您在插值字符串中替换变量。此功能通过直接将变量值嵌入文本来简化构建动态字符串的过程。用于字符串插值的变量必须实现 Show 特征

let x = 42
println("The answer is \{x}")

备注

插值表达式不能包含换行符、{}"

多行字符串可以使用前缀 #|$| 定义,其中前者将保留原始字符串,后者将执行转义和插值:

let lang = "MoonBit"
let raw =
  #| Hello
  #| ---
  #| \{lang}
  #| ---
let interp =
  $| Hello
  $| ---
  $| \{lang}
  $| ---
println(raw)
println(interp)
输出#
 Hello
 ---
 \{lang}
 ---
 Hello
 ---
 MoonBit
 ---

避免在同一个多行字符串中混用 $|#|;请在整个块中选择一种风格。

VSCode 扩展 包含一个操作,可将粘贴的文档转换为普通的多行字符串,并在纯文本与 MoonBit 多行字符串之间切换。

当期望类型是 String 时,数组字面量被重载为字符串。你可以通过单独列出每个字符来构造String

test {
  let c : Char = '中'
  let s : String = [c, '文']
  inspect(s, content="中文")
}

字符#

Char 表示一个 Unicode 码点。

let a : Char = 'A'
let b = '兔'
let zero = '\u{30}'
let zero = '\u0030'

当期望类型是 IntUInt16 时,字符字面量可以重载为对应类型:

test {
  let s : String = "hello"
  let b : UInt16 = s.code_unit_at(0) // 'h'
  assert_eq(b, 'h') // 'h' 被重载为 UInt16
  let c : Int = '兔'
  // 不可:超出范围
  // let d : UInt16 = '𠮷'
}

字节#

MoonBit 中的字节字面量是一个 ASCII 字符或一个转义序列,语法形式为 b'...'。字节字面量的类型是 Byte。例如:

fn main {
  let b1 : Byte = b'a'
  println(b1.to_int())
  let b2 = b'\xff'
  println(b2.to_int())
}
输出#
97
255

Bytes 是不可修改的字节序列。与字节类似,字节序列字面量的形式是 b"..."。例如:

test {
  let b1 : Bytes = b"abcd"
  let b2 = b"\x61\x62\x63\x64"
  assert_eq(b1, b2)
}

Byte字面量和Bytes字面量也支持转义序列,但是和字符串字面量的转义序列有些不同。下面的表格列出了支持的转义序列:

转义序列

描述

\n, \r, \t, \b

换行,回车,水平制表符,退格

\\

反斜杠

\x41

十六进制转义序列

\o102

八进制转义序列

备注

你可以通过向@buffer.T写入不同类型的数据来构造一个Bytes。例如:

test "buffer 1" {
  let buf : @buffer.Buffer = @buffer.new()
  buf.write_bytes(b"Hello")
  buf.write_byte(b'!')
  assert_eq(buf.contents(), b"Hello!")
}

数组字面量也可以通过指定序列中的每个字节,重载为 Bytes 序列。

test {
  let b : Byte = b'\xFF'
  let bs : Bytes = [b, b'\x01']
  inspect(
    bs,
    content=(
      #|b"\xff\x01"
    ),
  )
}

选择字节容器#

MoonBit 有几种面向字节的容器类型。它们彼此相关,但用途并不相同:

类型

所有权 / 可变性

可否扩容

典型用途

Bytes

拥有所有权,不可变

最终字节负载、API 边界、序列化后的数据

BytesView

借用的不可变视图

在不复制的情况下切片或解析已有字节数据

Array[Byte]

拥有所有权,可变

通用的可变字节存储

FixedArray[Byte]

拥有所有权,可变

定长工作缓冲区

ArrayView[Byte]

借用的数组视图

在不转移所有权的情况下传递基于数组的字节切片

MutArrayView[Byte]

借用的可变视图

原地修改借用的、基于数组的字节存储

@buffer.Buffer

拥有所有权的可变构造器

逐步构造字节数据,最后调用 contents()

有两个常见区别尤其重要:

  • BytesBytesView:前者是拥有所有权的不可变数据,后者是借用的不可变切片。

  • Array[Byte]ArrayView[Byte] / MutArrayView[Byte]:前者是拥有所有权的可变存储,后者是其上的借用只读视图或可变视图。

当你需要显式表达这些约束时,可以使用对应的只读和可变视图类型:ReadOnlyArray[Byte]MutArrayView[Byte]。模式匹配和位串解析同样适用于这些字节容器;参见 数组模式位串模式

元组#

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

fn main {
  fn pack(
    a : Bool,
    b : Int,
    c : String,
    d : Double
  ) -> (Bool, Int, String, Double) {
    (a, b, c, d)
  }

  let quad = pack(false, 100, "text", 3.14)
  let (bool_val, int_val, str, float_val) = quad
  println("\{bool_val} \{int_val} \{str} \{float_val}")
}
输出#
false 100 text 3.14

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

test {
  let t = (1, 2)
  let (x1, y1) = t
  let x2 = t.0
  let y2 = t.1
  assert_eq(x1, x2)
  assert_eq(y1, y2)
}

Ref#

Ref[T] 是一个包含类型 T 的值 val 的可变引用。

可以使用 { val : x } 构造它,并可以使用 ref.val 访问它。有关详细说明,请参见 结构体

let a : Ref[Int] = { val: 100 }

test {
  a.val = 200
  assert_eq(a.val, 200)
  a.val += 1
  assert_eq(a.val, 201)
}

Option 和 Result#

OptionResult 是 MoonBit 中表示可能的错误或失败的最常见类型。

  • Option[T] 表示可能缺失的类型 T 的值。它可以缩写为 T?

  • Result[T, E] 表示类型 T 的值或类型 E 的错误。

有关详细说明,请参见 枚举

test {
  let a : Int? = None
  let b : Option[Int] = Some(42)
  let c : Result[Int, String] = Ok(42)
  let d : Result[Int, String] = Err("error")
  match a {
    Some(_) => assert_true(false)
    None => assert_true(true)
  }
  match d {
    Ok(_) => assert_true(false)
    Err(_) => assert_true(true)
  }
}

数组#

数组是使用方括号 [] 构造的有限值序列,元素之间用逗号 , 分隔。例如:

let numbers = [1, 2, 3, 4]

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

test {
  let numbers = [1, 2, 3, 4]
  let a = numbers[2]
  numbers[3] = 5
  let b = a + numbers[3]
  assert_eq(b, 8)
}

Array[T]FixedArray[T]。视图类型为 ArrayView[T]MutArrayView[T](见下文)。

Array[T] 可以增长,FixedArray[T] 有固定的大小,因此需要使用初始值创建。

警告

一个常见的陷阱是使用相同的初始值创建 FixedArray

test {
  let two_dimension_array = FixedArray::make(10, FixedArray::make(10, 0))
  two_dimension_array[0][5] = 10
  assert_eq(two_dimension_array[5][5], 10)
}

这是因为所有单元格引用相同的对象(在这种情况下是 FixedArray[Int])。应该使用 FixedArray::makei(),它为每个索引创建一个对象。

test {
  let two_dimension_array = FixedArray::makei(10, fn(_i) {
    FixedArray::make(10, 0)
  })
  two_dimension_array[0][5] = 10
  assert_eq(two_dimension_array[5][5], 0)
}

当期望的类型已知时,MoonBit 可以自动重载数组,否则将创建 Array[T]

let fixed_array_1 : FixedArray[Int] = [1, 2, 3]

let fixed_array_2 = ([1, 2, 3] : FixedArray[Int])

let array_3 : Array[Int] = [1, 2, 3] // Array[Int]

ArrayView#

类似于其他语言中的 slice,视图是对集合的特定段的引用。您可以使用 data[start:end] 创建数组 data 的视图,引用从 startend(不包括)的元素。startend 索引都可以省略。

备注

ArrayView 本身是一个不可变的数据结构,但底层的 ArrayFixedArray 可以被修改。需要可变视图时,请使用 data.mut_view(...) 获取 MutArrayView[T]

test {
  let xs = [0, 1, 2, 3, 4, 5]
  let s1 : ArrayView[Int] = xs[2:]
  inspect(s1, content="[2, 3, 4, 5]")
  inspect(xs[:4], content="[0, 1, 2, 3]")
  inspect(xs[2:5], content="[2, 3, 4]")
  inspect(xs[:], content="[0, 1, 2, 3, 4, 5]")
  let mv : MutArrayView[Int] = xs.mut_view(start=1, end=3)
  mv[0] = 99
  inspect(xs[1], content="99")
}

Map#

MoonBit 在其标准库中提供了一个保留插入顺序的哈希映射数据结构,称为 MapMap 可以通过方便的字面量语法创建:

let map : Map[String, Int] = { "x": 1, "y": 2, "z": 3 }

目前,映射字面量语法中的键必须是常量。Map 也可以通过模式匹配优雅地解构,参见 Map 模式

Json#

MoonBit 通过重载字面量支持方便的 json 处理。当表达式的期望类型是 Json 时,数字、字符串、数组和映射字面量可以直接用于创建 json 数据:

let moon_pkg_json_example : Json = {
  "import": ["moonbitlang/core/builtin", "moonbitlang/core/coverage"],
  "test-import": ["moonbitlang/core/random"],
}

Json 值也可以进行模式匹配,参见 Json 模式

重载字面量#

通过重载字面量,你可以使用同一个语法来表示不同类型的值。例如,根据上下文期望的类型,使用1表示一个UInt或者Double。如果一个上下文中不能确定期望的类型,那么它默认为Int

fn expect_double(x : Double) -> Unit {

}

test {
  let x = 1 // type of x is Int
  let y : Double = 1
  expect_double(1)
}

重载字面量可以组合使用。如果一个数组字面量能够被重载为Bytes类型,并且一个数字字面量可以被重载为Byte类型,那么[1,2,3]就可以被重载为Bytes。下面的表格列出了可以重载的字面量和规则:

重载字面量

默认类型

可以被重载为

10, 0xFF, 0o377, 10_000

Int

UInt, Int64, UInt64, Int16, UInt16, Byte, Double, Float, BigInt

"str"

String

'c'

Char

Int

3.14

Double

Float

[a, b, c](字面量 a、b、c 的类型为 E)

Array[E]

FixedArray[E]String(如果 E 的类型是 Char)、Bytes(如果 E 的类型是 Byte)

在模式中也有类似的重载规则,更多细节见 模式匹配

备注

字面量重载不是值转换。如果要将一个变量转换为不同的类型的值,可以使用以to_开头的方法,例如to_int()to_double(),等等。

重载字面量中的转义序列#

转义序列能在重载的"..."'...'字面量中使用。转义序列的解释根据它被重载到的具体类型而定:

  • 简单转义序列

    包括\n\r\t\\\b。这些转义序列在任何"..."'...'字面量中都被支持。在期望的类型是String时,它们被解释为对应的字符;在期望的类型是Bytes时,它们被解释为对应的Byte

  • 字节转义序列

    \x41\o102转义序列表示一个Byte值。支持在重载为BytesByte类型的字面量中使用。

  • Unicode 转义序列

    \u5154\u{1F600}转义序列表示一个Char。支持在重载为StringChar类型的字面量中使用。

函数#

函数接受参数并产生结果。在 MoonBit 中,函数是一等公民,这意味着函数可以是其他函数的参数或返回值。MoonBit 的命名约定要求函数名不应以大写字母(A-Z)开头。请参见下面的 enum 部分中的构造器。

顶层函数#

函数可以定义为顶级或局部。我们可以使用 fn 关键字定义一个顶级函数,它将三个整数相加并返回结果,如下所示:

fn add3(x : Int, y : Int, z : Int) -> Int {
  x + y + z
}

请注意,顶级函数的参数和返回值需要显式类型注释。

顶层函数和方法也可以使用 declare 引入。声明函数只有签名而没有函数体,后续实现必须与该签名匹配。当你想先提供 API 形状、稍后再放置实现时,这很有用。

declare fn declared_add(x : Int, y : Int) -> Int

fn declared_add(x : Int, y : Int) -> Int {
  x + y
}

struct DeclaredCounter(Int)

declare fn DeclaredCounter::value(self : Self) -> Int

fn DeclaredCounter::value(self : Self) -> Int {
  self.0
}

test "declared functions" {
  @test.assert_eq(declared_add(1, 2), 3)
  @test.assert_eq(DeclaredCounter(4).value(), 4)
}

如果声明函数有实现,声明和实现必须在函数名、可见性、类型参数、参数、返回类型和 effect 上保持一致。

局部函数#

局部函数可以是命名的或匿名的。局部函数定义可以省略类型注释:在大多数情况下,它们可以自动推断。例如:

fn local_1() -> Int {
  fn inc(x) { // 命名为 `inc`
    x + 1
  }
  // 匿名,立即应用于整数字面量 6
  (fn(x) { x + inc(2) })(6)
}

test {
  assert_eq(local_1(), 9)
}

MoonBit 为简单的匿名函数提供了一个非常简洁的箭头函数语法:

  [1, 2, 3].eachi((i, x) => println("\{i} => \{x}"))
  // 只有一个参数时可以省略括号
  [1, 2, 3].each(x => println(x * x))

尽管非顶层的函数支持自动推导参数和返回值的类型,只有箭头函数语法允许推导副作用。如果一个 fn 可能 抛出错误进行异步操作,那么这个 fn 必须有显式的 raiseasync 标记。

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

let global_y = 3

fn local_2(x : Int) -> (Int, Int) {
  fn inc() {
    x + 1
  }

  fn four() {
    global_y + 1
  }

  (inc(), four())
}

test {
  assert_eq(local_2(3), (4, 4))
}

局部函数的定义内,可以使用这个函数自己或者之前定义的其他局部函数。如果要定义多个互相递归的局部函数,需要使用 letrec f = .. and g = .. 语法:

  fn f(x) {
    // `f` 可以调用它自己,但不能调用 `g`
    if x > 0 {
      f(x - 1)
    }
  }

  fn g(x) {
    // `g` 可以调用 `f` 和 `g` 自身
    if x < 0 {
      f(-x)
    } else {
      f(x)
    }
  }
  // 互递归的局部函数
  letrec even = x => x == 0 || odd(x - 1)
  and odd = x => x != 0 && even(x - 1)

函数应用#

函数可以应用于括号中的参数列表:

add3(1, 2, 7)

无论 add3 是一个使用名称定义的函数(如前面的示例)还是绑定到函数值的变量,都可以工作,如下所示:

test {
  let add3 = fn(x, y, z) { x + y + z }
  assert_eq(add3(1, 2, 7), 10)
}

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

test {
  let f = fn(x) { x + 1 }
  let g = fn(x) { x + 2 }
  let w = (if true { f } else { g })(3)
  assert_eq(w, 4)
}

部分应用#

部分应用允许函数调用时只提供部分的参数,生成一个接受被余下的参数并返回结果的函数。在 MoonBit 中,通过_操作符可以对函数进行部分应用:

fn add(x : Int, y : Int) -> Int {
  x + y
}

test {
  let add10 : (Int) -> Int = x => add(10, x)
  println(add10(5)) // 输出 15
  println(add10(10)) // 输出 20
}

_操作符表示括号中缺少的参数。部分应用允许在一对括号中使用多个_。例如,Array::fold(_, _, init=5)等价于fn(x, y) { Array::fold(x, y, init=5) }

_操作符也可以在创建枚举值、self.f(args)形式的函数调用和管道中使用。

带标签的参数#

顶层函数可以使用语法 label~ : Type 声明带标签的参数。label 也将作为函数体内的参数名:

fn labelled_1(arg1~ : Int, arg2~ : Int) -> Int {
  arg1 + arg2
}

可以通过语法 label=arg 提供带标签的参数。label=label 可以缩写为 label~

test {
  let arg1 = 1
  assert_eq(labelled_1(arg2=2, arg1~), 3)
}

带标签的函数可以以任何顺序提供。参数的求值顺序与函数声明中参数的顺序相同。

可选参数#

可以通过语法 label? : Type = default_expr 使参数变为可选,其中 default_expr 可以省略。如果在调用处未提供该参数,则将使用默认表达式:

fn optional(opt? : Int = 42) -> Int {
  opt
}

test {
  assert_eq(optional(), 42)
  assert_eq(optional(opt=0), 0)
}

默认表达式每次使用时都会被求值。并且默认表达式中的副作用(如果有)也会被触发。例如:

fn incr(counter? : Ref[Int] = { val: 0 }) -> Ref[Int] {
  counter.val = counter.val + 1
  counter
}

test {
  inspect(incr(), content="{val: 1}")
  inspect(incr(), content="{val: 1}")
  let counter : Ref[Int] = { val: 0 }
  inspect(incr(counter~), content="{val: 1}")
  inspect(incr(counter~), content="{val: 2}")
}

可选参数在调用处就是普通表达式。在 raiseasync 上下文中,你可以传入可能抛错或调用异步函数的表达式:

fn may_fail(x : Int) -> Int raise Failure {
  if x < 0 {
    fail("negative")
  }
  x
}

fn add_with_optional(base : Int, extra? : Int = 1) -> Int {
  base + extra
}

test {
  inspect(add_with_optional(1, extra=may_fail(2)), content="3")
}

对于 async 函数,可选参数表达式同样可以调用异步函数:

///|
async fn fetch_default() -> Int {
  ...
}

///|
async fn build(x? : Int = fetch_default()) -> Int {
  ...
}

///|
async fn use_value() -> Int {
  build(x=fetch_default())
}

如果要在不同的函数调用之间共享默认表达式的结果,可以将默认表达式提升到顶层 let 声明:

let default_counter : Ref[Int] = { val: 0 }

fn incr_2(counter? : Ref[Int] = default_counter) -> Int {
  counter.val = counter.val + 1
  counter.val
}

test {
  assert_eq(incr_2(), 1)
  assert_eq(incr_2(), 2)
}

默认表达式可以依赖于之前的参数,例如:

fn create_rectangle(a : Int, b? : Int = a) -> (Int, Int) {
  (a, b)
}

test {
  inspect(create_rectangle(10), content="(10, 10)")
}

没有默认值的可选参数#

当用户未提供值时,具有不同语义的情况是很常见的。没有默认值的可选参数的类型为 T?,默认值为 None。在直接提供这种可选参数时,MoonBit 将自动用 Some 包装该值:

fn new_image(width? : Int, height? : Int) -> Image {
  if width is Some(w) {
    ...
  }
  ...
}

let img2 : Image = new_image(width=1920, height=1080)

有时,直接传递类型为 T? 的值也很有用,例如在转发可选参数时。MoonBit 为此提供了一个语法 label?=value,并且 label?label?=label 的缩写:

fn image(width? : Int, height? : Int) -> Image {
  ...
}

fn fixed_width_image(height? : Int) -> Image {
  image(width=1920, height?)
}

自动填充参数#

MoonBit 支持在不同的调用位置自动填充特定类型的参数,例如函数调用的源位置。要声明一个自动填充参数,只需声明一个带标签的参数,并添加一个函数属性 #callsite(autofill(param_a, param_b))。现在,如果未显式提供参数,MoonBit 将在调用时自动填充它。

目前 MoonBit 支持两种类型的自动填充参数,SourceLoc,它是整个函数调用的源位置,以及 ArgsLoc,它是一个数组,包含每个参数的源位置(如果有):

#callsite(autofill(loc, args_loc))
fn f(_x : Int, loc~ : SourceLoc, args_loc~ : ArgsLoc) -> String {
  (
    $|loc of whole function call: \{loc}
    $|loc of arguments: \{args_loc}
  )
  // 整个函数调用的位置:<filename>:7:3-7:10
  // 参数的位置:[Some(<filename>:7:5-7:6), Some(<filename>:7:8-7:9), None, None]
}

自动填充参数非常有用,用于编写调试和测试工具。

函数别名#

MoonBit 允许用户用别名来调用一个函数。声明函数别名的语法如下:

#alias(g)
#alias(h, visibility="pub")
fn k() -> Bool {
  true
}

你也可以通过字段 visibility 创建具有不同可见性的函数别名。

控制结构#

条件表达式#

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

if x == y {
  expr1
} else if x == z {
  expr2
} else {
  expr3
}

结果周围的大括号是必需的。

请注意,条件表达式在 MoonBit 中始终返回一个值,结果和 else 子句的返回值必须是相同的类型。以下是一个示例:

let initial = if size < 1 { 1 } else { size }

else 子句只有在返回值的类型为 Unit的时候省略。

匹配表达式#

match 表达式类似于条件表达式,但它使用 模式匹配 来决定要评估哪个结果,并同时提取变量。

fn decide_sport(weather : String, humidity : Int) -> String {
  match weather {
    "sunny" => "tennis"
    "rainy" => if humidity > 80 { "swimming" } else { "football" }
    _ => "unknown"
  }
}

test {
  assert_eq(decide_sport("sunny", 0), "tennis")
}

如果省略了可能的条件,编译器将发出警告;如果真的出现该情况,程序将终止。

卫语句#

guard 语句用于检查指定的不变量。如果不变量的条件得到满足,程序将继续执行后续语句。如果条件不满足(即为假),则执行 else 块中的代码且返回其值(并跳过后续语句)。

fn guarded_get(array : Array[Int], index : Int) -> Int? {
  guard index >= 0 && index < array.length() else { None }
  Some(array[index])
}

test {
  debug_inspect(guarded_get([1, 2, 3], -1), content="None")
}

卫语句与 is 表达式#

let 语句可以与 模式匹配 一起使用。但是,let 语句只能处理一种情况。使用 is 表达式 配合卫语句可以解决这个问题。

在以下示例中,getProcessedText 假设输入的 path 指向的资源都是纯文本,并使用 guard 语句来在确保这个不变量的同时,解构出纯文本资源。与使用 match 语句相比,text 的后续处理可以少一级缩进。

enum Resource {
  Folder(Array[String])
  PlainText(String)
  JsonConfig(Json)
}

fn getProcessedText(
  resources : Map[String, Resource],
  path : String,
) -> String raise Error {
  guard resources.get(path) is Some(resource) else { fail("\{path} not found") }
  guard resource is PlainText(text) else { fail("\{path} is not plain text") }
  process(text)
}

如果省略了 else 部分,程序将在 guard 语句中指定的条件不为真或无法匹配时终止。

guard condition  // <=> guard condition else { panic() }
guard expr is Some(x)
// <=> guard expr is Some(x) else { _ => panic() }

While 循环#

在 MoonBit 中,while 循环可用于在条件为真时重复执行一段代码块。在执行代码块之前,将评估条件。使用 while 关键字定义 while 循环,后跟条件和循环体。循环体是一系列语句。只要条件为真,就会执行循环体。

fn main {
  let mut i = 5
  while i > 0 {
    println(i)
    i = i - 1
  }
}
输出#
5
4
3
2
1

循环体支持 breakcontinue。使用 break 可以退出当前循环,而使用 continue 则跳过当前迭代的剩余部分并继续下一次迭代。

fn main {
  let mut i = 5
  while i > 0 {
    i = i - 1
    if i == 4 {
      continue
    }
    if i == 1 {
      break
    }
    println(i)
  }
}
输出#
3
2

while 循环还支持可选的 nobreak 子句。当循环条件变为假时,将执行 nobreak 子句,然后循环将结束。

fn main {
  let mut i = 2
  while i > 0 {
    println(i)
    i = i - 1
  } nobreak {
    println(i)
  }
}
输出#
2
1
0

当有 nobreak 子句时,while 循环还可以返回一个值。返回值是 nobreak 子句的评估结果。在这种情况下,如果使用 break 退出循环,需要在 break 后提供一个返回值,该返回值应与 nobreak 子句的返回值类型相同。

fn main {
  let mut i = 10
  let r = while i > 0 {
    i = i - 1
    if i % 2 == 0 {
      break 5
    }
  } nobreak {
    7
  }
  println(r)
}
输出#
5
fn main {
  let mut i = 10
  let r = while i > 0 {
    i = i - 1
  } nobreak {
    7
  }
  println(r)
}
输出#
7

For 循环#

MoonBit 还支持 C 风格的 For 循环。关键字 for 后跟由分号分隔的变量初始化子句、循环条件和更新子句。它们不需要用括号括起来。例如,下面的代码创建了一个新的变量绑定 i,它在整个循环中都有作用域且是不可变的。这使得编写清晰的代码并对其进行推理更容易:

fn main {
  for i = 0; i < 5; i = i + 1 {
    println(i)
  }
}
输出#
0
1
2
3
4

变量初始化子句可以创建多个绑定:

for i = 0, j = 0; i + j < 100; i = i + 1, j = j + 1 {
  println(i)
}

应该注意,在更新子句中,当有多个绑定变量时,语义是同时更新它们。换句话说,在上面的示例中,更新子句不会按顺序执行 i = i + 1j = j + 1,而是同时递增 ij。因此,在更新子句中读取绑定变量的值时,总是会得到上一次迭代中更新的值。

变量初始化子句、循环条件和更新子句都是可选的。例如,以下两个是无限循环:

for i = 1; ; i = i + 1 {
  println(i)
}
for ;; {
  println("loop forever")
}

for 循环还支持 continuebreaknobreak 子句。与 while 循环一样,for 循环也可以使用 breaknobreak 子句返回一个值。

continue 语句跳过当前 for 循环的剩余部分(包括更新子句)并继续下一次迭代。continue 语句还可以更新 for 循环的绑定变量,只要后面跟着与绑定变量数量匹配的表达式,用逗号分隔。

例如,以下程序计算从 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
    }
  } nobreak {
    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 进行遍历。有关 Iter 类型的更多信息,请参见下面的 迭代器

for .. in 循环还支持遍历整数序列,例如:

test {
  let mut i = 0
  for j in 0..<10 {
    i += j
  }
  assert_eq(i, 45)
  let mut k = 0
  for l in 0..<=10 {
    k += l
  }
  assert_eq(k, 55)
}

除了单个值的序列外,MoonBit 还支持通过 MoonBit 标准库中的 Iter2 类型遍历两个值的序列,例如 Map。任何具有方法 .iter2() : Iter2[A, B] 的类型都可以使用两个循环变量的 for .. in 进行遍历:

for k, v in { "x": 1, "y": 2, "z": 3 } {
  println(k)
  println(v)
}

另一个使用两个循环变量的 for .. in 的示例是在遍历数组时跟踪数组索引:

fn main {
  for index, elem in [4, 5, 6] {
    let i = index + 1
    println("The \{i}-th element of the array is \{elem}")
  }
}
输出#
The 1-th element of the array is 4
The 2-th element of the array is 5
The 3-th element of the array is 6

for .. in 循环的主体支持诸如 returnbreak 和错误处理等控制流操作:

fn main {
  let map = { "x": 1, "y": 2, "z": 3, "w": 4 }
  for k, v in map {
    if k == "y" {
      continue
    }
    println("\{k}, \{v}")
    if k == "z" {
      break
    }
  }
}
输出#
x, 1
z, 3

如果循环变量未使用,可以使用 _ 忽略它。

范围表达式与 for .. in 循环#

for .. in 循环还能和范围表达式配合,用于遍历整数区间:

fn main {
  for x in 0..<5 {
    println(x)
  }
}
输出#
0
1
2
3
4

for .. in 循环中一共有四种可用的范围表达式:

  • a..<b:升序地从 a 遍历到 b(不包含 b

  • a..<=b:升序地从 a 遍历到 b(包含 b

  • a>..b:降序地从 a 遍历到 b(不包含 a

  • a>=..b:降序地从 a 遍历到 b(包含 a

带标记的 Continue/Break#

当一个循环被标记的时候,它可以从循环中的 break 或者 continue 中引用,例如:

test "break label" {
  let mut count = 0
  let xs = [1, 2, 3]
  let ys = [4, 5, 6]
  let res = outer~: for i in xs {
    for j in ys {
      count = count + i
      break outer~ j
    }
  } nobreak {
    -1
  }
  assert_eq(res, 4)
  assert_eq(count, 1)
}

test "continue label" {
  let mut count = 0
  let init = 10
  let res = outer~: for i = init {
    if i == 0 {
      break outer~ 42
    }
    for ;; {
      count = count + 1
      continue outer~ i - 1
    }
  }
  assert_eq(res, 42)
  assert_eq(count, 10)
}

defer 表达式#

defer 表达式可以实现可靠的资源释放。defer 的语法如下

defer <expr>
<body>

当程序离开 body 时,expr 里的内容会被执行。例如,下面的程序:

  defer println("释放资源")
  println("使用资源")

会先输出 使用资源,然后输出 释放资源。无论 body 以何种方式退出,defer 表达式都会被执行。defer 能够处理 错误,以及 return/break/continue 等控制流构造。

连续的 defer 会以倒序执行。例如,下面的程序:

  defer println("第一处 defer")
  defer println("第二处 defer")
  println("做些事情")

会先输出 做些事情,然后输出 第二处 defer,最后输出 第一处 defer

defer 右边的表达式里,不能使用 return/break/continue。目前,在 defer 右边的表达式里也不能抛出错误或调用 async 函数。

迭代器#

迭代器是一个对象,它在遍历序列的同时提供对其元素的访问。传统的面向对象语言如 Java 的 Iterator<T> 使用 next()hasNext() 来遍历迭代过程,而函数式语言(JavaScript 的 forEach,Lisp 的 mapcar)提供了一个高阶函数,该函数接受一个操作和一个序列,然后使用该操作应用于序列。前者称为外部迭代器(对用户可见),后者称为内部迭代器(对用户不可见)。

内置类型 Iter[T] 是 MoonBit 的外部迭代器实现。它提供 next() 来拉取下一个值:返回 Some(value) 并推进迭代器,或在迭代结束时返回 None。几乎所有内置的顺序数据结构都已经实现了 Iter

///|
fn filter_even(l : Array[Int]) -> Array[Int] {
  let l_iter : Iter[Int] = l.iter()
  l_iter.filter(x => (x & 1) == 0).collect()
}

///|
fn fact(n : Int) -> Int {
  let start = 1
  let range : Iter[Int] = start.until(n)
  range.fold(Int::mul, init=start)
}

常用的方法包括:

  • each: 遍历迭代器中的每个元素,对每个元素应用某个函数。

  • fold: 使用给定的函数,从给定的初始值开始,对迭代器的元素进行“折叠”。

  • collect: 将迭代器的元素收集到一个数组中。

  • filter: (惰性)根据谓词函数过滤迭代器的元素。

  • map: (惰性)使用映射函数转换迭代器的元素。

  • concat: (惰性)通过将第二个迭代器的元素附加到第一个迭代器,将两个迭代器合并为一个。

filtermap 这样的方法在序列对象(例如 Array)上非常常见。但是,Iter 的不同之处在于,任何构造新 Iter 的方法都是惰性的(即在调用时不会开始迭代,因为它被包装在一个函数内),因此不会为中间值分配内存。这就是使 Iter 优于遍历序列的原因:没有额外的成本。MoonBit 鼓励用户将 Iter 传递给函数,而不是传递序列对象本身。

预定义的序列结构如 Array 及其迭代器应该足够使用。但是,为了在自定义序列(元素类型为 S)中使用这些方法,我们需要实现 Iter,即返回 Iter[S] 的函数。以 Bytes 为例:

///|
fn iter(data : Bytes) -> Iter[Byte] {
  let mut index = 0
  Iter::new(fn() -> Byte? {
    if index < data.length() {
      let byte = data[index]
      index += 1
      Some(byte)
    } else {
      None
    }
  })
}

迭代器是单次遍历的:一旦调用 next() 或使用 eachfoldcollect 等方法消耗它们,其内部状态就会推进,无法重置。如果需要再次遍历序列,请从源头重新获取一个 Iter

自定义数据类型#

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

结构体#

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

struct User {
  id : Int
  name : String
  mut email : String
}
fn main {
  let u = User::{ id: 0, name: "John Doe", email: "john@doe.com" }
  u.email = "john@doe.name"
  //! u.id = 10
  println(u.id)
  println(u.name)
  println(u.email)
}
输出#
0
John Doe
john@doe.name

使用简写构造结构体#

如果已经有一些变量,如 nameemail,在构造结构体时重复这些名称是多余的。可以使用简写,它的行为完全相同:

let name = "john"
let email = "john@doe.com"
let u = User::{ id: 0, name, email }

如果没有其他具有相同字段的结构体,在构造结构体时添加结构体的名称是多余的:

let u2 = { id: 0, name, email }

结构体更新语法#

可以用这个语法来根据现有结构体创建一个新的结构体,但只更新部分字段。

fn main {
  let user = { id: 0, name: "John Doe", email: "john@doe.com" }
  let updated_user = { ..user, email: "john@doe.name" }
  println(
    (
      $|{ id: \{user.id}, name: \{user.name}, email: \{user.email} }
      $|{ id: \{updated_user.id}, name: \{updated_user.name}, email: \{updated_user.email} }
    ),
  )
}
输出#
{ id: 0, name: John Doe, email: john@doe.com }
{ id: 0, name: John Doe, email: john@doe.name }

给结构体自定义构造器#

MoonBit 还支持为每个 struct 类型定义自定义构造器。构造器是一个特殊方法,可以通过结构体名调用来创建值。首先像往常一样定义结构体:

struct IntBox {
  value : Int
} derive(Debug)

随后应将构造器实现为一个与结构体类型同名的方法。其返回值必须是该结构体本身:

fn IntBox::IntBox(value : Int) -> IntBox {
  { value, }
}

当一个结构体定义了构造器,它就可以直接通过类型名字被构造:

  let box = IntBox(10)
  debug_inspect(box, content="{ value: 10 }")

构造器调用遵循构造器方法的签名,因此无标签参数可以写成熟悉的 TypeName(value) 形式。

构造器也可以像普通函数一样使用带标签参数和可选参数:

struct StructWithConstr {
  x : Int
  y : Int
} derive(Debug)
fn StructWithConstr::StructWithConstr(x~ : Int, y? : Int = x) -> StructWithConstr {
  { x, y }
}
  let s = StructWithConstr(x=1)
  debug_inspect(s, content="{ x: 1, y: 1 }")

由于结构体构造器由普通函数实现,因此它们也可以抛出错误:

suberror BuildError {
  NegativeInput
} derive(Debug)

struct Positive {
  value : Int
} derive(Debug)
fn Positive::Positive(x : Int) -> Positive raise BuildError {
  guard x >= 0 else { raise NegativeInput }
  { value: x }
}
  debug_inspect(try? Positive(10), content="Ok({ value: 10 })")
  debug_inspect(try? Positive(-1), content="Err(NegativeInput)")

异步构造器使用 async fn TypeName::TypeName 声明,并且可以在异步代码中使用:

struct AsyncBox {
  value : Int
} derive(Debug)
async fn AsyncBox::AsyncBox(x : Int) -> AsyncBox {
  @async.sleep(0)
  { value: x }
}
async test "struct constructor async" {
  let box = AsyncBox(10)
  debug_inspect(box, content="{ value: 10 }")
}

通过 struct 构造器创建值与 枚举构造器 具有完全相同的语义,区别在于 struct 构造器不能用于模式匹配。例如,通过构造器创建来自外部包的 struct 时,如果表达式的期望类型已知,就可以省略包名。

由于 struct 构造器由普通函数实现,因此它们可以 抛出错误进行异步操作struct 构造器也支持 可选参数。可选参数的默认值写在构造器实现上,就像普通函数签名一样。

枚举#

枚举类型类似于函数式语言中的代数数据类型。熟悉 C/C++ 的用户可能更喜欢称其为标记联合。

枚举可以有一组情况(构造函数)。构造函数的名称必须以大写字母开头。可以使用这些名称来构造枚举的相应情况,或在模式匹配中检查枚举值属于哪个分支:

/// 一个枚举类型,表示两个值之间的顺序关系,
/// 有三种情况 "Smaller"、"Greater" 和 "Equal"
enum Relation {
  Smaller
  Greater
  Equal
}
/// 比较两个整数之间的顺序关系
fn compare_int(x : Int, y : Int) -> Relation {
  if x < y {
    // 当创建一个枚举时,如果目标类型已知,
    // 可以直接写构造函数名称
    Smaller
  } else if x > y {
    // 但是当目标类型未知时,
    // 你总是可以使用 `TypeName::Constructor` 来创建一个枚举
    Relation::Greater
  } else {
    Equal
  }
}

/// 输出一个类型为 `Relation` 的值
fn print_relation(r : Relation) -> Unit {
  // 使用模式匹配来决定 `r` 属于哪种情况
  match r {
    // 在模式匹配期间,如果类型已知,
    // 写构造函数的名称就足够了
    Smaller => println("smaller!")
    // 但是你也可以在模式匹配中使用 `TypeName::Constructor` 语法
    Relation::Greater => println("greater!")
    Equal => println("equal!")
  }
}
fn main {
  print_relation(compare_int(0, 1))
  print_relation(compare_int(1, 1))
  print_relation(compare_int(2, 1))
}
输出#
smaller!
equal!
greater!

枚举情况也可以携带额外数据。以下是使用枚举定义整数列表类型的示例:

enum Lst {
  Nil
  // 构造函数 `Cons` 携带额外的数据:列表的第一个元素,
  // 和列表的其余部分
  Cons(Int, Lst)
}
// 除了将额外数据绑定到变量之外,
// 你还可以继续匹配构造函数内部的额外数据。
// 以下是一个函数,用于判断列表是否只包含一个元素
fn is_singleton(l : Lst) -> Bool {
  match l {
    // 此分支仅匹配形状为 `Cons(_, Nil)` 的值,
    // 即长度为 1 的列表
    Cons(_, Nil) => true
    // 使用 `_` 匹配其他所有情况
    _ => false
  }
}

fn print_list(l : Lst) -> Unit {
  // 在模式匹配带有额外数据的枚举时,
  // 除了决定值属于哪种情况
  // 你还可以提取该情况内部的额外数据
  match l {
    Nil => println("nil")
    // 这里 `x` 和 `xs` 定义了新变量
    // 而不是引用现有变量,
    // 如果 `l` 是一个 `Cons`,那么 `Cons` 的额外数据
    // (第一个元素和列表的其余部分)
    // 将绑定到 `x` 和 `xs
    Cons(x, xs) => {
      println("\{x},")
      print_list(xs)
    }
  }
}
fn main {
  // 使用 `Cons` 创建值时,必须提供 `Cons` 的额外数据
  let l : Lst = Cons(1, Cons(2, Nil))
  println(is_singleton(l))
  print_list(l)
}
输出#
false
1,
2,
nil

构造器与带标签参数#

枚举构造器可以有带标签的参数:

enum E {
  // `x` 和 `y` 是有标签参数
  C(x~ : Int, y~ : Int)
}
// 使用有标签参数的构造函数进行模式匹配
fn f(e : E) -> Unit {
  match e {
    // `label=pattern`
    C(x=0, y=0) => println("0!")
    // `x~` 是 `x=x` 的缩写
    // 未匹配的有标签参数可以通过 `..` 省略
    C(x~, ..) => println(x)
  }
}
fn main {
  f(C(x=0, y=0))
  let x = 0
  f(C(x~, y=1)) // <=> C(x=x, y=1)
}
输出#
0!
0

也可以像在模式匹配中访问结构体字段一样访问构造函数的有标签参数:

enum Object {
  Point(x~ : Double, y~ : Double)
  Circle(x~ : Double, y~ : Double, radius~ : Double)
}

suberror NotImplementedError derive(Debug)

fn Objecct::distance_with(
  self : Object,
  other : Object,
) -> Double raise NotImplementedError {
  match (self, other) {
    // 对于通过 `Point(..) as p` 定义的变量,
    // 编译器知道它必须是构造函数 `Point`,
    // 因此可以通过 `p.x`、`p.y` 等直接访问 `Point` 的字段。
    (Point(_) as p1, Point(_) as p2) => {
      let dx = p2.x - p1.x
      let dy = p2.y - p1.y
      (dx * dx + dy * dy).sqrt()
    }
    (Point(_), Circle(_)) | (Circle(_), Point(_)) | (Circle(_), Circle(_)) =>
      raise NotImplementedError
  }
}
fn main {
  let p1 : Object = Point(x=0, y=0)
  let p2 : Object = Point(x=3, y=4)
  let c1 : Object = Circle(x=0, y=0, radius=2)
  try {
    println(p1.distance_with(p2))
    println(p1.distance_with(c1))
  } catch {
    _ => println("NotImplementedError")
  }
}
输出#
5
NotImplementedError

构造器与可变字段#

也可以为构造器定义可变字段。这对于定义命令式数据结构特别有用:

// 使用可变二叉搜索树实现的集合。
struct Set[X] {
  mut root : Tree[X]
}

fn[X : Compare] Set::insert(self : Set[X], x : X) -> Unit {
  self.root = self.root.insert(x, parent=Nil)
}

// 带有亲指针的可变二叉搜索树
enum Tree[X] {
  Nil
  // 只有带标签的参数可以是可变的
  Node(
    mut value~ : X,
    mut left~ : Tree[X],
    mut right~ : Tree[X],
    mut parent~ : Tree[X]
  )
}

// 将一个新元素插入到二叉搜索树中。
// 返回新的树
fn[X : Compare] Tree::insert(
  self : Tree[X],
  x : X,
  parent~ : Tree[X],
) -> Tree[X] {
  match self {
    Nil => Node(value=x, left=Nil, right=Nil, parent~)
    Node(_) as node => {
      let order = x.compare(node.value)
      if order == 0 {
        // 修改构造器的字段
        node.value = x
      } else if order < 0 {
        // 在这里创建的 `node` 和 `node.left` 之间的循环
        node.left = node.left.insert(x, parent=node)
      } else {
        node.right = node.right.insert(x, parent=node)
      }
      // 树不为空,所以新的树就是原来的树
      node
    }
  }
}

Extensible enum#

An extenum defines an open enum type. Unlike a regular enum, an extenum can receive more constructors later, including from another package. This is useful when a package wants to define the shared event, message, or extension-point type, while other packages contribute their own cases.

pub(all) extenum LogEvent[T] {
  Info(T)
}

Use extenum Type += { ... } to add constructors to an extensible enum in the same package:

pub(all) extenum LogEvent[T] += {
  Warning(T)
  Critical(T, T)
}

To extend an extensible enum from another package, qualify the target type with the package that defines the type:

pub(all) extenum @base.LogEvent[T] += {
  Debug(T)
}

Extensible enum constructors are qualified by the package that defines the constructor. For constructors from the current package, use the constructor name directly when the expected type is known. For constructors from another package, use @pkg.Constructor in expressions and patterns.

When a package imports both the base package and an extension package, values from both packages have the same extensible enum type:

pub fn describe(event : @base.LogEvent[String]) -> String {
  match event {
    @base.Info(message) => "info: \{message}"
    @base.Warning(message) => "warning: \{message}"
    @base.Critical(code, message) => "critical \{code}: \{message}"
    @plugin.Debug(message) => "debug: \{message}"
    _ => "unknown"
  }
}

pub fn debug_event(message : String) -> @base.LogEvent[String] {
  @plugin.Debug(message)
}

Pattern matching must include a wildcard branch, because more constructors can be added outside the current declaration.

Only extenum declarations can be extended. Regular enum declarations are closed.

元组结构体#

MoonBit 支持一种特殊的结构体称为元组结构体:

struct UserId(Int)

struct UserInfo(UserId, String)

元组结构体类似于只有一个构造函数的枚举(与元组结构体本身的名称相同)。因此,可以使用构造函数创建或使用模式匹配提取底层表示:

fn main {
  let id : UserId = UserId(1)
  let name : UserInfo = UserInfo(id, "John Doe")
  let UserId(uid) = id // uid : Int
  let UserInfo(_, uname) = name // uname: String
  println(uid)
  println(uname)
}
输出#
1
John Doe

除了模式匹配之外,还可以使用索引访问元素,类似于元组:

fn main {
  let id : UserId = UserId(1)
  let info : UserInfo = UserInfo(id, "John Doe")
  let uid : Int = id.0
  let uname : String = info.1
  println(uid)
  println(uname)
}
输出#
1
John Doe

类型别名#

MoonBit 支持使用语法 type NewType = OldType 定义类型别名:

警告

旧语法 typealias OldType as NewType 可能会在将来被移除。

pub type Index = Int
pub type MyIndex = Int
pub type MyMap = Map[Int, String]

与上面所有其他类型声明不同,类型别名不定义新类型,它只是一个行为与其定义完全相同的类型宏。因此,例如,不能为类型别名定义新方法或实现特征。

小技巧

类型别名可用于执行增量代码重构。

例如,如果要将类型 T@pkgA 移动到 @pkgB,可以在 @pkgA 中留下一个类型别名 type T = @pkgB.T

本地类型#

MoonBit 支持在顶层函数的顶部声明结构体/枚举,这些类型仅在当前顶层函数中可见。这些本地类型可以使用顶层函数的泛型参数,但不能引入额外的泛型参数。本地类型可以使用 derive 派生方法,但不能手动定义额外的方法。例如:

fn[T : Debug] toplevel(x : T) -> Unit {
  enum LocalEnum {
    A(T)
    B(Int)
  } derive(Debug)
  struct LocalStruct {
    a : (String, T)
  } derive(Debug)
  struct LocalStructTuple(T) derive(Debug)
  ...
}

目前,本地类型不支持声明为错误类型。

模式匹配#

模式匹配允许我们匹配特定模式并从数据结构中绑定数据。

简单模式#

我们可以将表达式与以下内容进行模式匹配:

  • 字面量,例如布尔值、数字、字符、字符串等

  • 常量

  • 结构体

  • 枚举

  • 数组

  • 键值对

  • JSON

等等。我们可以定义标识符来绑定匹配的值,以便稍后使用。

const ONE = 1

fn match_int(x : Int) -> Unit {
  match x {
    0 => println("zero")
    ONE => println("one")
    value => println(value)
  }
}

我们可以使用 _ 作为我们不关心的值的通配符,并使用 .. 忽略结构体或枚举的剩余字段,或数组(参见 [数组模式](#array-pattern))。

struct Point3D {
  x : Int
  y : Int
  z : Int
}

fn match_point3D(p : Point3D) -> Unit {
  match p {
    { x: 0, .. } => println("on yz-plane")
    _ => println("not on yz-plane")
  }
}

enum Point[T] {
  Point2D(Int, Int, name~ : String, payload~ : T)
}

fn[T] match_point(p : Point[T]) -> Unit {
  match p {
    //! Point2D(0, 0) => println("2D origin")
    Point2D(0, 0, ..) => println("2D origin")
    Point2D(_) => println("2D point")
    _ => panic()
  }
}

我们可以使用 as 为某些模式命名,可以使用 | 一次匹配多个情况。在单个模式中,变量名只能绑定一次,并且在 | 模式的两侧应绑定相同的变量集。

match expr {
  //! Add(e1, e2) | Lit(e1) => ...
  Lit(n) as a => ...
  Add(e1, e2) | Mul(e1, e2) => ...
  ...
}

数组模式#

数组模式可以用来匹配以下类型以获取其对应的元素或视图(View):

类型

元素

视图

Array[T], ArrayView[T], FixedArray[T]

T

ArrayView[T]

Bytes, BytesView

字节

BytesView

String, StringView

字符

StringView

数组模式可以有以下形式:

  • []:匹配空数组

  • [pa, pb, pc]:匹配长度为 3 的数组,并将其中的元素分别绑定到 pa, pb, pc

  • [pa, ..rest, pb]:匹配至少有两个元素的数组,并将第一个元素绑定到pa,最后一个元素绑定到 pb,其余元素绑定到 rest。如果不需要其余元素,可以省略绑定 rest。在 .. 部分前后允许任意数量的元素。由于 .. 可以匹配不确定数量的元素,因此在数组模式中最多只能出现一次。

test {
  let ary = [1, 2, 3, 4]
  if ary is [a, b, .. rest] && a == 1 && b == 2 && rest.length() == 2 {
    inspect("a = \{a}, b = \{b}", content="a = 1, b = 2")
  } else {
    fail("")
  }
  guard ary is [.., a, b] else { fail("") }
  inspect("a = \{a}, b = \{b}", content="a = 3, b = 4")
}

数组模式提供了一种 Unicode 安全的方式来操作字符串,这意味着它在访问元素的时候不会跨越代码单元边界。例如,我们可以检查一个包含 Unicode 的字符串是否是回文:

test {
  fn palindrome(s : String) -> Bool {
    for view = s.view() {
      match view {
        [] | [_] => break true
        [a, .. rest, b] => if a == b { continue rest } else { break false }
      }
    }
  }

  inspect(palindrome("abba"), content="true")
  inspect(palindrome("中b中"), content="true")
  inspect(palindrome("文bb中"), content="false")
}

当数组模式中有连续的字符或字节常量时,可以使用模式展开 .. 运算符将它们组合起来,使代码看起来更整洁。在这种情况下,.. 后跟字符串或字节常量匹配确切数量的元素,因此它可以在数组模式中多次使用。

const NO : Bytes = b"no"

test {
  fn match_string(s : String) -> Bool {
    match s {
      [.. "yes", ..] => true // equivalent to ['y', 'e', 's', ..]
    }
  }

  fn match_bytes(b : Bytes) -> Bool {
    match b {
      [.. NO, ..] => false // equivalent to ['n', 'o', ..]
    }
  }
}

位串模式#

位串模式可用于匹配字节容器中的打包比特字段。它们支持 BytesViewBytesArray[Byte]FixedArray[Byte]ReadOnlyArray[Byte]ArrayView[Byte]。使用带 be/le 后缀的显式位宽以明确端序。be 支持 1..64 位;le 仅支持按字节对齐的宽度(8 * n),因为小端只对字节序有明确意义。没有 .. 时,模式必须消费整个视图。

test {
  let packet : Bytes = b"\xD2\x10\x7F"
  let header : BytesView = packet[0:2]
  let (flag, kind, version, length) = match header {
    [u1be(flag), u3be(kind), u4be(version), u8be(length)] =>
      (flag, kind, version, length)
    _ => fail("bad header")
  }
  assert_eq(flag, 1)
  assert_eq(kind, 0b101)
  assert_eq(version, 0b0010)
  assert_eq(length, 16)
}

使用字面量位模式验证头部,并用 .. 捕获剩余数据以供下一步解析。

test {
  let data : Bytes = b"\xF1\xAA\xBB"
  let view : BytesView = data[0:]
  let tag = match view {
    [u4be(0b1111), u4be(tag), .. rest] => {
      assert_eq(rest, b"\xAA\xBB"[0:])
      tag
    }
    _ => fail("bad prefix")
  }
  assert_eq(tag, 0b0001)
}

常见字节容器示例(注意 MutArrayView 需要切片):

test {
  let b : Bytes = b"\x80"
  guard b is [u1be(1), ..] else { fail("Bytes") }

  let a : Array[Byte] = [b'\x80']
  guard a is [u1be(1), ..] else { fail("Array[Byte]") }

  let f : FixedArray[Byte] = [b'\x80']
  guard f is [u1be(1), ..] else { fail("FixedArray[Byte]") }

  let r : ReadOnlyArray[Byte] = [b'\x80']
  guard r is [u1be(1), ..] else { fail("ReadOnlyArray[Byte]") }

  let v : ArrayView[Byte] = a[:]
  guard v is [u1be(1), ..] else { fail("ArrayView[Byte]") }

  let mv : MutArrayView[Byte] = a.mut_view()
  guard mv[:] is [u1be(1), ..] else { fail("MutArrayView[Byte]") }
}

有符号模式使用二进制补码语义。例如,u1be 产生 01,而 i1be 产生 0-1

test {
  let bytes = b"\x80"
  let u : UInt = match bytes[:] {
    [u1be(u), ..] => u
    _ => fail("u1be")
  }
  let i : Int = match bytes[:] {
    [i1be(i), ..] => i
    _ => fail("i1be")
  }
  assert_eq(u, 1U)
  assert_eq(i, -1)
}

结果类型取决于位宽:

位宽

结果类型

1..32 位(u/i

UInt / Int

33..64 位(u

UInt64

33..64 位(i

Int64

范围模式#

对于内置整数类型和 Char,MoonBit 允许匹配值是否落在特定范围内。

范围模式的形式为 a..<ba..=b,其中 ..< 表示上限是排他的,..= 表示包含上限。ab 可以是以下之一:

  • 字面量

  • 使用 const 声明的常量

  • _,表示此模式在此侧没有限制

以下是一些示例:

const Zero = 0

fn sign(x : Int) -> Int {
  match x {
    _..<Zero => -1
    Zero => 0
    1..<_ => 1
  }
}

fn classify_char(c : Char) -> String {
  match c {
    'a'..='z' => "lowercase"
    'A'..='Z' => "uppercase"
    '0'..='9' => "digit"
    _ => "other"
  }
}

Map 模式#

MoonBit 允许在类似 map 的数据结构上方便地进行匹配。在 map 模式内,key : value 语法将在 map 中存在 key 时匹配,并将 key 的值与模式 value 匹配。key? : value 语法将无论 key 是否存在都匹配,value 将与 map[key](一个可选项)匹配。

match map {
  // 仅在 `map` 中存在 "b" 时匹配
  { "b": _, .. } => ...
  // 仅在 `map` 中不存在 "b" 且 "a" 存在于 `map` 时匹配。
  // 匹配时,将 `map` 中的 "a" 的值绑定到 `x`
  { "b"? : None, "a": x, .. } => ...
  // 编译器报告缺失的情况:{ "b"? : None, "a"? : None }
}
  • 要使用 map 模式匹配数据类型 TT 必须具有某种类型 KV 的方法 get(Self, K) -> Option[V](请参见 方法和特征)。

  • 目前,map 模式的键部分必须是字面量或常量

  • Map 模式始终是开放的:未匹配的键会被静默忽略,并且需要添加 .. 以显示这一点

  • Map 模式将编译为高效的代码:每个键最多只会被获取一次

Json 模式#

当匹配的值具有类型 Json 时,可以直接使用字面量模式,以及构造函数:

match json {
  { "version": "1.0.0", "import": [..] as imports, .. } => ...
  { "version": Number(i, ..), "import": Array(imports), .. } => ...
  ...
}

守卫条件#

模式匹配表达式中的每个分支都可以有一个守卫条件。守卫条件是一个布尔表达式,只有当该条件为真时,对应的分支才会被匹配。如果守卫条件为假,则跳过该分支并尝试下一个分支。例如:

fn guard_cond(x : Int?) -> Int {
  fn f(x : Int) -> Array[Int] {
    [x, x + 42]
  }

  match x {
    Some(a) if f(a) is [0, b] => a + b
    Some(b) => b
    None => -1
  }
}

test {
  assert_eq(guard_cond(None), -1)
  assert_eq(guard_cond(Some(0)), 42)
  assert_eq(guard_cond(Some(1)), 1)
}

注意,在检查所有模式是否都被匹配表达式覆盖时,不会考虑守卫条件。因此,您会看到以下情况的警告:

fn guard_check(x : Int?) -> Unit {
  match x {
    Some(a) if a >= 0 => ()
    Some(a) if a < 0 => ()
    None => ()
  }
}

警告

不鼓励在守卫条件中调用可能通过副作用改变被匹配的值的函数。在这种情况下,被改变的部分不会在后续模式中重新求值。请谨慎使用。

泛型#

泛型在顶层函数和数据类型定义中受支持。可以在方括号内引入类型参数。我们可以重写上述数据类型 List,添加类型参数 T 以获得列表的通用版本。然后,我们可以定义列表上的通用函数,如 mapreduce

///|
enum List[T] {
  Nil
  Cons(T, List[T])
}

///|
fn[S, T] List::map(self : List[S], f : (S) -> T) -> List[T] {
  match self {
    Nil => Nil
    Cons(x, xs) => Cons(f(x), xs.map(f))
  }
}

///|
fn[S, T] List::reduce(self : List[S], op : (T, S) -> T, init : T) -> T {
  match self {
    Nil => init
    Cons(x, xs) => xs.reduce(op, op(init, x))
  }
}

特殊语法#

管道#

MoonBit 提供了方便的管道语法 x |> f(y)f <| x,可以用来串联常规函数调用,或让嵌套的构建器风格代码更易读:

5 |> ignore // <=> ignore(5)
[] |> Array::push(5) // <=> Array::push([], 5)
1
|> add(5) // <=> add(1, 5)
|> x => { x + 1 }
|> ignore // <=> ignore(add(1, 5))

MoonBit 代码遵循数据优先风格,也就是说,函数将它的“主题”放置在第一个参数的位置。所以,管道默认将左侧的值填入右侧的函数调用的第一个参数的位置。例如 x |> f(y)等价于f(x,y)

你也可以使用_操作符改变x在函数f的调用中的插入位置,例如x |> f(y, _), 这等价于f(y,x)。带标签的参数也是支持的。

管道操作符也可以连接到箭头函数。通过管道传入箭头函数时,函数体必须用花括号包裹,例如 value |> x => { x + 1 }

反向管道运算符会把右侧表达式作为左侧调用的最后一个参数。例如,f <| x 等价于 f(x),而 f(a, b) <| c 等价于 f(a, b, c)。这对于 DSL 风格的代码尤其有用,因为像 div([text("hello")]) 这样的嵌套调用可以改写为 div <| [text <| "hello"]

let page = div <| [
    text <| "hello",
    section("toolbar") <| fn() { [text <| "save", text <| "cancel"] },
  ]
inspect(
  page,
  content="div(text(hello), toolbar: div(text(save), text(cancel)))",
)

因为反向管道会附加最后一个参数,所以它也很适合最后一个参数是 lambda 的函数,从而可以写出类似 section("toolbar") <| fn () { ... } 这样的尾随 lambda 风格。

级联运算符#

级联运算符..用于连续对同一值执行一系列可变操作。语法如下:

let arr = []..append([1])

这里 x..f() 等价于 { x.f(); x }

考虑以下情况:对于具有诸如write_stringwrite_charwrite_object等方法的StringBuilder类型,我们经常需要对同一StringBuilder值执行一系列操作:

let builder = StringBuilder::new()
builder.write_char('a')
builder.write_char('a')
builder.write_object(1001)
builder.write_string("abcdef")
let result = builder.to_string()

为了避免重复输入builder,其方法通常设计为返回self本身,允许使用.运算符链接操作。为了区分不可变和可变操作,在 MoonBit 中,对于所有返回Unit的方法,可以使用级联运算符进行连续操作,而无需修改方法的返回类型。

let result = StringBuilder::new()
  ..write_char('a')
  ..write_char('a')
  ..write_object(1001)
  ..write_string("abcdef")
  .to_string()

is 表达式#

is 表达式测试值是否符合特定模式。它返回一个 Bool 值,并可以在期望布尔值的任何地方使用,例如:

fn[T] is_none(x : T?) -> Bool {
  x is None
}

fn start_with_lower_letter(s : String) -> Bool {
  s is ['a'..='z', ..]
}

通过 is 表达式绑定的标识符可以在以下的上下文中使用:

  1. 在与表达式(&&)中:左侧表达式中绑定的标识符可以在右侧表达式中使用

    fn f(x : Int?) -> Bool {
      x is Some(v) && v >= 0
    }
    
  2. if 的第一个分支中:如果条件是一系列布尔表达式 e1 && e2 && ...,则可以在条件为真的分支中使用 is 表达式中绑定的标识符。

    fn g(x : Array[Int?]) -> Unit {
      if x is [v, .. rest] && v is Some(i) && i is (0..=10) {
        debug(v)
        println(i)
        debug(rest)
      }
    }
    
  3. 下面举一个在 guard 中使用的情况:

    fn h(x : Int?) -> Unit {
      guard x is Some(v)
      println(v)
    }
    
  4. while 循环中的使用:

    fn i(x : Int?) -> Unit {
      let mut m = x
      while m is Some(v) {
        println(v)
        m = None
      }
    }
    

is 表达式只能接受一个简单模式,如果你需要通过 as 把模式绑定到某个变量上,需要加括号。比如:

fn j(x : Int) -> Int? {
  Some(x)
}

fn init {
  guard j(42) is (Some(a) as b)
  println(a)
  debug(b)
}

正则字面量表达式#

re"..." 是一个正则字面量表达式,其类型为 Regex

正则字面量是普通表达式,因此可以存入局部绑定、作为参数传递、用作默认参数值,也可以定义为常量:

let r : Regex = re"a(b+)"
const IDENT_START : Regex = re"[A-Za-z_]"
const IDENT : Regex = IDENT_START + re"[A-Za-z0-9_]*"

Regex 值也可以通过 + 组合成序列,通过 | 组合成备选。在需要正则常量表达式的地方,例如 =~,可以直接引用由正则字面量定义的具名 const

与普通字符串字面量不同,正则字面量中的反斜杠不需要二次转义。例如,应写 re"/\*",而不是 re"/\\*"

const REGEX_IDENT_START = re"[A-Za-z_]"

const REGEX_IDENT_CONT = re"[A-Za-z0-9_]*"

const REGEX_AB : Regex = re"a" + re"b"

fn regex_default_arg(re? : Regex = re"abc") -> Bool {
  re.execute("zabc") is Some(_)
}

test {
  let regex : Regex = re"a(b+)"
  assert_true(regex.execute("abbb") is Some(_))
  assert_true(regex.execute("ac") is None)

  assert_true(REGEX_AB.execute("ab") is Some(_))
  assert_true(REGEX_AB.execute("ac") is None)
  assert_true(regex_default_arg())
}

非法的正则字面量会在编译期被拒绝。

正则字面量使用 MoonBit 的正则语法,支持的形式包括:

  • 字面字符:普通字符匹配其自身

  • 通配符:. 匹配任意单个字符,包括换行符

  • 字符类:[abc][^abc][a-z]

  • 字符类中的 POSIX 类:[[:digit:]][[:alpha:]][[:space:]][[:word:]][[:xdigit:]]

  • 量词:*+?{n}{n,}{n,m}

  • 非贪婪量词:*?+???{n}?{n,}?{n,m}?

  • 分组与分支:( ... )(?: ... )(?<name> ... )a|b

  • 断言:^$\b\B

  • 作用域修饰符:(?i: ... ) 用于大小写不敏感匹配

转义规则按正则语义处理,而不是按字符串语义处理。常见转义包括 \n\r\t\f\v,以及 \.\( 这类元字符转义,还有 Unicode 转义 \uXXXX / \u{X...}。如果要匹配字面量 {,请使用 [{],不要写成 \{。这是为了给未来在正则字面量中支持插值预留语法空间,因为 \{ 会和插值语法冲突。

还有一些重要语义和限制:

  • ^$ 是非多行锚点:只匹配整个输入的开头和结尾

  • \b\B 当前在正则字面量作为一等 Regex 值使用时可用;但在 =~ 这样的 regex match expression 常量上下文中暂不可用,未来这一限制预计会放宽。

  • POSIX 字符类基于 ASCII

  • 不支持 \d\D\s\S\w\W。请改用 [[:digit:]][^[:digit:]][[:space:]][^[:space:]][[:word:]][^[:word:]]

  • re"..." 不支持 \xHH 字节转义;请改用 Unicode 转义或直接写普通字符。

  • 不支持前瞻、后顾、反向引用和字符类集合运算

  • 在字符类中,- 用于表示范围。若要匹配字面量连字符,请写 \-;不支持把 - 放在字符类开头或结尾来表示字面量。

具名捕获组(例如 (?<id>[0-9]+))属于 Regex 值本身。它们可与 Regex::executeMatchResult::named_group 等 API 配合使用,但不会自行引入 MoonBit 绑定变量。

当正则字面量作为一等 Regex 值使用时,Regex::execute 等操作采用 first-match 语义:它们返回从搜索位置开始找到的第一个匹配,不提供 longest-match 模式。

正则匹配表达式#

正则匹配表达式使用 =~ 运算符,以正则常量表达式在 StringView 中进行搜索。这是一种较新的正则匹配形式,旨在取代实验性的 lexmatch。该表达式返回 Bool

input =~ re"abc"
input =~ ((PREFIX + SUFFIX) as whole, before=head, after=tail)
input =~ (re"b", before~, after~)

右侧必须是正则常量表达式:可以是 re"abc" 这样的正则字面量、具名 const,或者由常量通过 +(拼接)、|(分支)和括号构造出来的表达式。不允许任意运行时值。

使用 as 绑定匹配到的子串。使用 beforeafter 将未匹配的前缀和后缀绑定为 StringViewbefore~after~ 是简写,分别绑定名为 beforeafter 的变量。

这和正则的具名捕获组是两回事。例如在 re"(?<id>[0-9]+)" 中,名称 id 属于正则引擎的捕获元数据,不是 MoonBit 绑定变量。如果你需要在 =~ 中引入绑定,请使用 as,例如 (re"(?<id>[0-9]+)" as digits)

is 一样,=~ 引入的绑定可以用于相同的布尔流上下文,例如 && 的右侧和 if 的 true 分支。正则匹配默认按搜索语义工作,因此 "zabc!" =~ re"abc" 的结果是 true。如果需要限制匹配必须发生在输入的开头或结尾,请使用 ^$ 等锚点。

=~ 同样采用 first-match 语义。它在设计上不会支持 longest-match 行为。

test {
  let input = " let_name = 42 "
  if (input =~ (
      (REGEX_IDENT_START + REGEX_IDENT_CONT) as ident,
      before=head,
      after=tail
    )) {
    assert_true(head is " ")
    assert_true(ident is "let_name")
    assert_true(tail is " = 42 ")
  } else {
    fail("expected identifier")
  }

  if ("abc" =~ (re"b", before~, after~)) {
    assert_true(before is "a")
    assert_true(after is "c")
  } else {
    fail("expected middle match")
  }

  let source : StringView = "abc"
  if (source =~ (re"." as ch, after=rest)) {
    assert_eq(ch, 'a')
    assert_true(rest is "bc")
  } else {
    fail("expected leading char")
  }

  assert_true("zabc!" =~ re"abc")
  assert_true(!("zabc!" =~ re"^abc"))
}

上面的例子中,headidenttailbeforeafterrest 的类型都是 StringView。绑定变量 ch 的类型是 Char,因为 re"." 恰好匹配一个字符。

Lexmatch#

警告

lexmatchlexmatch? 已弃用。新代码请优先使用 regex match expression。本节仅作为现有代码的参考保留。

lexmatch 用正则模式匹配 String,并让你绑定匹配到的各个片段。搜索模式的写法是 (before, regex pieces, after),其中 beforeafter 是可选的绑定,用于捕获未匹配的前缀和后缀,并用逗号分隔;中间的正则片段只用空白分隔。正则本身由一串字符串字面量组成,因此可以拆成多行或在各部分之间插入注释。你也可以用 as 绑定子模式,例如 ("b*" as b)

lexmatch? 是类似 is 的布尔检查,并且可以在与 is 表达式相同的上下文中引入绑定。

在旧代码中,搜索模式的 lexmatch 写法如下:

lexmatch text {
  (before, "a" ("b*" as b) "c", after) => ...
  _ => ...
}

if text lexmatch? ("a" ("b*" as b) "c") && b.length() > 0 {
  ...
}

在新代码中,请改用 =~ 编写这些搜索模式检查。

lexmatch 还支持类似词法器的模式:lexmatch <expr> with longest,它会在备选项中选择最长匹配(例如在 longest 模式下 if|[a-z]* 会把 iff 匹配为 iff,而首个匹配的搜索模式会先匹配到 if)。正则匹配表达式不提供这种最长匹配模式。

正则字面量支持把 \\b\\B 作为正则语法的一部分使用,但这些单词边界断言目前在 regex match expression 的常量上下文中还不可用。当正则作为一等 Regex 值使用时,它们是可用的,未来这一限制预计会放宽。另外,正则字面量也不支持 \\d\\D\\s\\S\\w\\W。请改用字符类中的 POSIX 字符类,例如 [[:digit:]]

test {
  let text = "xxabbbcyy"
  if text =~ (re"a" + (re"b*" as b) + re"c", before~, after~) {
    inspect(before, content="xx")
    inspect(b, content="bbb")
    inspect(after, content="yy")
  } else {
    fail("")
  }

  if text =~ (re"a" + (re"b*" as b) + re"c") && b.length() > 0 {
    inspect(b, content="bbb")
  }

  let keyword = "iff"
  lexmatch keyword with longest {
    ("if|[a-z]*" as ident) => inspect(ident, content="iff")
    _ => fail("")
  }
}

展开运算符#

MoonBit 提供了一个展开运算符,用于在使用数组字面量语法构造 ArrayStringBytes 时展开元素序列。要展开这样的序列,需要在其前面加上 .. 前缀,并且该序列必须具有 iter() 方法,并且该方法能够产生相应类型的元素。

例如,我们可以使用展开运算符构造一个数组:

test {
  let a1 : Array[Int] = [1, 2, 3]
  let a2 : FixedArray[Int] = [4, 5, 6]
  let a3 : @list.List[Int] = @list.from_array([7, 8, 9])
  let a : Array[Int] = [..a1, ..a2, ..a3, 10]
  inspect(a, content="[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]")
}

同样,我们可以使用展开运算符构造一个字符串:

test {
  let s1 : String = "Hello"
  let s2 : StringView = "World".view()
  let s3 : Array[Char] = [..s1, ' ', ..s2, '!']
  let s : String = [..s1, ' ', ..s2, '!', ..s3]
  inspect(s, content="Hello World!Hello World!")
}

最后一个例子展示了如何使用展开运算符构造一个字节序列。

test {
  let b1 : Bytes = b"hello"
  let b2 : BytesView = b1[1:4]
  let b : Bytes = [..b1, ..b2, 10]
  inspect(
    b,
    content=(
      #|b"helloell\x0a"
    ),
  )
}

TODO 语法#

todo语法 (...) 是一种特殊构造,用于标记尚未实现或用于未来功能的占位符代码段。例如:

fn todo_in_func() -> Int {
  ...
}