使用包管理项目#

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

包和模块#

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

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

一个模块由 name 字段标识,通常由两部分组成,用 / 分隔:user-name/project-name。包由相对于源代码根目录的路径标识,该路径由 source 字段定义。完整标识符为 user-name/project-name/path-to-pkg

在从另一个包中使用内容时,模块之间的依赖关系应首先在 moon.mod.json 中声明,使用 deps 字段。然后在 moon.pkg.json 中声明包之间的依赖关系,使用 import 字段。

包的默认别名是标识符中 / 分隔的最后一部分。可以使用 @pkg_alias 访问导入的实体,其中 pkg_alias 可以是完整标识符或默认别名。也可以使用 import 字段定义自定义别名。

pkgB/moon.pkg.json#
{
    "import": [
        "moonbit-community/language/packages/pkgA",
        {
            "path": "moonbit-community/language/packages/pkgC",
            "alias": "c"
        }
    ]
}
pkgB/top.mbt#
pub fn add1(x : Int) -> Int {
  @moonbitlang/core/int.abs(@c.incr(@pkgA.incr(x)))
}

内部包#

你可以定义只对某些包可用的内部包。

a/b/c/internal/x/y/z 中的代码只对包 a/b/ca/b/c/** 可用。

访问控制#

MoonBit 具有全面的访问控制系统,管理代码的哪些部分可以从其他包访问。此系统有助于维护封装、信息隐藏和清晰的 API 边界。可见性修饰符适用于函数、变量、类型和特征,允许对代码如何被他人使用进行细粒度控制。

函数#

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

别名#

默认情况下,所有别名,即 函数别名方法别名类型别名特征别名,对其他包是 不可见 的。

可以在定义前使用 pub 修饰符使其公开。

类型#

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

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

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

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

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

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

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

// Package A
pub 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`!

特征(trait)#

特征有四种可见性,就像 structenum:私有、抽象、只读和完全公开。

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

  • 特征默认是抽象的。只有特征类型的名称对外部可见,特征的方法被隐藏

  • 只读特征,使用 pub trait 声明,它们的方法可以从外部调用,但只有当前包可以为只读特征添加新实现

  • 完全公开的特征,使用 pub(open) trait 声明,在外部包也可以增加实现,其方法可以自由使用

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

特征的实现#

实现本身有独立的可见性,就像函数一样。除非实现是 pub,否则在当前包之外,类型不会被认为已经实现了特征。

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

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

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

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

警告

目前,空特征会自动实现。

以下是抽象特征的示例:

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

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

fn[N : Number] sub(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[N : Number] op_add(x : N, y : N) -> N
fn[N : Number] op_sub(x : N, y : N) -> N

impl Number for Int
impl Number for Double

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

虚拟包#

警告

虚拟包是一个实验性功能。可能存在错误和未定义的行为。

你可以定义虚拟包,它作为一个接口。它们可以在构建时被特定的实现替换。目前,虚拟包只能包含普通函数。

虚拟包在保持代码不变的情况下,可以在不同的实现之间进行切换。

定义一个虚拟包#

你需要声明它是一个虚拟包,并在 MoonBit 接口文件中定义其接口。

moon.pkg.json 中,你需要添加字段 virtual :

{
  "virtual": {
    "has-default": true
  }
}

The has-default 表示虚拟包是否有默认实现。

在包内,你需要添加一个接口文件 package-name.mbti,其中 package-name默认别名 相同:

/src/packages/virtual/virtual.mbti#
package "moonbit-community/language/packages/virtual"

fn log(String) -> Unit

The interface文件的第一行需要是 package "full-package-name"。接下来是声明。访问控制pub 关键字和函数参数名称应该省略。

提示

如果你不确定如何定义接口,可以创建一个普通包,使用 TODO 语法 定义所需的函数,并使用 moon info 帮助你生成接口。

实现虚拟包#

一个虚拟包可以有一个默认实现。通过将 virtual.has-default 设置为 true,你可以在同一包内照常实现代码。

/src/packages/virtual/top.mbt#
pub fn log(s : String) -> Unit {
  println(s)
}

虚拟包也可以由第三方实现。通过将 implements 定义为目标包的完整名称,编译器可以警告你关于缺失的实现或不匹配的实现。

{
  "implement": "moonbit-community/language/packages/virtual"
}
/src/packages/implement/top.mbt#
pub fn log(string : String) -> Unit {
  ignore(string)
}

使用虚拟包#

要使用虚拟包,与其他包一样:在你想要使用它的包中定义 import 字段。

覆盖虚拟包#

如果虚拟包有一个默认实现,并且是你想要的,则不需要额外的配置。

否则,你可以通过提供一个实现数组来定义 overrides 字段,指定你想要使用的实现。

/src/packages/use_implement/moon.pkg.json#
{
  "overrides": ["moonbit-community/language/packages/implement"],
  "import": [
    "moonbit-community/language/packages/virtual"
  ],
  "is-main": true
}

在使用实体时,你应该引用虚拟包。

/src/packages/use_implement/top.mbt#
fn main {
  @virtual.log("Hello")
}