第9章 struct・参照型・パッケージ
9.1 この章で学ぶこと
この章で学ぶ内容は以下のとおりです。
学ぶこと |
ポイント |
|---|---|
|
Mojo のユーザー定義型の中心。Python の |
|
参照 vs コピーがシンボル名に現れる |
|
|
参照型(ref / Pointer) |
自己参照やヒープ確保が必要な場面 |
パッケージとモジュール |
|
9.2 struct:なぜ class でなく struct か
Python の class との最大の違いは値型か参照型かです。
Python |
Mojo |
|
|---|---|---|
メモリ配置 |
ヒープ上のオブジェクト。変数はそこへのポインタ |
スタック上に直接配置(デフォルト) |
代入の意味 |
参照のコピー(同じオブジェクトを指す) |
値のコピー(独立した別のデータ) |
継承 |
|
継承なし。 |
vtable |
あり(動的ディスパッチ) |
なし(静的ディスパッチ) |
コンパイル時のサイズ確定 |
不可(任意のフィールドが追加できる) |
可(フィールドが固定) |
各項目について補足します。
代入の意味:参照共有 vs 値コピー
Python では b = a と書くと、b は a と同じオブジェクトを指します。一方を変更するともう一方にも反映されます。
# Python: b と a は同じオブジェクトを指す
class Point:
def __init__(self, x): self.x = x
a = Point(1)
b = a # 参照のコピー
b.x = 99
print(a.x) # → 99(a も変わる)
Mojo の struct では b = a が値のコピーになります。b と a は完全に独立したデータです。
# Mojo: b は a の独立したコピー
var a = Counter(1)
var b = a # 値のコピー
b.increment()
print(a.get()) # → 1(a は変わらない)
継承なし、trait による振る舞いの共有
Python の class は継承ができますが、継承は vtable(仮想関数テーブル)を必要とし、メソッド呼び出しのたびにポインタを辿る動的ディスパッチが発生します。
Mojo の struct は継承を持たず、メソッド呼び出しはコンパイル時に解決する静的ディスパッチです。
これにより関数呼び出しオーバーヘッドが排除されます。
振る舞いを共有したい場合は trait を使います(9.6 節)。
trait はインターフェースだけを定義し、実装の継承は行いません。
コンパイラは呼び出し先を静的に確定できるため、動的ディスパッチのコストはかかりません。
コンパイル時のサイズ確定
スタックに値を置くには、コンパイル時にそのサイズが確定している必要があります。
Python の class はプログラム実行中にフィールドを追加できるため、サイズが不確定でヒープにしか置けません。
Mojo の struct はフィールドが宣言時に固定されており、コンパイラがバイト数を計算してスタック領域を確保できます。
スタック配置によってヒープ確保のオーバーヘッドがなく、CPU キャッシュにも乗りやすくなります。
所有権システムとの関係
struct は値型であるため、第 10 章の所有権システムと直接連動します。
struct の変数は明確な「所有者」を持ち、スコープを抜けると __deinit__ が呼ばれてリソースが解放されます。
var 引数修飾子や ^(transfer)による所有権移動も struct の値型としての性質があってこそ機能します。
図9-1: Python class と Mojo struct の比較
左(Python class)はヒープに確保したオブジェクトへの参照を変数が保持し、b = a は同じオブジェクトを指す参照のコピーになる。右(Mojo struct)はスタックに値を直接配置し、var b = a は独立した値のコピーになる。vtable・継承の有無とコンパイル時サイズ確定の違いもあわせて示す。
ヒープに置きたい場合
struct のデフォルトはスタック配置ですが、ヒープに置きたい場合はポインタ型でラップします。
String や List[T] はすでにこのパターンを使っています。
String の struct ヘッダ(ポインタ、長さ、タグ)はスタックに置かれますが、文字列データ本体はヒープに確保されます。
struct 自体をヒープに置く場合は次の 2 つが主な選択肢です。
型 |
所有者 |
特徴 |
|---|---|---|
|
複数可(参照カウント) |
|
|
手動管理 |
|
ArcPointer[T] の使い方を簡単に示します。
from std.memory import ArcPointer
var p = ArcPointer(Counter(1))
p[].value = 42
var q = p # 参照カウントが増える(コピーではなく共有)
print(q[].value) # → 42
# p と q の両スコープを抜けるとカウントが 0 になり自動解放
struct の値型としての性質を保ちながら、複数の場所から同じデータを参照したいときに ArcPointer[T] を使います。
Pointer[T] の unsafe 操作は性能やシステムプログラミングを目的とした低レベルな操作が必要なときに使い、その詳細は 9.7.3 節で扱います。
9.3 struct の基本とアセンブリ
この節では最小の struct である Counter のコンパイル結果を追い、次の 3 点を確認します。
structがスタックに直接置かれること:ヒープ確保は一切発生しないmut selfとselfの違いが型システム上で区別されること:呼び出し側は同じアドレス渡しでも、書き換えてよいかがコンパイル時に確定するメソッド呼び出しが静的ディスパッチであること:実行時の判定コードが存在しない
Counter は value: Int を 1 つ持つ最小の struct です。
increment() で値を増やし、get() で読み取ります。
コンパイル後のアセンブリには mut self と self の違いが明確に刻まれています。
struct Counter:
var value: Int
def __init__(out self, start: Int):
self.value = start
def increment(mut self):
self.value += 1
def get(self) -> Int:
return self.value
def main():
var c = Counter(0)
c.increment()
print(c.get())
リスト9-1: structs_what_counter.mojo
このコードをコンパイルすると main() のアセンブリは次のようになります。
; def main():
30: subq $0x88, %rsp ; スタック 136 バイト確保
; var c = Counter(0)
37: xorl %eax, %eax ; start = 0 を準備
39: movl %eax, %edi ; edi にコピー(第 1 引数として使う)
3b: leaq 0x18(%rsp), %rsi ; self(Counter の領域)のアドレス
40: movq %rsi, 0x10(%rsp) ; そのアドレスを 0x10(%rsp) に保存
45: callq "_...Counter::__init__(::SIMD[::DType(int), ::SIMDLength(1)])" ; コンストラクタ
; c.increment()
4a: movq 0x10(%rsp), %rdi ; 保存しておいた &c を rdi に
4f: callq "_...Counter::increment(structs_what_counter::Counter)" ; mut self
; print(c.get())
54: movq 0x10(%rsp), %rdi ; 同じ &c を rdi に
59: callq "_...Counter::get(structs_what_counter::Counter)" ; self のみ
アセンブリを 4 つのブロックに分けて読みます。
① スタック確保(subq $0x88, %rsp)
0x88 は 136 バイトです。
Counter が持つ Int(8 バイト)そのものは小さくても、print(c.get()) まで含めた main() 全体で使う一時領域(後述する文字列オブジェクトの組み立て領域を含む)がまとめて確保されるため、値のサイズよりも大きな領域になります。
この 1 命令で main() 全体で使うローカル変数の領域が確保されます。
② コンストラクタ呼び出し(Counter::__init__)
37: xorl %eax, %eax ; 引数 start = 0 を準備
39: movl %eax, %edi ; edi へコピー
3b: leaq 0x18(%rsp), %rsi ; &c(self の書き込み先アドレス)を rsi に
40: movq %rsi, 0x10(%rsp) ; &c を後続の呼び出しのために保存
45: callq Counter::__init__ ; コンストラクタを呼ぶ
leaq 0x18(%rsp), %rsi は c の置き場所(スタック上のアドレス)を __init__ に渡しています。
out self は「まだ空のメモリ領域に自分で書き込む」という意味で、その領域のアドレスが rsi 経由で渡されます。
このアドレスは 0x10(%rsp) にも保存され、後続の increment() / get() 呼び出しで使い回されます。
③ c.increment()(Counter::increment(structs_what_counter::Counter))
4a: movq 0x10(%rsp), %rdi ; 保存しておいた &c を rdi に
4f: callq Counter::increment ; mut self を受け取る
mut self を宣言したメソッドには、②で保存した c のアドレス(&c)がそのまま渡されます。
increment の内部で self.value += 1 を行うと、このアドレスが指す元の c が直接書き換わります。
④ c.get()(Counter::get(structs_what_counter::Counter))
54: movq 0x10(%rsp), %rdi ; 同じ &c を rdi に
59: callq Counter::get ; self のみを受け取る
mut なしの self も、呼び出し側のコードとしては ③ と同じく &c を渡しているだけです。
Mojo コンパイラが「get は c を変更しない」と確定しているため、increment と同じくアドレス渡しのまま安全に実装できます。
注釈
以前の Mojo では、increment(mut self) のシンボル名には Counter::increment(Counter&) のように & が付き、get(self) のシンボル名 Counter::get(Counter) には付かないという表記上の違いがありました。
Mojo 1.0.0 ではこのシンボル表記が変わり、両者とも structs_what_counter::Counter という同じ形式で現れます(上記の ③ ④ で確認したとおりです)。
ただし呼び出し側の実際の命令(movq 0x10(%rsp), %rdi で &c を渡す)は mut self と self のどちらでも同じであり、この点は以前から変わっていません。
mut self か self かは、呼び出せる操作の範囲(書き換えてよいか)としてコンパイル時に区別されており、それはシンボル名の表記とは独立した、型システム上の制約です。
シンボル名がアセンブリに現れた形をまとめます。
メソッド定義 |
シンボル名 |
意味 |
|---|---|---|
|
|
|
|
|
同じく |
mut self か self かは、コンパイル時に確定する型システム上の制約です。
実行時に「どちらのメソッドか」を判定するコードは生成されません。
コラム: struct のスタックレイアウト
Counter は var value: Int を 1 つ持ちます。Int は 8 バイトです。プロローグの subq $0x88, %rsp(136 バイト確保)の内訳は:
0x18(%rsp)〜0x1f(%rsp)(8 バイト):Counter.valueの領域残りは
&cの退避スロット(0x10(%rsp))と、print(c.get())のための一時領域
Python の Counter(0) はヒープに PyObject を確保し、参照カウント・型ポインタ・value フィールドをヒープに配置します。Mojo の Counter 自体はスタック 8 バイト(Int 1 個分)で完結しており、ヒープ確保は一切ありません(136 バイトの大半は print() 呼び出しのための領域です)。
図9-2: Counter struct のアセンブリレベルの構造
左側に 3 つのアセンブリブロック(コンストラクタ・可変参照・読み取り)、右側にスタックレイアウトとシンボル名の違いを示す。subq $0x88, %rsp で 136 バイトのスタック領域を確保し、Counter.value が 0x18(%rsp) に直接配置される。mut self と self はいずれも structs_what_counter::Counter としてシンボル名に現れ、&c を渡す呼び出し規約自体は同じだが、書き換えてよいかどうかは型システム上コンパイル時に区別される。
9.4 __init__ と out self
__init__ は struct のコンストラクタです。
var c = Counter(0) と書いたとき、コンパイラは Counter::__init__ を呼んで c を初期化します。
Python の __init__ と名前は同じですが、第 1 引数の扱いが異なります。
Python では __init__ の self はすでに確保されたオブジェクトを指しています。
Mojo では __init__ の self はまだ何も書かれていない空のメモリ領域です。
そのため Mojo の __init__ では第 1 引数を out self と修飾します。
struct Counter:
var value: Int
def __init__(out self, start: Int):
self.value = start # 空のメモリに初めて書き込む
def main():
var c = Counter(5)
print(c.value)
リスト9-2: counter_init.mojo
out self は「このメソッドが self のすべてのフィールドを初期化する責任を持つ」という宣言です。
フィールドを書き忘れるとコンパイルエラーになります。
Python では未初期化フィールドを持つオブジェクトが実行時まで存在できますが、Mojo ではそれをコンパイル時に防ぎます。
__init__ の第 1 引数が Python の self と異なる理由を整理します。
引数修飾子 |
役割 |
|---|---|
|
|
|
呼び出し元の |
|
|
out は「self はまだ空の状態で渡される。このメソッドが初期化の責任を持つ」という意味です。
Python の __init__ が None を返すのと似た発想ですが、Mojo ではコンパイラがフィールドの未初期化を検出できます。
図9-3: __init__ における self の違いと初期化フロー
Python の self はすでに確保済みのオブジェクトを指すが、Mojo の out self は空のメモリ領域(0x10(%rsp))を指す。__init__ は leaq でそのアドレスを受け取り movq で値を書き込むことで初期化を完了する。フィールドを書き忘れた場合は実行時ではなくコンパイル時にエラーが検出される。
9.5 @implicit と暗黙変換
Mojo はデフォルトで暗黙の型変換を行いません。
describe_distance(10.0) と書いたとき、引数の型が Miles であれば Float64 はそのままでは渡せません。
@implicit は、この「明示的に変換しなくてよい」という許可をコンパイラに与えるデコレータです。
struct Miles:
var value: Float64
@implicit
def __init__(out self, km: Float64):
self.value = km * 0.621371
def describe_distance(m: Miles):
print("miles =", m.value)
def main():
describe_distance(10.0)
リスト9-3: structs_implicit_miles.mojo
describe_distance(10.0) が呼ばれたとき、コンパイラは次の手順を踏みます。
引数の型
Float64、期待する型Miles→ 型が合わないMilesの__init__に@implicitが付いているものを探すdef __init__(out self, km: Float64)が見つかる →Miles(10.0)に変換変換後の値を
describe_distanceに渡す
この変換はコンパイル時に完結します。 実行時の型チェックは発生しません。
ポイント |
詳細 |
|---|---|
|
この |
|
|
利点 |
呼び出し側を簡潔に書ける。リテラルをそのまま渡せる |
注意点 |
暗黙変換が多すぎると「なぜ型が合っているか」が追いにくくなる |
@implicit を付けない場合は describe_distance(Miles(10.0)) と明示的に書く必要があります。
変換の意図を読者に伝えたいときは明示、利便性を優先したいときは @implicit と使い分けます。
図9-4: @implicit による暗黙の型変換
左(Without @implicit)は Float64 をそのまま渡すとコンパイルエラーになり明示的な変換が必要。右(With @implicit)はコンパイラが Float64 → Miles(10.0) に自動変換する。下段にコンパイル時の変換プロセス 4 ステップと、利便性と明示性のトレードオフを示す。
9.6 trait:コピー・ムーブを型で表明する
trait は「この型が持つ能力」をコンパイル時に宣言する仕組みです。
Python の Protocol や ABC に相当しますが、Mojo では duck typing ではなく、型定義の段階で能力を明示します。
struct Label(Copyable): の (Copyable) は「この struct は Copyable trait を満たす」という宣言です。
コンパイラはこの宣言に基づいて a.copy() の呼び出しを許可します。
宣言がなければコンパイルエラーになります。
@fieldwise_init
struct Label(Copyable):
var text: String
def main():
var a = Label("hello")
var b = a.copy()
print(a.text, b.text)
リスト9-4: structs_copyable_label.mojo
Copyable を満たすには __copyinit__ メソッドが必要ですが、@fieldwise_init を付けた struct はコンパイラが自動生成します。
フィールドがすべてコピー可能な型(String, Int, Float64 など)であれば、(Copyable) と宣言するだけで .copy() が使えます。
trait |
意味 |
付けないとどうなるか |
|---|---|---|
|
|
|
|
|
|
|
コピーできるなら move もできる |
— |
Copyable だけ宣言しておけば Movable は自動で付いてきます。
逆に Movable だけ宣言した型は「move はできるがコピーはできない」という意味になります。
これは大きなバッファや排他リソースを持つ struct に使うパターンです。
コラム: なぜデフォルトでコピーを許さないか
Rust や Mojo のような所有権を持つ言語では、コピーが起きる場所を明示することがパフォーマンスに効きます。
大きな List や String を含む struct を暗黙にコピーすると、予期しないヒープ確保が起きます。
Copyable を型定義で宣言することで、「この型のコピーは意図的な設計」と明示できます。
Python では代入はすべて参照コピーのため、このような問題は起きません。 ただし「値を変えたはずが元も変わっていた」というバグの原因にもなります。
図9-5: trait による Copyable と Movable の使い分け
左から Copyable only(コピーもムーブも可)、Movable only(ムーブのみ)、No traits(どちらも不可)の 3 パターンを示す。Copyable ⊃ Movable の包含関係、@fieldwise_init による自動生成の有無、Python / Rust / Mojo のデフォルト動作の比較もあわせて示す。
9.7 参照型
struct は値型です。
変数に代入するとデータそのものがコピーされ、スタック上に実体が置かれます。
一方、参照型は「データの場所(アドレス)」を保持します。
データ自体はヒープ上にあり、複数の変数が同じ実体を指すことができます。
Mojo は値型を基本にしていますが、参照型が必要になる場面があります。
ref、Pointer、ArcPointer の 3 種類がその代表で、それぞれ「借用」「手動管理」「共有所有」という異なる役割を持ちます。
Mojo 1.0 では、かつて別の型だった UnsafePointer は Pointer に統合され、危険な操作(確保・解放・オフセット計算など)は unsafe_ 接頭辞のメソッドで明示するようになりました。
9.7.1 なぜ参照が必要になるか
struct の値はスタック上に配置されるため、コンパイル時にサイズが確定していなければなりません。
しかし次のようなケースではスタックだけでは対応できません。
struct Nodeがnext: Nodeフィールドを持つ場合 →NodeのサイズにNodeが含まれ無限再帰になる実行時まで長さが決まらない可変長データを扱う場合
複数の所有者が同一データを共有する場合
これらはいずれもヒープ上に値を置いてポインタ経由でアクセスすることで解決します。 Mojo が提供する主な参照型とポインタ型を目的別に整理すると次のとおりです。
型 |
種類 |
用途 |
|---|---|---|
|
参照(借用) |
既存の値への一時的なエイリアス。 |
|
非所有ポインタ |
ライフタイムと連動した安全なポインタ |
|
生ポインタ |
ヒープの直接操作( |
|
参照カウントポインタ |
複数の所有者が必要な場合。解放は自動 |
ref は既存の値を「借りる」だけです。
所有権は移動しません。
ref が有効なのは借りた元の値が生きている間だけです。
Pointer の unsafe 操作はヒープの生アドレスを持ちます。
解放の責任はプログラマにあります。
ArcPointer は参照カウントを内部に持ち、最後の所有者がスコープを抜けたときに自動解放します。
図9-6: 参照・ポインタが必要になる理由
スタックだけでは解決できない 3 つの問題(自己参照構造、可変長データ、複数所有者)と各問題に対応する解決策を示す。下段は ref、Pointer[T](安全な借用/unsafe 操作)、ArcPointer[T] の種類、用途、解放方法の一覧。
9.7.2 ref:インプレース更新
ref は「コピーせずに元の値を直接読み書きしたい」ときに使います。
for ループで最もよく登場します。
def main():
var xs: List[Int] = [1, 2, 3]
for ref x in xs:
x *= 2
print(xs)
リスト9-5: reference_ref_list_double.mojo
for ref x in xs: と書いた場合と書かなかった場合の違いを比較します。
# ref なし:各要素のコピーに対して操作する
for x in xs:
x *= 2 # xs の中身は変わらない
# ref あり:各要素への参照を直接書き換える
for ref x in xs:
x *= 2 # xs の中身が [2, 4, 6] に変わる
ref x は xs[i] のアドレスへのエイリアスです。
x *= 2 はそのアドレスに乗算結果を書き戻す操作になります。
アセンブリレベルでは lea でアドレスを取得して直接書き込む命令列になります(コピーを介しません)。
ref は借用なので、xs が有効な間だけ使えます。
ループの外に ref を持ち出すことはできません。
図9-7: ref によるインプレース更新
左(Without ref)は x が要素のコピーのため xs は変わらない。右(With ref)は ref x が xs[i] のアドレスへのエイリアスとなり xs が直接書き換わる。アセンブリレベルでは movq によるコピー操作と leaq + imulq によるアドレス経由の直接書き換えの違いとして現れる。
9.7.3 Pointer の unsafe 操作:ヒープの直接操作
Mojo 1.0 より前は、手動管理専用の UnsafePointer[T] という別の型が存在しました。
1.0 では Pointer[T] に統合され、危険な操作(確保・書き込み・解放など)は unsafe_ 接頭辞のメソッドで明示するようになりました。
unsafe_alloc でメモリを確保し、unsafe_free で解放するまでの責任はすべてプログラマが持ちます。
基本的な操作の流れは次のとおりです。
from std.memory.alloc import unsafe_alloc
var ptr = unsafe_alloc[Int](1) # ヒープに 8 バイト確保
ptr.unsafe_write(42) # アドレスに 42 を書き込む
print(ptr[]) # デリファレンスして読む → 42
ptr.unsafe_free() # 解放(忘れるとメモリリーク)
手動管理のリスクを避けるには __deinit__ を持つ struct にラップする RAII パターンが有効です。
アセンブリレベルの動作、複数要素の扱い、RAII パターンの詳細は 第12章 ポインタ・GPU・レイアウト で取り上げます。
図9-8: Pointer の unsafe 操作によるヒープの直接操作
4 段階のライフサイクル(Allocation → Initialize → Dereference → Free)をスタックとヒープのメモリ図とアセンブリ命令で示す。下段に危険性(use-after-free、double-free、リーク)と RAII パターンによる安全化、使用上のポイントをあわせて示す。
9.7.4 ArcPointer:参照カウントによる共有
Pointer の unsafe 操作は所有者が 1 人であることを前提にします。
複数の変数が同じヒープオブジェクトを共有したい場合は ArcPointer(Atomic Reference Counting Pointer)を使います。
ArcPointer は内部に参照カウンタを持ちます。
arc2 = arc のようにコピーするたびにカウントが増え、スコープを抜けるたびに減ります。
カウントが 0 になった時点でヒープが自動解放されます。
from std.memory import ArcPointer
@fieldwise_init
struct Config(Copyable):
var value: Int
def main():
var arc = ArcPointer(Config(42))
var arc2 = arc # 参照カウントが 2 になる
arc2[].value = 99
print(arc[].value) # 99(同じヒープオブジェクトを共有)
# arc, arc2 がスコープを抜けるとカウントが 0 になり自動解放
リスト9-6: reference_arc_pointer_config.mojo
コードのポイントを整理します。
ArcPointer(Config(42)):Configをヒープ上に置き、参照カウント 1 で初期化var arc2 = arc:カウントが 2 になる。arcとarc2は同じヒープ上の 1 つのConfigを指すarc2[].value = 99:arc2経由で書き換えると、arc[]からも99が見えるスコープを抜けると
arc2のカウントが先に 1 減り、最後にarcのカウントが 0 になって解放される
Pointer の unsafe 操作との最大の違いは、カウントが 0 になった瞬間に自動解放される点です。
unsafe_free を手動で呼ぶ必要がなく、__deinit__ も不要です。
コラム: Arc の “A” は Atomic(アトミック)
ArcPointer の参照カウンタの増減は CPU のアトミック命令(x86 では lock xaddq、ARM では stlxr)で行われます。
複数スレッドから同じオブジェクトを共有しても安全なのはこのためです。
ただし、アトミック操作はメモリバリアを伴うため、シングルスレッドのみの用途ではカウント操作のコストが無駄になります。
スレッドをまたがない場合は Pointer の unsafe 操作 + RAII パターンのほうがオーバーヘッドが少なくなります。
図9-9: ArcPointer による参照カウントの共有
5 段階のライフサイクル(初期作成 RC=1 → コピーで RC=2 → 共有書き換え → arc2 スコープ抜けで RC=1 → arc スコープ抜けで RC=0、自動解放)をメモリ図で示す。最終段階で lock xaddq によるアトミックデクリメントと callq destructor が実行される。下段に Pointer の unsafe 操作との所有者、解放、コストの比較表を示す。
9.7.5 Pointer:安全な非所有ポインタ
Pointer[T] は、標準ライブラリが提供する安全な非所有ポインタです。
データを所有せず、参照先のライフタイム(origin)情報をコンパイラが追跡する点が、9.7.3 節で見た unsafe_ 系メソッドによる手動管理との違いです。
unsafe_alloc/unsafe_free のような unsafe 操作は「解放済みアドレスにアクセスする」「2 回解放する」などのミスをコンパイラが検出できません。
Pointer[T] を安全な借用として使う場合はライフタイム情報を持つため、こうした誤りを型システムで防ぎやすくなっています。
# 概念例(API は版により異なります)
var x: Int = 42
var p = Pointer(to=x) # x を指す安全なポインタ
print(p[]) # 42
ref がスタック上の既存値への短命な借用であるのに対し、Pointer はライフタイム情報を持つ参照として、より広い場面で使えます。
ただし生メモリの確保・解放や任意のアドレス操作が必要な低レベル処理では、引き続き Pointer の unsafe 操作(unsafe_alloc/unsafe_free等)を使います。
9.8 Packages
Package(パッケージ)は __init__.mojo を持つディレクトリです。
複数の .mojo ファイルに分散したコードをひとつの名前空間としてまとめ、利用者には from shapes import Rect のような単純な import 経路を提供します。
Python の __init__.py と同じ発想ですが、Mojo では型情報がコンパイル時に確定するため、パッケージの境界が「何を公開するか」の明確な仕切りになります。
利用者はパッケージ内部のファイル構成を知らなくても、__init__.mojo に並べられた名前だけを使えます。
9.8.1 ディレクトリ構成と各ファイルの役割
サンプルは shapes パッケージを main.mojo から使う構成です。
packages_demo/
main.mojo ← エントリポイント
shapes/
__init__.mojo ← 公開 API をここに集める
rect.mojo ← Rect 型の定義
util.mojo ← 補助関数
それぞれのファイルを順に見ていきます。
rect.mojo:Rect 型を定義します。
@fieldwise_init
struct Rect:
var width: Int
var height: Int
def area(self) -> Int:
return self.width * self.height
リスト9-7: rect.mojo
shapes パッケージの内部モジュールです。
Rect は shapes/rect.mojo に定義されていますが、利用者はこのパスを直接 import する必要はありません。
util.mojo:パッケージ内で使う補助関数を置きます。
def min_dim(a: Int, b: Int) -> Int:
return a if a < b else b
リスト9-8: util.mojo
init.mojo:パッケージの公開 API を一か所にまとめます。
from .rect import Rect
from .util import min_dim
リスト9-9: __init__.mojo
from .rect import Rect の . は「同じ shapes/ ディレクトリ内の rect.mojo」を指す相対 import です。
__init__.mojo に再エクスポートを書いておくことで、利用者は from shapes import Rect とだけ書けばよく、ファイル分割の内部構造を隠せます。
main.mojo:利用者側のコードです。
from shapes import Rect, min_dim
def main():
var r = Rect(3, 4)
print(r.area())
print(min_dim(r.width, r.height))
リスト9-10: main.mojo
from shapes import Rect, min_dim の shapes はディレクトリ名です。
main.mojo と同じ階層に shapes/ ディレクトリがあり、その中に __init__.mojo があれば Mojo はそこを package として認識します。
要素 |
Python との比較 |
Mojo |
|---|---|---|
パッケージの定義 |
|
|
公開 API の整理 |
|
|
相対 import |
|
同じ( |
配布用ビルド |
|
|
配布用ビルドについて補足します。
mojo precompile shapes -o shapes.mojoc を実行すると、shapes/ ディレクトリ全体がコンパイル済みの単一ファイル shapes.mojoc に変換されます。
利用者は shapes/ ディレクトリを手元に置かなくても shapes.mojoc だけで import できます。
ソースから import するときはディレクトリ名がパッケージ名になりますが、コンパイル済みパッケージから import するときはファイル名がパッケージ名になります(名前を変えたいときは .mojoc をリネームするのではなく mojo precompile をやり直します)。
ただし .mojoc は、それを生成したコンパイラのバージョンに強く結び付く形式で、汎用の配布形式ではありません。
別バージョンのコンパイラで読み込むとコンパイルエラーになります。
バージョンをまたいで配布したい場合は、ソースをそのまま配る source package(shapes/ ディレクトリ一式)のほうが安全です。
図9-10: __init__.mojo による公開 API の整理
ディレクトリ構成と各ファイル(rect.mojo・util.mojo・__init__.mojo・main.mojo)の関係を示す。__init__.mojo が内部モジュールを再エクスポートすることで利用者はシンプルな import 経路を使える。左下に Python との比較表、中央下にパッケージのメリット、右下に import パターン 3 種を示す。
9.9 まとめ
本章で扱った内容を以下にまとめます。
要素 |
押さえるべきこと |
|---|---|
|
スタック配置・値型・継承なし・静的ディスパッチ |
|
呼び出し側は同じアドレス渡し。書き換え可否は型システムで区別される |
|
|
|
コピー・ムーブを型定義で明示する。デフォルトは禁止 |
参照の種類 |
|
パッケージ |
|