9. struct・参照型・パッケージ
9.1. この章で学ぶこと
structを使った型の作り方参照やポインタが必要になる場面
パッケージとモジュールの基本
この章から、Mojo らしい 型の考え方 がはっきり出てきます。
Python に慣れていると少し硬く感じますが、
まずは 「Mojo では struct が中心」 と押さえれば大丈夫です。
9.2. struct の基本
この章でいちばん大事なのは、Mojo ではユーザー定義型の中心が struct だ ということです。
9.2.1. struct とは何か
struct は、値をまとめて扱うための型です。
フィールドとメソッドをひとつにまとめられます。
ポイントは次の通りです。
structはユーザー定義型の基本になるフィールドはデフォルトで可変
初期化は
__init__で行うメソッドは
defで定義する継承ではなく、trait や generics で振る舞いを共有する
つまり、Python のクラスに少し似ていますが、
Mojo では struct を軸に型を組み立てる と捉えるとよいでしょう。
次の例は、カウンタを struct で表したものです。コンストラクタは def __init__(out self, …) と書き、まだ中身のないインスタンスを 初期化してから返す 役割を担います。フィールドを変えるメソッドは mut 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())
リスト-1: structs_what_counter.mojo
Counter(0) は __init__ 呼び出しの糖衣構文です。increment が value を書き換えられるのは、受け取りが mut self だからです。読み取りだけなら get のように self のまま で問題ありません。
9.2.2. メソッドと初期化
__init__ は、インスタンスを作るときの初期化処理です。
複数の形で定義することもできます。
@implicit を使うと、暗黙変換に関わる書き方もできます。
ただし、最初は 「自動で変換される場合がある」 くらいの理解で十分です。
Miles は「キロメートルをマイル換算して保持する型」です。@implicit を付けた __init__ があると、Miles を期待する場所に Float64 を渡しただけで、コンパイラがこのコンストラクタ経由で変換してくれます。
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)
リスト-2: structs_implicit_miles.mojo
describe_distance(10.0) は、一見すると Float64 を渡しているだけですが、実際には Miles が必要な文脈なので、10.0 から Miles への暗黙変換が行われます。暗黙変換は読みやすさと引き換えに追いづらくもなるため、意図して使うことが大切です。
9.2.3. trait の役割
Mojo では、コピーできるか、ムーブできるかといった性質を trait で表します。
たとえば次のような名前が出てきます。
CopyableMovable
ここで大事なのは、値の扱い方まで型の設計に入っている ことです。 Python よりも、データの動きがはっきり見える言語だと理解すると整理しやすくなります。
Copyable を付けると、コピー用の仕組み(.copy() やコピー初期化子など)が型に付与されます。次の Label は @fieldwise_init でフィールド順の初期化を自動生成しつつ、Copyable で「コピー可能なラベル」として扱えます。
@fieldwise_init
struct Label(Copyable):
var text: String
def main():
var a = Label("hello")
var b = a.copy()
print(a.text, b.text)
リスト-3: structs_copyable_label.mojo
a.copy() は a と同じ内容の別インスタンスを作ります。Mojo ではデフォルトで値型のコピーが暗に許されるとは限らないので、コピーを許す設計にするかを trait で明示する、という読み方ができます(Copyable は Movable も含意する、とマニュアルにあります)。
詳細: Structs
補足: 公式では「すべて struct」という考え方が強く出てきます。まずは、Mojo の型の中心が
structだと押さえれば十分です。
9.3. 参照型
struct だけでは表しにくい形がある ことも押さえておきます。
9.3.1. 自己参照をそのまま持てない
struct は、自分自身をそのままフィールドに持てません。
なぜなら、サイズが決められなくなるからです。
たとえば、「次のノードを持つノード」のような構造では、 そのままではうまく表せません。
同じ型の値をそのまま入れ子にすると、型のサイズが無限に再帰して定まらない、と理解するとよいです。実際には UnsafePointer や ArcPointer など、ヒープ上の別オブジェクトへの参照として「次」を表すパターンになります(詳細は Structs — reference types)。
9.3.2. そこで参照が必要になる
このような場面では、ポインタや参照の仕組みを使います。
代表的なものとして、次の名前が出てきます。
refPointerArcPointerUnsafePointer
ここでは、全部を細かく覚える必要はありません。 まずは、入れ子の構造や共有を表したいときに、参照の仕組みが必要になる と理解すれば十分です。
ref は、既存のコンテナの要素などを エイリアス(別名) として扱い、インプレース更新に使います。
def main():
var xs: List[Int] = [1, 2, 3]
for ref x in xs:
x *= 2
print(xs)
リスト-4: reference_ref_list_double.mojo
for ref x in xs の x は各要素への可変参照なので、x *= 2 が リストの中身そのものを更新します。
次に、ヒープに1要素分だけ確保して UnsafePointer で触る最小例です。alloc で領域を取り、init_pointee_copy で値を書き込み、読み出し後に free で返却します。
def main():
var ptr = alloc[Int](1)
ptr.init_pointee_copy(42)
print(ptr[])
ptr.free()
リスト-5: reference_heap_int_cell.mojo
これは連結リストの一片ではありませんが、「値型のフィールドだけでは表せないとき、ヒープ+ポインタで表す」 という発想の入り口になります。
9.3.3. unsafe が出てくる理由
ヒープ上のメモリを直接扱う場面では、unsafe が出てくることがあります。
これは、安全性の確認を自分で強く意識して扱う領域 です。
初学者の段階では、
「普通の struct では足りない場面で、より低水準な仕組みが出てくる」
と押さえておけば大丈夫です。
次の HeapInts は、可変長に近い「要素をヒープに載せる」例です。UnsafePointer[Int, MutExternalOrigin] は コンパイラの追跡外のミュータブル領域を指すポインタ型で、alloc/destroy_pointee/free を ペアで正しく呼ぶ責任がプログラマ側にあります。
struct HeapInts(Writable):
var data: UnsafePointer[Int, MutExternalOrigin]
var size: Int
def __init__(out self, *values: Int):
self.size = len(values)
self.data = alloc[Int](self.size)
for i in range(self.size):
(self.data + i).init_pointee_copy(values[i])
def __del__(deinit self):
for i in range(self.size):
(self.data + i).destroy_pointee()
self.data.free()
def get(self, i: Int) -> Int:
return (self.data + i)[]
def main():
var h = HeapInts(1, 2, 3)
print(h.get(0), h.get(2))
リスト-6: reference_heapints_managed.mojo
__del__(終了処理)で要素ごとに destroy_pointee し、最後にブロック全体を free しています。このように 資源の寿命を型のデストラクタに結びつける と、低水準な領域でも読みやすさを保ちやすくなります。
出典: Mojo Manual — structs/reference
補足: この節では、実装の細部よりも「なぜ参照やポインタが必要になるのか」をつかむのが大事です。
9.4. Packages
最後に、コードをどうわけて管理するかを見ます。
9.4.1. 基本
モジュールは
__init__.mojoを含む単位で考えるパッケージは
mojopkg.tomlで設定するimportやfrom ... import ...で読み込む__init__.mojoで公開 API を整理できる
次のレイアウトは、shapes ディレクトリをパッケージにした例です(__init__.mojo があるディレクトリがパッケージ名になります)。
packages_demo/
main.mojo
shapes/
__init__.mojo
rect.mojo
util.mojo
rect.mojo に Rect を定義し、util.mojo に補助関数を置きます。
@fieldwise_init
struct Rect:
var width: Int
var height: Int
def area(self) -> Int:
return self.width * self.height
リスト-7: rect.mojo
def min_dim(a: Int, b: Int) -> Int:
return a if a < b else b
リスト-8: util.mojo
__init__.mojo から サブモジュールの名前を再エクスポートすると、利用側はパッケージ名だけで済ませられます。
from .rect import Rect
from .util import min_dim
リスト-9: __init__.mojo
エントリの main.mojo では、from shapes import … の形で読み込みます。
from shapes import Rect, min_dim
def main():
var r = Rect(3, 4)
print(r.area())
print(min_dim(r.width, r.height))
リスト-10: main.mojo
packages_demo ディレクトリで mojo build main.mojo を実行するとビルドできます。パッケージ境界と公開 API の整理は、この __init__.mojo の役割として押さえてください。
9.4.2. Python と似ている点
import の感覚は、かなり Python に近いです。
相対 import も使えます。
.は同じ階層..はひとつ上の階層
このあたりは、Python の経験があると入りやすいです。
上の shapes/__init__.mojo の from .rect import Rect のように、パッケージ内の別ファイルを . 起点で指します。util も同様に from .util import min_dim と書くと、サブモジュール名を省略した import に寄せられます。
9.4.3. Mojo らしい点
一方で、パッケージとしてどう配るか は Mojo 独自の意識が少し強くなります。
mojopkg.toml では、パッケージの情報や依存関係をまとめます。
また、mojo package で配布用のビルドも行います。
ここで大事なのは、ファイルをわけるだけでなく、公開する境界も整理する ことです。
その役目を __init__.mojo が担います。
たとえば mojo package shapes -o shapes.mojopkg のようにビルドすると、ソースディレクトリの代わりに .mojopkg を import 先にできるようになります(パッケージ名は -o で付けたファイル名に対応。詳細は Packages と mojo package)。
詳細: Packages
補足: Python に似ていますが、Mojo では「パッケージとしてどう見せるか」を少しはっきり意識します。
9.5. この章を一文で言うと
Mojo では struct を中心に型を作り、必要に応じて参照を使い、パッケージで公開範囲を整理していきます。
9.6. まとめ
Mojo のユーザー定義型の中心は
structメソッドや初期化も
structにまとめて書く継承ではなく、trait や generics で振る舞いを共有する
自己参照や共有が必要な場面では、参照やポインタが出てくる
パッケージでは
__init__.mojoとmojopkg.tomlが重要になる
Python に似て見える部分はありますが、 この章からは 「型をどう設計するか」 がより大事になってきます。