Skip to content

v0.1.z でのパッケージシステムの設計案

Takashi Suwa edited this page Dec 7, 2022 · 16 revisions

概要

従来のv0.0.zのSATySFiはパッケージと呼ばれる概念がありつつもその立ち位置が曖昧で,ほぼモジュールと1対1対応する形で使用していました.SATySFi v0.1.0では,パッケージという概念をリリースの単位をなすものとしてはっきりとモジュールとは区別して扱うことにし,本稿に示したようなパッケージシステムを採用したいと考えています.これは以下のような理由によります:

  • 複数の “パッケージ” が同一のモジュール名を使っているような状況でも,名前が衝突したりせず整然と扱えるようにしたいため.
  • ソースコードに限らず,フォントをはじめとする種々のリソースもパッケージという単位で扱えた方がやはり名前の衝突防止などに関して自然であるため.
  • 外部ツールとSATySFiに分かれている場合に比べ,「文書ファイル内に依存パッケージの制約を記述する」といった機能が自然に実現できるなど,ユーザにとっての使用感上恩恵があるため.

従来はこのリリースの単位としてのパッケージをSatyrographosという外部ツールが管理しており,SATySFi本体は関知していませんでしたが,SATySFi v0.1.0ではSATySFi自身がこの意味のパッケージという単位を認識するようにしよう,という試みです.処理系本体の機能が肥大化してややモノリシック気味になってしまうのが弱点ですが,ユーザにとっての弱点はなく,利便性の向上が期待できるのではないかと思います.2022年12月1日現在,すでに dev-0-1-0-package-system branchにて大枠の実装を終えており,動かすことができます.

既存の別言語としてはElmなども処理系本体がパッケージを扱えるような形態をとっていますが,筆者個人の感想としては使用感が良く,参考にしてはどうかとも思っています.

なお,有志によって開発して頂いているパッケージマネージャSatyrographosとの役割分担などについては,今後開発者やユーザの皆さんから意見を頂いたり相談等させて頂けるとありがたいです.

パッケージシステムの使用例

文書作成時

std-ja-book クラスおよび tabular パッケージを利用して sample.saty という文書を作成したいとします.このとき,まず以下のような sample.saty の雛形を作成します:

foo/
└── sample.saty
#[dependencies [
  (`std-ja-book`, `0.0.1`),
  (`tabular`, `0.0.1`),
]]
use open package StdJaBook
use package Tabular

document (|
  title  = {サンプル文書},
  author = {田中 太郎},
|) '<
  +p{
    こんにちは.
  }
>

先頭の #[dependencies …] が依存するパッケージとそのバージョン制約の指定です.ここでは例えば (`std-ja-book`, `0.0.1`) によって std-ja-book パッケージの 0.0.1 またはその後方互換性を壊さない後継バージョンのいずれか(つまり 0.0.1 $≤ v &lt;$ 0.1.0 なるバージョン $v$ のいずれか.SATySFiではCargoと同じくmajor versionが 0 の場合でもminor versionが変わらない限り後方互換とみなすような少し厳しいsemverを使うことにします)を使いたいということを指定しています.

use open package StdJaBookstd-ja-book パッケージが提供するメインモジュール(後述)である StdJaBook を文書中で使うことを表しています.これを書くことで document 函数が使えるようになります.open なしで単に use package StdJaBook とすると,メンバーはスコープに読み込まれず,StdJaBook.document の形でのみ使えるようになります.

この文書を以下のように solve コマンドで処理し,パッケージ間の依存関係をもとに必要な各パッケージのバージョンを決定します:

$ satysfi solve sample.saty

すると,(依存関係の制約解消に成功した場合は)各パッケージのバージョンが固定されてソースコードが取得・配置され,それらの情報を記録したロックファイル sample.satysfi-lock が生成されます:

foo/
├── sample.saty
└── sample.satysfi-lock

続いて build コマンドにより文書を組みます(ここでロックファイルの内容を参照し,文書ファイルを処理する前に依存パッケージの実装を読みにいきます):

$ satysfi build sample.saty

これがSATySFiの処理の中核で,最終的にPDFを出力します:

foo/
├── sample.saty
├── sample.satysfi-aux
├── sample.satysfi-lock
└── sample.pdf

.satysfi-aux は従来と同じく相互参照のためのダンプファイルであり,Git管理下に置く必要はありません.

ロックファイルは使いたいパッケージが変わらない限り solve コマンドで更新する必要はなく,またビルドの再現性のためにGitなどによるバージョン管理の下に加えるのが望ましいものです.新たに依存パッケージを追加したい場合は #[dependencies …] に追記して solve コマンドを叩くことでロックファイルが更新され,パッケージの実装も必要に応じて取得されます.

ライブラリパッケージ実装時

calc という簡単なパッケージをつくりたいとします.以下のようにコンフィグファイル satysfi.yaml とソースファイル src/calc.satyh を配置します:

calc/
├── satysfi.yaml
└── src/
    └── calc.satyh
language: "0.1.0"
package: "calc"
version: "0.0.1"
contents:
  type: "library"
  source_directories:
  - "./src"
  main_module: "Calc"
  dependencies:
  - name: "stdlib"
    requirements: [ "0.0.1" ]
use open package Stdlib

module Calc :> sig
  val sum : list int -> int
end = struct
  val sum ns = List.fold-left ( + ) 0 ns
end

コンフィグファイルの dependencies: … に依存パッケージとそのバージョン制約を列挙します.ここでは stdlib パッケージの 0.0.1 またはその後方互換な後継バージョンを使うことを指示しています.実際,ソースファイル中では use open package Stdlibstdlib を使用しており,List モジュールが Stdlib モジュールのメンバです.

ライブラリの場合も以下のように solve コマンドで処理します(引数は satysfi.yaml のあるディレクトリ):

$ satysfi solve .

これで処理すると,やはりロックファイル package.satysfi-lock が生成されます:

calc/
├── satysfi.yaml
├── package.satysfi-lock
└── src/
    └── calc.satyh

パッケージも以下でビルドすることができます(特に生成物はないため,型検査されるだけです):

$ satysfi build .

モジュールとパッケージの役割の定義

  • モジュール (module): 実装の詳細を外部に漏らさないための抽象化の単位.有限個のメンバ (member) と呼ばれる内容からなり,各メンバは函数だったり,型だったり,或いは入れ子のモジュールだったりする.モジュールはメンバのうち一部の存在を非公開にしたり,型のメンバの定義を抽象化して外部に提供したりする.また,異なるモジュールに属するプログラムの間での名前空間の切り分けも担う.
  • パッケージ (package): リリースの単位.特にインターフェイスの互換性を制御するカプセル化の単位であり,同時に変更されるモジュールが集まっている.また,やはり異なるパッケージに属するプログラムの間での名前空間の切り分けも担う.

モジュールの仕組み

モジュールシステム自体はML系言語のものとほぼ同一です.より具体的にはF-ing Modulesに準拠したものを採用する予定で,2022年12月2日現在で既に dev-0-1-0 ブランチにてほぼ実装済みです.

細かい言語設計の話としては,ライブラリの1ファイルがちょうど1つのモジュールの束縛であるように制限されます.モジュールは入れ子にできるのでどのモジュールも独立したファイルに書かれねばならないわけではありませんが,各ファイルの内容は1つのモジュールの束縛になっていなければならない,ということです.

具体的には,各ファイルは基本的に以下の $file$ の形式をとります(ただし, $[A]^{\ast}$ は空列を含む $A$ の有限個並んだ列を, $[A]^{?}$$A$ が0個または1個であることをそれぞれ表します):

$$ \begin{align} \mathit{file} &\mathrel{::=} [\mathit{header}]^{\ast}\ \mathbf{module}\ X\ [\mathrel{:>} S]^{?} = M \\ \mathit{header} &\mathrel{::=} \mathbf{use}\ X\ \mathbf{of}\ \mathit{string}\ |\ \mathbf{use}\ X\ |\ \mathbf{use}\ \mathbf{package}\ X \end{align} $$

他のファイルのモジュールに依存する場合は $\mathit{header}$ の列をファイル先頭に書くことによって使います.依存関係は循環していてはいけません. $\mathit{header}$ の形式は3種類あり,それぞれ以下の意です:

  • $\mathbf{use}\ X\ \mathbf{of}\ \mathit{string}$
    • 相対パスでモジュール $X$ に依存することを表す.
    • 文書ファイル,および文書ファイルから $\mathbf{use}\ X\ \mathbf{of}\ \mathit{string}$ で読み込まれているファイルを追って再帰的に辿り着くファイル(=ローカルファイル)でのみ使える.
  • $\mathbf{use}\ X$
    • 同一パッケージのファイルモジュール $X$ に依存することを表す.
    • 文書ファイルやローカルファイルでは使えない.
  • $\mathbf{use}\ \mathbf{package}\ X$
    • 他のパッケージ $X$ に依存することを表す.
    • $X$メインモジュールの名前(メインモジュールについては後述).

ファイル名は,そのファイルが束縛しているモジュール名に拡張子をつけたものでなければなりません.

なお,従来のv0.0.zでは “明示的に @require:@import: では依存を示していないものの既に読み込まれている” ファイルの中で定義された値や型が見えてしまう仕組みでしたが,v0.1.zではそうした “暗黙のバイパス” はなくなり,依存することを $\mathit{header}$ で明示したファイルにしか依存できないようにする予定です.

パッケージの仕組み

パッケージは前述のとおり複数のモジュールからなるリリースの単位です.

各パッケージはメインモジュール (main module) と呼ばれるただひとつのモジュールだけをパッケージ外に公開します.例えば,標準ライブラリを stdlib という1つのパッケージにするなら,Stdlib というメインモジュールがあり,Stdlib.satyh は以下のような内容をもつ,という具合です:

use Option
use List
...

module Stdlib :> sig
  module Option : sig
    val get-or-else 'a : 'a -> option 'a -> 'a
    ...
  end

  module List : sig
    val map 'a 'b : ('a -> 'b) -> list 'a -> list 'b
    ...
  end

  ...
end = struct
  module Option = Option
  module List = List
  ...
end

OptionList などは入れ子のモジュールとして外部からアクセスできるようになります(つまり Stdlib.OptionStdlib.List という形で見え,必要に応じて open Stdlib すれば単に OptionList として使える).また,これらの実装は Option.satyhList.satyh といった stdlib パッケージ内の別ファイルによって与えられており,それが Stdlib.satyh の先頭の require で読み込まれている,というわけです.

メインモジュールの名前は,パッケージ名をケバブケースからアッパーキャメルケースに変換したものであることが強く推奨されます.

依存解決前のコンフィギュレーション

各パッケージには satysfi.yaml というコンフィギュレーションファイル (configuration file) を配置します.これは以下のような形式をとり,ユーザが書きます:

// コンフィギュレーションファイル
Config ::= {
  "language": String,      //期待するSATySFiのバージョン

  "name": String,          //パッケージ名
  "version": String,       //パッケージのsemver
  "author": Array[String], //開発者名のリスト
  ...
  "contents": Contents,    //パッケージの内容
}

Contents ::= LibraryPackage | FontPackage | DocumentPackage | ...

LibraryPackage ::= {
  "type": "library",
  "source_directories": Array[String],    //ソースファイルを格納したディレクトリへの相対パスのリスト
  "test_directories": Array[String],      //テストファイルを格納したディレクトリへの相対パスのリスト
  "main_module": String,                  //メインモジュールの名前
  "dependencies": Array[Dependency],      //依存パッケージの指定
  "test_dependencies": Array[Dependency], //テストでのみ依存するパッケージの指定
  ...
}

DocumentPackage ::= {
  "type": "document",
  "source_files": Array[String],     //ソースファイルの相対パス
  "dependencies": Array[Dependency], //依存パッケージの指定
  ...
}

//依存パッケージの指定
Dependency ::= {
  "name": String,        //依存パッケージ名
  "requirement": String, //バージョン制約の記述
}

FontPackage ::= {
  "type": "font",
  "elements:" Array[FontFile], //パッケージに属するフォントファイルの一覧
  ...
}

FontFile ::= {
  "path": RelativePath, //フォントファイルへの相対パス.例: "./fonts/foo.otf", "./fonts/foo.ttc"
  "math": Bool,         //数式フォントとして使うか否か.省略可,デフォルトでは false
  "contents" : OpentypeSingle | OpentypeCollection,
}

OpentypeSingle ::= {
  "type": "opentype_single",
  "name": String, //SATySFiで使用する場合のフォント名
}

OpentypeCollection ::= {
  "type": "opentype_collection",
  "names": Array[String], //SATySFiで使用する場合の,各要素のフォント名
}

これは現状の Satyristes ファイルに相当します(あくまでも大枠・暫定であり,Satyristes ファイルなどを参考として改良すべき箇所があるかもしれません).

依存解決後のコンフィギュレーション

文書ファイルやパッケージをビルドするために,依存する各パッケージのバージョンが固定されたら,以下の形式のいわゆるロックファイル satysfi.lock.yaml が文書ファイルやパッケージ直下に出力されます.SATySFiはこのロックファイルを読んでビルドします.

// ロックファイル
LockConfig ::= {
  "language": String,    //期待するSATySFiのバージョン
  "checksum": String,    //もとになった satysfi.yaml のチェックサム.更新の検査に使う
  "locks": Array[Lock],  //ロックされたパッケージ群.間接的なものも含めて依存するものが列挙される
}

// ロックされたパッケージ
Lock ::= {
  "name": LockName,                 //ロックされたパッケージの名称.例: "enumitem.2.0.0"
  "dependencies": Array[LockName],  //依存する他のロックされたパッケージたち
  "location": LockLocation,         //ロックされたパッケージの場所
}

// ロックされたパッケージを一意的に識別するための名前
LockName ::= String

// ロックされたパッケージの場所
LockLocation ::= LocalLocation | GlobalLocation

// ロックファイルからの相対パスでの指定
LocalLocation ::= {
  "type": "local",
  "path": RelativePath,
}

// LIBROOTからの相対パスでの指定
GlobalLocation ::= {
  "type": "global",
  "path": RelativePath,
}

備考

ここに掲げたパッケージシステムの設計は,ほぼ同様のものを既にSesterlという拙作の言語で実装し,Rebar3というErlang向けのビルドシステムと組み合わせて実用しています.この場合,SesterlがSesterl向けのコンフィグレーションファイル sesterl.yaml をもとにRebar3向けのコンフィギュレーションファイル rebar.config を生成し,Rebar3がそれを使って依存ライブラリのソースコードを取得するという形態をとっています.

まとめ

本稿では,SATySFi v0.1.zでの導入を検討しているパッケージシステムの大枠の設計を記述しました.これによりSATySFi本体がパッケージという形式でリリースの単位を扱うようになり,名前空間の分離なども明瞭になります.

特に依存パッケージの取得・配置処理などSATySFi本体が今のところ関知しない点についてもSATySFiが直接見るコンフィギュレーションファイルに記述するか否かについては議論が分かれそうに思いますが,プロトタイプ実装をつくって開発者やユーザの皆さんからご意見を伺ったりしたいと思います.

Clone this wiki locally