MoonBit:新手之旅#

这一文档针对不熟悉语言的新手用户所写,并不打算作为一个几分钟就能读完的小文章。本文希望为那些对 MoonBit 的编程思路 (更加现代化,函数式的) 不甚了解的用户提供一个简洁而不失易懂性的指南。

如果您想直接深入了解语言,请参阅总体介绍

安装#

语言扩展

目前,MoonBit 的开发支持是通过 VS Code 扩展实现的。请前往VS Code Marketplace下载 MoonBit 语言支持。

工具链

(推荐) 如果您已安装了上面的扩展,运行操作菜单中的 “Install moonbit toolchain” 即可直接安装运行时并跳过这部分介绍:安装运行时

我们还提供了一个安装脚本:Linux 和 macOS 用户可以通过以下方式安装:

curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash

对于 Windows 用户,使用 Powershell:

Set-ExecutionPolicy RemoteSigned -Scope CurrentUser; irm https://cli.moonbitlang.com/install/powershell.ps1 | iex

这将自动安装 MoonBit 到 $HOME/.moon 并将其添加到您的 PATH

如果安装后遇到 moon 未找到的情况,请尝试重新启动终端或 VSCode 以使环境变量生效。

请注意,目前 MoonBit 还不适用于生产环境:它正在积极开发中。要更新 MoonBit,只需再次运行上面的命令即可。

运行 moon help 可以看到一堆子命令。但是现在我们只需要 build runnew 这三个命令。

要创建一个项目(或模块,更正式地说),运行 moon new。您将看到一个创建向导。

如果您选择创建一个 exec 模式项目,您将得到:

my-project
├── LICENSE
├── moon.mod.json
├── README.md
└── src
    ├── lib
    │   ├── hello.mbt
    │   ├── hello_test.mbt
    │   └── moon.pkg.json
    └── main
        ├── main.mbt
        └── moon.pkg.json

其中包含一个 main lib,其中包含一个 fn main,作为程序的入口。尝试运行 moon run src/main

如果你选择创建一个 lib 模式项目,你将得到:

my-project
├── LICENSE
├── moon.mod.json
├── README.md
└── src
    ├── lib
    │   ├── hello.mbt
    │   ├── hello_test.mbt
    │   └── moon.pkg.json
    ├── moon.pkg.json
    └── top.mbt

在本教程中,我们将使用 lib 模式项目,并假设项目名称为 examine

示例:找到通过考试的人#

在这个例子中,我们将尝试找出,给定一些学生的分数,有多少人通过了考试?

为此,我们将从定义数据类型开始,确定我们的函数,并编写我们的测试。然后我们将实现我们的函数。

除非另有说明,以下内容将在文件 src/top.mbt 中定义。

数据类型#

MoonBit 中的基本数据类型包括以下内容:

  • Unit

  • Bool

  • Int, UInt, Int64, UInt64, Byte, …

  • Float, Double

  • Char, String, …

  • Array[T], …

  • 元组,和其他类型

要使用原始类型表示包含学生 ID 和分数的记录,我们可以使用一个包含学生 ID (类型为 String)和分数(类型为 Double)的 2-元组,如 (String, Double)。然而,这并不是很直观,因为我们无法区分其他可能的数据类型,例如包含学生 ID 和学生身高的记录。

因此,我们选择使用 结构体 声明我们自己的数据类型:

struct Student {
  id : String
  score : Double
}

一个人可以通过或者不通过考试,因此判断结果可以使用 枚举类型 定义:

enum ExamResult {
  Pass
  Fail
}

函数#

函数 是一段代码,它接受一些输入并产生一个结果。

在我们的例子中,我们需要判断一个学生是否通过了考试:

fn is_qualified(student : Student, criteria: Double) -> ExamResult {
  ...
}

这个函数接受一个类型为 Student 的输入 student,一个类型为 Double 的输入 criteria(因为每门课程的标准可能不同,每个国家的标准可能不同)然后返回一个 ExamResult

使用 ... 语法可以让我们暂时不实现函数。

我们还需要找出有多少学生通过了考试:

fn count_qualified_students(
  students : Array[Student],
  is_qualified : (Student) -> ExamResult
) -> Int {
  ...
}

在 MoonBit 中,函数是一等公民,这意味着我们可以将一个函数绑定到一个变量,将一个函数作为参数传递或将一个函数作为结果接收。这个函数接受一个学生记录数组和另一个函数,判断学生是否通过了考试。

编写测试#

我们可以定义内联测试来定义函数的预期行为。这也有助于确保在重构程序时不会出现回归(破坏现有行为)。

test "is qualified" {
  assert_eq!(is_qualified(Student::{ id : "0", score : 50.0 }, 60.0), Fail)
  assert_eq!(is_qualified(Student::{ id : "1", score : 60.0 }, 60.0), Pass)
  assert_eq!(is_qualified(Student::{ id : "2", score : 13.0 }, 7.0), Pass)
}

我们会收到报错信息,提醒我们 ExamResult 没有实现 ShowEq

ShowEqtraits。在 MoonBit 中,trait 定义了一个类型应该能够执行的一些常见操作。

例如,Eq 定义了应该有一种方法来比较两个相同类型的值,这个方法叫做 op_equal

trait Eq {
  op_equal(Self, Self) -> Bool
}

Show 定义了应该有一种方法,要么将一个类型的值转换为 String,要么使用 Logger 写入:

trait Show {
  output(Self, &Logger) -> Unit
  to_string(Self) -> String
}

assert_eq 使用它们来约束传递的参数,以便比较两个值并在它们不相等时打印它们:

fn assert_eq![A : Eq + Show](value : A, other : A) -> Unit {
  ...
}

我们需要为我们的 ExamResult 实现 EqShow。有两种方法可以实现。

  1. 通过定义一个显式实现:

    impl Eq for ExamResult with op_equal(self, other) {
      match (self, other) {
        (Pass, Pass) | (Fail, Fail) => true
        _ => false
      }
    }
    

    在这里,我们使用 模式匹配 来检查 ExamResult 的情况。

  2. 另一种方法是通过 派生 ,因为 EqShow内置 traits 并且 ExamResult 的输出非常直接:

    enum ExamResult {
      Pass
      Fail
    } derive(Show)
    

现在我们已经实现了 traits,我们可以继续实现我们的测试:

test "count qualified students" {
  let students = [
    { id: "0", score: 10.0 },
    { id: "1", score: 50.0 },
    { id: "2", score: 61.0 },
  ]
  let criteria1 = fn(student) { is_qualified(student, 10) }
  let criteria2 = fn(student) { is_qualified(student, 50) }
  assert_eq!(count_qualified_students(students, criteria1), 3)
  assert_eq!(count_qualified_students(students, criteria2), 2)
}

在这里,我们使用 lambda 表达式 来重用先前定义的 is_qualified 来创建不同的标准。

我们可以运行 moon test 来查看测试是否成功。

实现函数#

对于 is_qualified 函数,只需要简单的比较:

fn is_qualified(student : Student, criteria : Double) -> ExamResult {
  if student.score >= criteria {
    Pass
  } else {
    Fail
  }
}

在 MoonBit 中,最后一个表达式的结果是函数的返回值,每个分支的结果是 if 表达式的值。

对于 count_qualified_students 函数,我们需要遍历数组,检查每个学生是否通过。

一个简单的版本是使用一个可变值和一个 for 循环

fn count_qualified_students(
  students : Array[Student],
  is_qualified : (Student) -> ExamResult
) -> Int {
  let mut count = 0
  for i = 0; i < students.length(); i = i + 1 {
    if is_qualified(students[i]) == Pass {
      count += 1
    }
  }
  count
}

然而,这既不高效(由于边界检查)也不直观,所以我们可以用 for .. in 循环 替换 for loop:

fn count_qualified_students(
  students : Array[Student],
  is_qualified : (Student) -> ExamResult
) -> Int {
  let mut count = 0
  for student in students {
    if is_qualified(student) == Pass { count += 1}
  }
  count
}

还有另一种方法是使用为 迭代器

fn count_qualified_students(
  students : Array[Student],
  is_qualified : (Student) -> ExamResult
) -> Int {
  students.iter().filter(fn(student) { is_qualified(student) == Pass }).count()
}

现在之前定义的测试应该通过了。

公开库#

恭喜您完成了第一个 MoonBit 库!

您现在可以与其他开发人员分享它,这样他们就不需要重复您所做的工作。

但在此之前,您还有一些其他事情要做。

调整可见性#

为了看到其他人如何使用我们的程序,MoonBit 提供了一种称为 “黑盒测试” 的机制。

让我们将上面定义的 test 块移动到一个新文件 src/top_test.mbt 中。

糟糕!现在有错误抱怨:

  • is_qualifiedcount_qualified_students 未绑定

  • FailPass 未定义

  • Student 不是一个记录类型,字段 id 未找到,等等。

所有这些问题都来自于可见性问题。默认情况下,定义的函数对当前包(由当前文件夹绑定)之外的程序的其他部分不可见。默认情况下,类型被视为抽象类型,即我们只知道存在类型 Student 和类型 ExamResult。通过使用黑盒测试,您可以确保您希望其他人拥有的一切确实被赋予了预期的可见性。

为了让其他人使用这些函数,我们需要在 fn 前添加 pub 使函数公开。

为了让其他人构造类型和读取内容,我们需要在 structenum 前添加 pub(all) 使类型公开。

我们还需要稍微修改 count qualified students 的测试,添加类型注释:

test "count qualified students" {
  let students: Array[@examine.Student] = [
    { id: "0", score: 10.0 },
    { id: "1", score: 50.0 },
    { id: "2", score: 61.0 },
  ]
  let criteria1 = fn(student) { @examine.is_qualified(student, 10) }
  let criteria2 = fn(student) { @examine.is_qualified(student, 50) }
  assert_eq!(@examine.count_qualified_students(students, criteria1), 3)
  assert_eq!(@examine.count_qualified_students(students, criteria2), 2)
}

请注意,我们使用 @examine 访问类型和函数,这是您的包的名称。这是其他人使用您的包的方式,但您可以在黑盒测试中省略它们。

现在,编译应该可以正常工作,测试应该再次通过。

发布库#

现在您已经准备好了,您可以将这个项目发布到 mooncakes.io,MoonBit 的模块注册中心。您也可以在那里找到其他有趣的项目。

  1. 执行 moon login 并按照说明使用现有的 GitHub 账户创建您的账户。

  2. moon.mod.json 中修改项目名称为 <您的 GitHub 账户名>/<项目名称>。运行 moon check 查看 moon.pkg.json 中是否有其他受影响的地方。

  3. 执行 moon publish,您就完成了。您的项目将可供他人使用。

默认情况下,项目将在 Apache 2.0 下共享,这是一种宽松的许可证,允许每个人使用。您还可以通过更改 moon.mod.json 中的 license 字段和 LICENSE 的内容,使用其他许可证,例如 MulanPSL 2.0

结束语#

到目前为止,我们已经了解了 MoonBit 的基本特性和一些不那么简单的特性,然而 MoonBit 是一个功能丰富的、多范式的编程语言。访问 语言导览 了解更多语法和基本类型的信息,以及其他文档,更好地掌握 MoonBit。