使用包管理项目#

在大规模开发项目时,项目通常需要分解为相互依赖的较小模块单元。更常见的是使用其他人的工作:最典型的是 core,MoonBit 的标准库。

包和模块#

在 MoonBit 中,代码组织的最重要单元是包,它由多个源代码文件和一个单独的 moon.pkg.json 配置文件组成。包可以是一个 main 包,包含一个 main 函数,或者是一个用作库的包。

一个项目对应一个模块,由多个包和一个单独的 moon.mod.json 配置文件组成。

在从另一个包中使用内容时,模块之间的依赖关系应首先在 moon.mod.json 中声明。然后在 moon.pkg.json 中声明包之间的依赖关系。然后可以使用 @pkg 访问导入的实体,其中 pkg 是导入包路径的最后一部分或 moon.pkg.json 中声明的别名:

pkgB/moon.pkg.json#
{
    "import": [
        "moonbit-community/language/packages/pkgA"
    ]
}
pkgB/top.mbt#
pub fn add1(x : Int) -> Int {
  @pkgA.incr(x)
}

访问控制#

默认情况下,所有函数定义和变量绑定对其他包是 不可见 的。可以在顶层 let/fn 前使用 pub 修饰符使其公开。

MoonBit 中有四种不同的类型可见性:

  • 私有类型,使用 priv 声明,对外部世界完全不可见

  • 抽象类型,这是类型的默认可见性。只有抽象类型的名称对外部可见,类型的内部表示被隐藏

  • 只读类型,使用 pub(readonly) 声明。只读类型的内部表示对外部可见,但用户只能从外部读取这些类型的值,不允许构造和修改

  • 完全公开类型,使用 pub(all) 声明。外部世界可以自由构造、修改和读取这些类型的值

除了类型本身的可见性外,公开的结构体的字段可以用 priv 注释,这将完全隐藏字段对外部世界。请注意,具有私有字段的 struct 不能直接在外部构造,但可以使用函数式 struct 更新语法更新公共字段。

只读类型是一个非常有用的功能,受到 OCaml 中 私有类型 的启发。简而言之,pub(readonly) 类型的值可以通过模式匹配和点语法解构,但不能在其他包中构造或修改。请注意,在定义 pub(readonly) 类型的同一包中没有限制。

// Package A
pub(readonly) struct RO {
  field: Int
}
test {
  let r = { field: 4 }       // 可以
  let r = { ..r, field: 8 }  // 可以
}

// Package B
fn println(r : RO) -> Unit {
  println("{ field: ")
  println(r.field)  // 可以
  println(" }")
}
test {
  let r : RO = { field: 4 }  // 错误:不能创建公共只读类型 RO 的值!
  let r = { ..r, field: 8 }  // 错误:不能修改公共只读字段!
}

MoonBit 中的访问控制遵循一个原则,即公开的类型、函数或变量不能以私有类型定义。这是因为私有类型可能无法在使用公开的实体的所有地方访问。MoonBit 包含了健全性检查,以防止违反这一原则的用例发生。

pub(all) type T1
pub(all) type T2
priv type T3

pub(all) struct S {
  x: T1  // 可以
  y: T2  // 可以
  z: T3  // 错误:公开字段使用了私有类型 `T3`!
}

// 错误:公开函数使用了私有类型 `T3`!
pub fn f1(_x: T3) -> T1 { ... }
// 错误:公开函数的返回值使用了私有类型 `T3`!
pub fn f2(_x: T1) -> T3 { ... }
// 可以
pub fn f3(_x: T1) -> T1 { ... }

pub let a: T3 = { ... } // 错误:公开变量的类型是私有类型 `T3`!

方法和特征实现的访问控制#

为了使特征系统一致(即每个 Type: Trait 对都有全局唯一的实现),并防止第三方包意外地修改现有程序的行为,MoonBit 对谁可以定义方法/实现类型的特征采用了以下限制:

  • 只有定义类型的包才能为其定义方法。因此,不能为内建和外部类型定义新方法或覆盖旧方法。

  • 只有类型的包或特征的包才能定义实现。例如,只有 @pkg1@pkg2 允许编写 impl @pkg1.Trait for @pkg2.Type

上述第二条规则允许通过定义新特征并实现它来为外部类型添加新功能。这使 MoonBit 的特征和方法系统灵活,同时享有良好的一致性属性。

特征的可见性和封闭特征#

特征有四种可见性,就像 structenum:私有、抽象、只读和完全公开。私有特征使用 priv trait 声明,对外部完全不可见。抽象特征是默认可见性:只有特征的名称对外部可见,特征中的方法不会暴露。只读特征使用 pub(readonly) trait 声明,它们的方法可以从外部调用,但只有当前包可以为只读特征添加新实现。最后,完全公开特征使用 pub(open) trait 声明,它们对外部新实现是开放的,它们的方法可以自由使用。

抽象和只读特征是封闭的,因为只有定义特征的包才能实现它们。在其包外实现封闭(抽象或只读)特征会导致编译器错误。

以下是抽象特征的示例:

trait Number {
 op_add(Self, Self) -> Self
 op_sub(Self, Self) -> Self
}

fn add[N : Number](x : N, y: N) -> N {
  Number::op_add(x, y)
}

fn sub[N : Number](x : N, y: N) -> N {
  Number::op_sub(x, y)
}

impl Number for Int with op_add(x, y) { x + y }
impl Number for Int with op_sub(x, y) { x - y }

impl Number for Double with op_add(x, y) { x + y }
impl Number for Double with op_sub(x, y) { x - y }

从包外,用户只能看到以下内容:

trait Number

fn op_add[N : Number](x : N, y : N) -> N
fn op_sub[N : Number](x : N, y : N) -> N

impl Number for Int
impl Number for Double

Number 的作者可以利用只有 IntDouble 可以实现 Number 这一事实,因为在外部不允许新的实现。