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__ 呼び出しの糖衣構文です。incrementvalue を書き換えられるのは、受け取りが 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 で表します。

たとえば次のような名前が出てきます。

  • Copyable

  • Movable

ここで大事なのは、値の扱い方まで型の設計に入っている ことです。 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 で明示する、という読み方ができます(CopyableMovable も含意する、とマニュアルにあります)。

詳細: Structs

出典: Mojo Manual — structs

補足: 公式では「すべて struct」という考え方が強く出てきます。まずは、Mojo の型の中心が struct だと押さえれば十分です。

9.3. 参照型

struct だけでは表しにくい形がある ことも押さえておきます。

9.3.1. 自己参照をそのまま持てない

struct は、自分自身をそのままフィールドに持てません。 なぜなら、サイズが決められなくなるからです。

たとえば、「次のノードを持つノード」のような構造では、 そのままではうまく表せません。

同じ型の値をそのまま入れ子にすると、型のサイズが無限に再帰して定まらない、と理解するとよいです。実際には UnsafePointerArcPointer など、ヒープ上の別オブジェクトへの参照として「次」を表すパターンになります(詳細は Structs — reference types)。

9.3.2. そこで参照が必要になる

このような場面では、ポインタや参照の仕組みを使います。

代表的なものとして、次の名前が出てきます。

  • ref

  • Pointer

  • ArcPointer

  • UnsafePointer

ここでは、全部を細かく覚える必要はありません。 まずは、入れ子の構造や共有を表したいときに、参照の仕組みが必要になる と理解すれば十分です。

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 xsx は各要素への可変参照なので、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]コンパイラの追跡外のミュータブル領域を指すポインタ型で、allocdestroy_pointeefreeペアで正しく呼ぶ責任がプログラマ側にあります。

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 しています。このように 資源の寿命を型のデストラクタに結びつける と、低水準な領域でも読みやすさを保ちやすくなります。

詳細: Structs — reference types

出典: Mojo Manual — structs/reference

補足: この節では、実装の細部よりも「なぜ参照やポインタが必要になるのか」をつかむのが大事です。

9.4. Packages

最後に、コードをどうわけて管理するかを見ます。

9.4.1. 基本

  • モジュールは __init__.mojo を含む単位で考える

  • パッケージは mojopkg.toml で設定する

  • importfrom ... import ... で読み込む

  • __init__.mojo で公開 API を整理できる

次のレイアウトは、shapes ディレクトリをパッケージにした例です(__init__.mojo があるディレクトリがパッケージ名になります)。

packages_demo/
  main.mojo
  shapes/
    __init__.mojo
    rect.mojo
    util.mojo

rect.mojoRect を定義し、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__.mojofrom .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 で付けたファイル名に対応。詳細は Packagesmojo package)。

詳細: Packages

出典: Mojo Manual — packages

補足: Python に似ていますが、Mojo では「パッケージとしてどう見せるか」を少しはっきり意識します。

9.5. この章を一文で言うと

Mojo では struct を中心に型を作り、必要に応じて参照を使い、パッケージで公開範囲を整理していきます。

9.6. まとめ

  • Mojo のユーザー定義型の中心は struct

  • メソッドや初期化も struct にまとめて書く

  • 継承ではなく、trait や generics で振る舞いを共有する

  • 自己参照や共有が必要な場面では、参照やポインタが出てくる

  • パッケージでは __init__.mojomojopkg.toml が重要になる

Python に似て見える部分はありますが、 この章からは 「型をどう設計するか」 がより大事になってきます。