MoonBit 新手之旅
这一文档针对不熟悉语言的新手用户所写,并不打算作为一个几分钟就能读完的小文章。 本文希望为那些对 MoonBit 的编程思路 (更加现代化,函数式的) 不甚了解的用户提供一个简洁而不失易懂性的指南。
对于那些希望能直接接触语言的用户,请移步语法参考
本文译自对应的英文文档 A Tour of MoonBit for Beginners
安装
语言插件
目前 MoonBit 提供的开发支持都是通过 VS Code 插件实现的。转到 VS Code Marketplace 来下载 MoonBit 语言支持。
工具链
(推荐方式)若您已安装了上述插件,可以通过 action menu 中的 'Install moonbit toolchain' 下载工具链并跳过这部分介绍。
除去插件支持外,我们还提供一键安装脚本:Linux 和 macOS 用户可以通过以下命令安装
curl -fsSL https://cli.moonbitlang.cn/install/unix.sh | bash
Windows 用户可以通过 PowerShell 安装:
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser; irm https://cli.moonbitlang.cn/install/powershell.ps1 | iex
安装脚本会将工具链安装到 $HOME/.moon
下,并自己添加到 PATH
环境变量里。
MoonBit 目前处于活跃开发的阶段,尚不满足生产环境的需求。若需要更新,只需再次运行上述命令即可。
可以通过 moon new
交互式地创建新项目(模块),填写好必要信息之后将会在项目路径下生成如下结构
my-project
├── LICENSE
├── moon.mod.json
├── README.md
└── src
├── lib
│ ├── hello.mbt
│ ├── hello_test.mbt
│ └── moon.pkg.json
└── main
├── main.mbt
└── moon.pkg.json
这是一个很典型的项目结构,试试 运行 moon run src/main
。
现在可以开始我们的月兔之旅了。
开始编码
本文中我们会把以下所有的代码都写在 main.mbt
中。main
函数是程序的入口点,并且只能出现在 main
包中。
更细致的介绍可以在我们的《MoonBit 构建系统配置》一文中找到。
变量
变量通过 let
来定义:
let e = 2.718281828459045 // double
let int_min = -2147483648 // int
let int_max : Int = 2147483647 // 显式类型标注
let tuple = (1, 2) // 2-tuple
fn init {
let array = [1, 2, 3, 4, 5]
// array = [4, 5, 6, 7, 8] // 错误:`let` 创建的是不可变绑定。
let mut mut_array = [1, 2, 3, 4, 5]
mut_array = [4, 5, 6, 7, 8]
println(mut_array)
}
MoonBit 是一个带类型推断的严格类型语言。在上方的例子中,let
绑定(我们习惯说绑定而不是赋值)一个符号到一个值上,
这个符号也就被推断为拥有和值一致的类型。试试将光标悬浮到代码块的任何符号上来查看其类型。
默认情况下 let
绑定创建的是一个不可变的引用(这里的引用指的不是 C++ 或 Rust 的引用,只是一种符号和值的联系)。
也就是说不能让某个符号去引用别的值,除非重新绑定它(使用 let
),否则应当使用 let mut
。
函数
函数不过是一段接受某些输入并产生一个输出的一段代码。我们用关键词 fn
来定义一个函数(MoonBit 中的函数不应该由大写字母 A-Z 起头):
fn identity[T](x : T) -> T { // `Identity` 则不符合命名规范
x
}
在这个例子中,我们显式地给定了类型。注意类型标注写法上和传统类 C 语言的差别:
后者使用前缀类型标注 T x
,我们则用后缀类型标注 x: T
。
我们在返回类型前用箭头 ->
来表示一个函数的本质:从某个类型到某个类型的映射。
正式地说,这一写法叫做尾随类型标注(trailing return type),诸如 C++、Rust、Swift 等语言都有相同的写法。
表达式一词在本文的介绍中没有严格定义,直观上可理解为某样具有我们关心的值的东西。
因此, 一个函数类型可以写 作 (S) -> T
,其中 S
(用圆括号括起)是参数类型,而 T
是返回类型。
MoonBit 中的函数是头等公民,即函数可以像普通的变量一样传来传去,只要保证类型正确即可。
fn compose[S, T, U](f : (T) -> U, g : (S) -> T) -> (S) -> U {
let composition = fn(x : S) { f(g(x)) } // 定义一个 `f` `g` 的复合函数
// 和其他函数式语言相似
// moonbit 还提供管道(pipe)操作符 `|>`
fn(x : S) { g(x) |> f } // 等价写法
}
现在的语言一般都有 Lambda 表达式。大部分语言是通过语法糖来实现这一特性的。 一个 Lambda 表达式不过是一个匿名的闭包,这一特性也体现在 MoonBit 的语法上:
闭包只捕捉其周围(即同一缩进等级的,假设代码已经格式化过)的变量和自己的约束变量
fn foo() -> Int {
fn inc(x) { x + 1 } // 命名为 `inc`
(fn (x) { x + inc(2) })(6) // 匿名函数,即 lambda 表达式
// 函数会自动捕捉最后一个表达式的值并返回
}
foo() // => 9
现在我们学习了最基本的知识,接下来写点代码吧。
实现链表
链表由一系列指向自己之后的结点的结点构成。听起来很递归?实 际上也的确如此,我们就用递归的方式实现它:
enum List[T] {
Nil // 基本情况:空表
Cons(T, List[T]) // 递归定义
}
enum
即枚举类,和传统面向对象语言中的相同。不过我们尽量不用面向对象的叫法(分支,cases),而称其为构造子。
因而上述代码可以读作
类型
List[T]
可以由Nil
Cons
构造子构造而来,前者表示一个空链表,后者能够容纳一些类型为T
的数据和链表的剩余部分。 这里的方括号告诉我们这是一个多态/泛型(polymorphic/generic)函数,即一个元素类型为T
的链表(此处T
可以是任何类型,因此称多态)。 如果用Int
实例化这个T
,我们就定义了一个整数的链表。
另外一个常见的数据类型是我们的老熟人 Struct
,其工作方式和类 C 语言中的同名结构相似。我们用上面定义的 List
和下方将要定义的 User
来创建一个用户列表:
struct User {
id : Int
name : String
// 默认情况下 Struct 的属性/字段是不可变的
// `mut` 关键字就和我们之前说的一样
mut email : String
} derive(Show)
// 我们通过把函数第一个参数定义为 `self: User` 来给该 Struct 定义一个 method
// 写法和 Python 类似
// 注意:只有类型所在的包能为其定义方法。 不能直接为外部类型定义方法。
fn greetUser(self : User) -> String { // `User` 的一个方法
let id = self.id
let name = self.name
"Greetings, \{name} of id \{id}" // 字符串插值写法
}
// 构造 User 对象的写法
let evan : User = { id: 0, name: "Evan", email: "someone@example.com" }
// 可以用一个语法糖将 evan 的属性复制一遍,
// 并把其 email 换成其他的值,构造一个新对象
let listOfUser : List[User] = Cons(evan, Cons({ ..evan, email: "someoneelse@example.com" }, Nil))
除了这两种数据类型之外,还有一种较为特殊的枚举类型:type
. 可以看作其将已存在的类型包装起来,
成为一个新的类型。这样能在保持原有类型特性的基础上继续新增方法,
为外部类型定义方法的同时不需修改其本身。
例如 User
中的 name
的类型,我们还可以定义为
type UserName String // 一个新类型 UserName,基于 String
// 可以为 UserName 定义方法,String 则不行。
fn is_blank(self : UserName) -> Bool {
// 通过 `.0` 访问其内部类型(String)
// iter() 创建一个内部迭代器(internal iterator)
// 并借此以函数式的风格在某个序列结构上迭代
// find_first 遇到第一个 true (即非空字符)就短路
let res = self.0.iter().find_first(
fn(c) { if c == ' ' { false } else { true } },
)
match res {
Some(_) => false
// 找不到非空字符,所以是只由空格组成的字符串
None => true
}
}
enum
struct
newtype
是定义数据类型的三种方式,MoonBit 中没有 class
,且也不需要。
derive
关键字就和 Java 的 implements
相似。这里 Show
是一个类型类(type trait/class),
它指出一个类型的内容可以被打印出来。那么什么是类型类?
类型类
类型类(trait)就是我们在传统面向对象编程中熟悉的接口(interface)。
println(evan)
会输出 {id: 0, name: "Evan", email: "someone@example.com"}
,
这是因为 User
由内置类型 Int
String
组成,后者已经实现该 trait,
所以我们不需要手动实现。
不妨通过实现 to_string()
来实现我们自己定义的 Printable
trait:
trait Printable {
to_string(Self) -> String
}
fn to_string(self : User) -> String {
(self.id, self.name, self.email).to_string()
} // 这样就实现了 `Printable`
fn to_string[T : Printable](self : List[T]) -> String {
let string_aux = to_string_aux(self)
// 函数参数可以有标签
"[" + string_aux.substring(end=string_aux.length() - 1) + "]"
}
// 多态函数一定是顶层函数
fn to_string_aux[T : Printable](self : List[T]) -> String {
match self {
Nil => ""
Cons(x, xs) => "\{x} " + to_string_aux(xs)
}
}
listOfUser.to_string()
// => [(0, Evan, someone@example.com) (0, Evan, someoneelse@example.com)]
在 Java 中我们一般使用 <T extends Printable>
来限制某个泛型函数的参数类型,
使得类型 T
的对象都能够输出。
与之相仿,在 MoonBit 中我们一般写成 [T: Printable]
。
模式匹配
在上面的例子中我们使用了 match
表达式,这是 MoonBit(和许多函数式语言的)的核心特性之一。
简单地说,我们使用模式匹配(pattern matching)来解构(destructure, 即脱去外层的包装/抽象)一个结构。
上方的 match
代码可以表述为
如果
self
是通过Nil
构造子(空链表)构造的,我们返回""
; 否则如果self
是通过Cons(x,xs)
构造出来的(非空链表) 我们输出x
和链表的剩余部分,其中x
是链表的头,xs
是剩余部分。
直观上,我们是把 x
和 xs
(这两个符号也是原地绑定的)从 self
中通过模式匹配提取出来。
不妨借此定义一些典型的链表操作 map
reduce
zip
:
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))
}
}
fn zip[T](self : List[T], other : List[T]) -> List[T] {
match (self, other) {
(Nil, _) => Nil // 我们用下划线表示我们不关心的值
(_, Nil) => Nil
(Cons(x, xs), Cons(y, ys)) => Cons(x, Cons(y, zip(xs, ys)))
}
}
现在我们总算是拥有一个勉强可用的链表类型了。
实际上,绝大多数时候我们都用内置的 Array
类型,
因为其性能要好得多。
模式匹配也能够在 let
中使用。在 greetUser()
中,可以把两个 let
绑定改写为
fn greetUserAlt(self : User) -> String {
// 从 `self` 中提取 `id` `name` 字段,忽略 `email`
let { id: id, name: name, email: _ } = self
// 等价写法,但是忽略 `id `name` 之外的所有字段。
let { id, name, .. } = self
"Greetings, \{name} of id \{id}"
}
循环
最后来了解一下面向对象语言中十分重要的循环结构。虽然我们上面主要使用递归,
MoonBit 本身是一个多范式语言,
因而其也保留了 C 风格命令式的的 for
while
循环结构。
除此之外,MoonBit 还提供了一种更有意思的写法:函数式循环。譬如斐波那契数就可以写成
fn fib(n : Int) -> Int {
loop n, 0, 1 { // 引入 3 个循环变量: `n` `a = 0` `b = 1`
// `loop` 中也可以使用 pattern matching
0, a, _ => a // 只有 0 能从 0 构造出来。
// 将 `b` 赋给 `a`,`(a + b)` 赋给 `b`,循环计数器 `n - 1`
n, a, b => continue n - 1, b, a + b
}
}
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(fib) // => [1,1,2,3,5,8,13,21,34,55]
从语义上讲, 函数式循环更强调迭代过程中每个状态的转换, 相比 tail-recursive 的写法, 前者的可读性更佳,也保留了递归的风格,同时还具备和尾递归相同的性能。
结束语
到目前为止,我们已学习了 MoonBit 的基本语法和一些较为重要的特性,但 MoonBit 本身是一个具有众多特性, 多范式的编程语言。我们建议您在确定 MoonBit 的基本语法合口味之后, 读一读一些有意思的样例, 来加深您的理解。