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 run 和 new 这三个命令。
要创建一个项目(或者说模块),运行 moon new <path>,其中 moon new examine,您将得到:
examine
├── Agents.md
├── cmd
│ └── main
│ ├── main.mbt
│ └── moon.pkg.json
├── LICENSE
├── moon.mod.json
├── moon.pkg.json
├── my_project_test.mbt
├── my_project.mbt
├── README.mbt.md
└── README.md -> README.mbt.md
其中包含一个 cmd/main 包,内有一个 fn main 作为程序的入口。尝试运行 moon run cmd/main。
在这个教程中,我们假设项目名称为 examine,当前工作目录也是 examine。
示例:找到通过考试的人#
在这个例子中,我们将尝试找出,给定一些学生的分数,有多少人通过了考试?
为此,我们将从定义数据类型开始,确定我们的函数,并编写我们的测试。然后我们将实现我们的函数。
除非另有说明,以下内容将在文件 top.mbt 中定义。
数据类型#
MoonBit 中的 基本数据类型 包括以下内容:
UnitBoolInt,UInt,Int64,UInt64,Byte, …Float,DoubleChar,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 没有实现 Show 和 Eq。
Show 和 Eq 是 traits。在 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 -> Unit {
...
}
我们需要为我们的 ExamResult 实现 Eq 和 Show。有两种方法可以实现。
通过定义一个显式实现:
impl Eq for ExamResult with op_equal(self, other) { match (self, other) { (Pass, Pass) | (Fail, Fail) => true _ => false } }
在这里,我们使用 模式匹配 来检查
ExamResult的情况。另一种方法是通过 派生 ,因为
Eq和Show是 内置 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 块移动到一个新文件 top_test.mbt 中。
糟糕!现在有错误抱怨:
is_qualified和count_qualified_students未绑定Fail和Pass未定义Student不是一个记录类型,字段id未找到,等等。
所有这些问题都来自于可见性问题。默认情况下,定义的函数对当前包(由当前文件夹绑定)之外的程序的其他部分不可见。默认情况下,类型被视为抽象类型,即我们只知道存在类型 Student 和类型 ExamResult。通过使用黑盒测试,您可以确保您希望其他人拥有的一切确实被赋予了预期的可见性。
为了让其他人使用这些函数,我们需要在 fn 前添加 pub 使函数公开。
为了让其他人构造类型和读取内容,我们需要在 struct 和 enum 前添加 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 的模块注册中心。您也可以在那里找到其他有趣的项目。
执行
moon login并按照说明使用现有的 GitHub 账户创建您的账户。在
moon.mod.json中修改项目名称为<您的 GitHub 账户名>/<项目名称>。运行moon check查看moon.pkg.json中是否有其他受影响的地方。执行
moon publish,您就完成了。您的项目将可供他人使用。
默认情况下,项目将在 Apache 2.0 下共享,这是一种宽松的许可证,允许每个人使用。您还可以通过更改 moon.mod.json 中的 license 字段和 LICENSE 的内容,使用其他许可证,例如 MulanPSL 2.0。
结束语#
到目前为止,我们已经了解了 MoonBit 的基本特性和一些不那么简单的特性,然而 MoonBit 是一个功能丰富的、多范式的编程语言。访问 语言导览 了解更多语法和基本类型的信息,以及其他文档,更好地掌握 MoonBit。
