パッケージでプロジェクトを管理する#

大規模なプロジェクトを開発するとき、プロジェクトは通常、相互に依存する小さなモジュール単位に分割する必要があります。さらに多くの場合、他人の成果物を利用します。代表的なものが、MoonBit の標準ライブラリである core です。

パッケージとモジュール#

MoonBit では、コード整理の最も重要な単位はパッケージです。パッケージは複数のソースコードファイルと 1 つのパッケージ設定ファイル(moon.pkg、または旧形式の moon.pkg.json)から構成されます。パッケージは、main 関数を含む main パッケージにも、is-main フィールドで識別されるライブラリ用パッケージにもなれます。

モジュールに対応するプロジェクトは、複数のパッケージと 1 つの moon.mod.json 設定ファイルから構成されます。

モジュールは name フィールドで識別され、通常は / で区切られた 2 つの部分、つまり user-name/project-name から構成されます。パッケージは、source フィールドで定義されるソースルートからの相対パスで識別されます。完全な識別子は user-name/project-name/path-to-pkg になります。

別のパッケージのものを使う場合、まずモジュール間の依存関係を moon.mod.jsondeps フィールドで宣言する必要があります。その後、パッケージ間の依存関係をパッケージファイル(moon.pkg、または旧形式の moon.pkg.json)の import フィールドで宣言します。多くの core パッケージも同じ規則に従います。@json@test、その他の通常の core エイリアスを使う場合は、core_package_not_imported 警告を避けるために、対応する moonbitlang/core/... パッケージを import に追加してください。

パッケージの デフォルトエイリアス は、識別子を / で分割した最後の部分です。@pkg_alias を使うと import した要素にアクセスできます。ここで pkg_alias は完全な識別子でもデフォルトエイリアスでも構いません。カスタムエイリアスは、import フィールドで定義できます。

moon.pkg では、カスタムエイリアスは次のように書きます:

import {
  "moonbit-community/language/packages/pkgA",
  "moonbit-community/language/packages/pkgC" @c,
  "moonbitlang/core/builtin",
}
pkgB/top.mbt#
///|
pub fn add1(x : Int) -> Int {
  @moonbitlang/core/builtin.Add::add(0, @c.incr(@pkgA.incr(x)))
}

prelude と組み込み名#

@pkg. を省略すると、MoonBit は現在のパッケージと prelude の中で修飾されていない名前を解決します。そのため、同じ名前のローカル定義があると prelude の定義は隠されます。

fn println(msg : String) -> String {
  "log: \{msg}"
}

///|
fn shadowed_println() -> String {
  println("hello")
}

///|
fn builtin_answer() -> Int {
  let answer : Int = 42
  answer
}

prelude は特別なパッケージです。デフォルトで利用でき、そこで公開された名前は明示的な import なしで通常の未修飾名解決に参加します。

コンパイラ組み込み要素は別のカテゴリです。Int などの型はどのパッケージからも import されるのではなく、言語そのものに組み込まれているため、@builtin.Int は存在しません。同じ区別は StringBoolUnit などのコンパイラ既知の名前にも当てはまります。

内部パッケージ#

特定のパッケージからのみ利用できる内部パッケージを定義できます。

a/b/c/internal/x/y/z 内のコードは、a/b/ca/b/c/** のパッケージからのみ利用できます。

using#

using 構文を使うと、別のパッケージで定義されたシンボルを import できます。

pkgC/top.mbt#
///|
pub using @pkgA {incr, trait Trait, type Type}

pub 修飾子を付けると、再 export と見なされます。

アクセス制御#

MoonBit には、どのコード部分が他のパッケージからアクセス可能かを管理する包括的なアクセス制御システムがあります。この仕組みは、カプセル化、情報隠蔽、明確な API 境界の維持に役立ちます。可視性修飾子は関数、変数、型、trait に適用でき、他者がどのようにコードを利用できるかを細かく制御できます。

関数#

デフォルトでは、すべての関数定義と変数束縛は他のパッケージからは 見えません。トップレベルの let/fn の前に pub 修飾子を付けると公開できます。

エイリアス#

デフォルトでは、function aliasmethod alias は元の定義の可視性に従います。一方で、type aliasusing は他のパッケージからは 見えません

定義の前に pub 修飾子を付けるか、annotation 内の visibility フィールドに値を入れることができます。

#

MoonBit の型には 4 種類の可視性があります:

  • Private 型:priv で宣言され、外部から完全に見えません

  • Abstract 型:型のデフォルト可視性です。

    Abstract 型では名前だけが外部に見え、型の内部表現は隠されます。Abstract 型を デフォルトにしているのは、カプセル化と情報隠蔽を促すための設計上の選択です。

  • pub で宣言される readonly 型です。

    readonly 型の内部表現は外部から見えますが、外部からできるのは値の読み取りだけです。構築と変更はできません。

  • pub(all) で宣言される完全公開型です。

    外部からこれらの型を自由に構築し、値を読み取り、可能であれば変更できます。

型自体の可視性に加えて、public な struct のフィールドに priv を付けると、そのフィールドを外部から完全に隠せます。private フィールドを持つ struct は外部から直接構築できませんが、関数的な struct 更新構文を使って public フィールドは更新できます。

readonly 型は、OCaml の private types に触発された非常に便利な機能です。要するに、pub 型の値は パターンマッチやドット構文で分解できますが、他のパッケージでは構築や変更は できません。

注釈

pub 型が定義された同じパッケージ内では制限はありません。

// Package A
pub struct RO {
  field: Int
}
test {
  let r = { field: 4 }       // OK
  let r = { ..r, field: 8 }  // OK
}

// Package B
fn println(r : RO) -> Unit {
  println("{ field: ")
  println(r.field)  // OK
  println(" }")
}
test {
  let r : RO = { field: 4 }  // ERROR: Cannot create values of the public read-only type RO!
  let r = { ..r, field: 8 }  // ERROR: Cannot mutate a public read-only field!
}

MoonBit のアクセス制御は、pub 型・関数・変数を private 型を使って定義してはならない、という原則に従います。private 型は pub な要素が使われるすべての場所で利用できるとは限らないためです。MoonBit には、この原則に反する使い方を防ぐための整合性チェックが組み込まれています。

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

pub(all) struct S {
  x: T1  // OK
  y: T2  // OK
  z: T3  // ERROR: public field has private type `T3`!
}

// ERROR: public function has private parameter type `T3`!
pub fn f1(_x: T3) -> T1 { ... }
// ERROR: public function has private return type `T3`!
pub fn f2(_x: T1) -> T3 { ... }
// OK
pub fn f3(_x: T1) -> T1 { ... }

pub let a: T3 = { ... } // ERROR: public variable has private type `T3`!

trait#

trait にも structenum と同様に 4 種類の可視性があります。private、abstract、readonly、fully public です。

  • Private trait は priv trait で宣言され、外部からは完全に見えません。

  • Abstract trait はデフォルトの可視性です。trait 名だけが外部に見え、trait 内のメソッドは公開されません。

  • Readonly trait は pub trait で宣言され、メソッドは外部から呼び出せますが、readonly trait に新しい実装を追加できるのは現在のパッケージだけです。

  • Fully public trait は pub(open) trait で宣言され、現在のパッケージ外からも新しい実装を追加でき、メソッドを自由に使えます。

Abstract trait と readonly trait は sealed です。trait を定義したパッケージだけがそれらを実装できるからです。sealed な(abstract または readonly の)trait をそのパッケージ外で実装するとコンパイラエラーになります。

trait 実装#

実装は関数と同様に独立した可視性を持ちます。実装が pub でない限り、その型は現在のパッケージ外では trait を満たしているとは見なされません。

trait システムの整合性(つまり、すべての Type: Trait の組に対して世界で一意の実装があること)を保ち、サードパーティ製パッケージが既存プログラムの挙動を誤って変更するのを防ぐため、MoonBit では型に対するメソッド定義や trait 実装に次の制限を設けています:

  • 型を定義したパッケージだけが、その型のメソッドを定義できます。したがって、組み込み型や外部型に対して新しいメソッドを定義したり、既存メソッドを上書きしたりはできません。

    • この規則には例外があります。local methods です。ただしローカルメソッドは常に private なので、MoonBit の型システムの整合性を壊しません。

  • 実装を定義できるのは、その型のパッケージか trait のパッケージだけです。例えば、impl @pkg1.Trait for @pkg2.Type を書けるのは @pkg1@pkg2 だけです。

上記 2 つ目の規則により、新しい trait を定義して実装することで外部型に新しい機能を追加できます。これにより、MoonBit の trait とメソッドの仕組みは整合性を保ちながら柔軟になります。

警告

現在、空の trait は自動的に実装されます。

abstract trait の例を示します:

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 の作者は、Number を実装できるのが IntDouble だけであるという事実を利用できます。外部では新しい実装を追加できないからです。

仮想パッケージ#

警告

仮想パッケージは実験的機能です。バグや未定義の動作が含まれている可能性があります。

インターフェースとして機能する仮想パッケージを定義できます。仮想パッケージはビルド時に特定の実装へ置き換えられます。現在、仮想パッケージには通常の関数しか含められません。

仮想パッケージは、コードを変更せずに異なる実装を切り替えたいときに便利です。

仮想パッケージの定義#

仮想パッケージであることを宣言し、そのインターフェースを MoonBit の interface ファイルに定義する必要があります。

moon.pkg には、virtual フィールドを追加する必要があります:

options(
  "virtual": { "has-default": true },
)

has-default は、その仮想パッケージにデフォルト実装があるかどうかを示します。

パッケージ内には、interface ファイル pkg.mbti を追加する必要があります:

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

fn log(String) -> Unit

interface ファイルの 1 行目は package "full-package-name" である必要があります。続いて宣言を書きます。アクセス制御pub キーワードと、関数のパラメータ名は省略してください。

ヒント

interface の定義に迷う場合は、通常のパッケージを作成し、TODO syntax を使って必要な関数を定義し、moon info で interface 生成を補助できます。

仮想パッケージの実装#

仮想パッケージにはデフォルト実装を持たせることができます。virtual.has-defaulttrue にすると、同じパッケージ内で通常どおりコードを実装できます。

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

仮想パッケージはサードパーティによって実装することもできます。implements に対象パッケージの完全名を指定すると、未実装や不一致の実装についてコンパイラが警告してくれます。

options(
  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#
import {
  "moonbit-community/language/packages/virtual",
}

options(
  "is-main": true,
  overrides: [ "moonbit-community/language/packages/implement" ],
)

要素を使うときは、仮想パッケージを参照してください。

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