基础#

内置数据结构#

布尔值#

MoonBit 有一个内置的布尔类型,它有两个值:truefalse。布尔类型用于条件表达式和控制结构。

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

数字#

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

类型

描述

示例

Int16

16-bit signed integer

(42 : Int16)

Int

32 位有符号整数

42

Int64

64 位有符号整数

1000L

UInt16

16-bit unsigned integer

(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"
println(a[0])
println(a[1])
let b =
  #| Hello
  #| MoonBit\n
  #|
println(b)
输出#
'兔'
'r'
 Hello
 MoonBit\n

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

转义序列

描述

\n,\r,\t,\b

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

\\

反斜杠

\x41

十六进制转义序列

\o102

八进制转义序列

\u5154,\u{1F600}

Unicode 转义序列

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

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

多行字符串默认不支持插值,但您可以通过将前导 #| 更改为 $| 来为特定行启用插值:

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

 ---

字符#

Char 表示一个 Unicode 码点。

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

字节#

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)
}

元组#

元组是使用圆括号 () 构造的有限值集合,元素之间用逗号 , 分隔。元素的顺序很重要;例如,(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 : Option[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]

  • 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 = [1, 2, 3] // Array[Int]

ArrayView#

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

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]")
}

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 模式

函数#

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

顶层函数#

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

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

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

局部函数#

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

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

test {
  assert_eq!(local_1(), 9)
}

还有一种称为矩阵函数的形式,它使用模式匹配

let extract : (Int?, Int) -> Int = fn {
  Some(x), _ => x
  None, default => default
}

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

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))
}

函数应用#

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

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)
}

带标签的参数#

顶层函数可以使用语法 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 提供默认值,使带标签的参数可以省略。如果在调用时未提供此参数,则将使用默认表达式:

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}")
}

如果要在不同的函数调用之间共享默认表达式的结果,可以将默认表达式提升到顶层 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 sub_array[X](
  xs : Array[X],
  offset~ : Int,
  len~ : Int = xs.length() - offset
) -> Array[X] {
  xs[offset:offset + len].iter().to_array()
}

test {
  assert_eq!(sub_array([1, 2, 3], offset=1), [2, 3])
  assert_eq!(sub_array([1, 2, 3], offset=1, len=1), [2])
}

在提供可选参数时自动插入 Some#

通常可选参数的类型为 T?,默认值为 None。在这种情况下,显式传递参数需要包装一个 Some,这很丑:

fn ugly_constructor(width~ : Int? = None, height~ : Int? = None) -> Image {
  ...
}

let img : Image = ugly_constructor(width=Some(1920), height=Some(1080))

幸运的是,MoonBit 提供了一种特殊类型的可选参数来解决这个问题。使用 label? : T 声明的可选参数的类型为 T?,默认值为 None。在直接提供这种可选参数时,MoonBit 将自动插入 Some

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

let img2 : Image = nice_constructor(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 支持在调用时自动填充特定类型的参数,例如函数调用的源位置。要声明一个自动填充参数,只需将一个可选参数声明为 _ 作为默认值。现在,如果未显式提供参数,MoonBit 将在调用时自动填充它。

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

fn f(_x : Int, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> String {
  $|loc of whole function call: \{loc}
  $|loc of arguments: \{args_loc}
  // loc of whole function call: <filename>:7:3-7:10
  // loc of arguments: [Some(<filename>:7:5-7:6), Some(<filename>:7:8-7:9), None, None]
}

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

控制结构#

条件表达式#

条件表达式由条件、结果和可选的 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 {
  inspect!(guarded_get([1, 2, 3], -1), content="None")
}

卫语句与赋值绑定#

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

在以下示例中,getProcessedText 假设输入的 path 指向的资源都是纯文本,并使用 guard 语句来确保这个不变量。与使用 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 语句中指定的条件不为真或无法匹配时终止。

guard condition  // <=> guard condition else { panic() }
guard let Some(x) = expr
// <=> guard let Some(x) = expr 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 循环还支持可选的 else 子句。当循环条件变为假时,将执行 else 子句,然后循环将结束。

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

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

fn main {
  let mut i = 10
  let r = while i > 0 {
    i = i - 1
    if i % 2 == 0 {
      break 5
    }
  } else {
    7
  }
  println(r)
}
输出#
5
fn main {
  let mut i = 10
  let r = while i > 0 {
    i = i - 1
  } else {
    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 循环还支持 continuebreakelse 子句。与 while 循环一样,for 循环也可以使用 breakelse 子句返回一个值。

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
    }
  } 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 进行遍历。有关 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

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

函数式循环#

函数式循环是 MoonBit 中的一个强大功能,它使您可以以函数式风格编写循环。

函数式循环接收参数并返回一个值。它使用 loop 关键字定义,后跟其参数和循环体。循环体是一系列子句,每个子句由模式和表达式组成。与输入匹配的模式的子句将会被执行,并且循环将返回表达式的值。如果没有对应的模式,程序会中止。使用 continue 关键字和参数进入循环的下一次迭代。使用 break 关键字和参数从循环中返回一个值。如果值是循环体中的最后一个表达式,则可以省略 break 关键字。

test {
  fn sum(xs : @immut/list.T[Int]) -> Int {
    loop xs, 0 {
      Nil, acc => break acc // <=> Nil, acc => acc
      Cons(x, rest), acc => continue rest, x + acc
    }
  }

  assert_eq!(sum(Cons(1, Cons(2, Cons(3, Nil)))), 6)
}

警告

目前在 loop exprs { ... } 中,exprs 是非空列表,而 for { ... } 用于无限循环。

Labelled Continue/Break#

When a loop is labelled, it can be referenced from a break or continue from within a nested loop. For example:

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
    }
  } else {
    -1
  }
  assert_eq!(res, 4)
  assert_eq!(count, 1)
}

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

迭代器#

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

内置类型 Iter[T] 是 MoonBit 的内部迭代器实现。几乎所有内置的顺序数据结构都已经实现了 Iter

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

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

常用的方法包括:

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

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

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

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

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

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

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

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

///|
fn iter(data : Bytes) -> Iter[Byte] {
  Iter::new(fn(visit : (Byte) -> IterResult) -> IterResult {
    for byte in data {
      guard let IterContinue = visit(byte) else { x => break x }

    } else {
      IterContinue
    }
  })
}

几乎所有 Iter 实现都与 Bytes 的实现相同,唯一的主要区别是实际执行迭代的代码块。

实现细节#

类型 Iter[T] 基本上是 ((T) -> IterResult) -> IterResult 的类型别名,它是一个高阶函数,接受一个操作,IterResult 是一个枚举对象,用于跟踪当前迭代的状态,包含以下 2 种状态:

  • IterEnd: 标记迭代结束

  • IterContinue: 标记迭代结束尚未到达,暗示迭代将在此状态继续。

简单来说,Iter[T] 接受一个函数 (T) -> IterResult 并使用它将 Iter[T] 本身转换为类型为 IterResult 的新状态。

迭代器提供了一种统一的遍历数据结构的方式,它们基本上可以无成本地构建:只要 fn(yield) 不执行,迭代过程就不会开始。

Iter::run() 在在内部触发迭代。链接各种 Iter 方法可能看起来很美观,但请注意抽象层下面的繁重工作。

因此,与外部迭代器不同,一旦迭代开始,除非到达末尾,否则无法停止。诸如 count() 这样的方法,它计算迭代器中元素的数量看起来像是一个 O(1) 操作,但实际上具有线性时间复杂度。请谨慎使用迭代器,否则可能会出现性能问题。

自定义数据类型#

创建新数据类型有两种方法: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 }

枚举#

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

fn print_list(l : List) -> 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 : List = 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)
}

type! NotImplementedError  derive(Show)

fn distance_with(self : Object, other : Object) -> Double!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 {
    e => println(e)
  }
}
输出#
5
NotImplementedError

构造器与可变字段#

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

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

fn Set::insert[X : Compare](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 Tree::insert[X : Compare](
  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
    }
  }
}

Newtype#

MoonBit 支持一种特殊的枚举称为 newtype:

// `UserId` 是一个与 `Int` 不同的全新类型,
// 你可以为 `UserId` 定义新方法等。
// 但与此同时,`UserId` 的内部表示
// 与 `Int` 完全相同
type UserId Int

type UserName String

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

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

除了模式匹配,还可以使用 ._ 提取 newtype 的内部表示:

fn main {
  let id : UserId = UserId(1)
  let uid : Int = id._
  println(uid)
}
输出#
1

类型别名#

MoonBit 支持使用语法 typealias Name = TargetType 定义类型别名:

pub typealias Index = Int

// 类型别名默认为私有
typealias MapString[X] = Map[String, X]

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

小技巧

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

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

本地类型#

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

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

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

模式匹配#

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

简单模式#

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

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

  • 常量

  • 结构体

  • 枚举

  • 数组

  • 键值对

  • JSON

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

const ONE = 1

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

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

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 match_point[T](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) => ...
  _ => ...
}

数组模式#

对于 ArrayFixedArrayArrayView,MoonBit 允许使用数组模式。

数组模式有以下形式:

  • []:匹配空数据结构

  • [pa, pb, pc]:匹配已知数量的元素,在此示例中为 3 个

  • [pa, ..]:匹配已知数量的元素,后跟未知数量的元素

  • [.., pa]:匹配已知数量的元素,前面是未知数量的元素

test {
  let ary = [1, 2, 3, 4]
  let [a, b, ..] = ary
  inspect!("a = \{a}, b = \{b}", content="a = 1, b = 2")
  let [.., a, b] = ary
  inspect!("a = \{a}, b = \{b}", content="a = 3, b = 4")
}

范围模式#

对于内置整数类型和 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 的方法 op_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)} => ...
  _ => ...
}

泛型#

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

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

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

fn 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提供了一个方便的管道运算符|>,可以用于链接常规函数调用:

5 |> ignore // <=> ignore(5)
[] |> push(5) // <=> push([], 5)
1
|> add(5) // <=> add(1, 5)
|> ignore // <=> ignore(add(1, 5))

级联运算符#

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

x..f()

x..f()..g() 等价于 {x.f(); x.g(); 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()

TODO 语法#

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

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