第8章 型・演算子・制御・エラー

8.1 この章で学ぶこと

この章で学ぶ内容は以下のとおりです。

表8-1: この章で学ぶこと

学ぶこと

ポイント

struct と名義的型システム

フィールドが同じでも名前が違えば別の型

SIMD 型とアセンブリ

SIMD[DType.float64, 4] + 10.0addpd 命令になる様子

^ の 2 つの意味

ビット XOR(二項演算子)と所有権転送(後置演算子)

エラー処理

try/except/else/finallyraise e^

8.2 Types

8.2.1 struct と名義的型システム

Mojo では多くの型を struct で定義します。 設計上の特徴は 名義的(nominal) な型システムにあります。

# x, y を持つ struct が二つあっても、名前が違えば別の型(名義的)


@fieldwise_init
struct Point2D:
    var x: Int
    var y: Int


@fieldwise_init
struct PointXY:
    var x: Int
    var y: Int


def main():
    var p = Point2D(10, 20)
    var q = PointXY(10, 20)
    print(p.x, p.y, q.x, q.y)

リスト8-1: types_struct_nominal.mojo

@fieldwise_init は、フィールドの宣言順に引数を取る __init__ を自動生成するデコレータです。 Point2D(10, 20) と書けば x=10, y=20 で初期化できます。 手書きのコンストラクタが不要になります。

Point2DPointXY はどちらも x: Inty: Int を持ちますが、別の型として扱われます。 次のような代入はコンパイルエラーになります。

var p = Point2D(10, 20)
var q: PointXY = p   # エラー: cannot implicitly convert 'Point2D' to 'PointXY'

フィールドの形(構造)が同じでも、型の名前が違えば別の型です。 これが 名義的型システム(nominal type system)の意味です。

Python のダックタイピングや TypeScript の構造的型システムとは正反対の設計です。 Python では「xy を持つオブジェクトなら何でもよい」という発想ですが、Mojo では「Point2D という名前の型でなければならない」と厳密に区別します。

表8-2: struct と名義的型システム

名義的(Mojo・Rust・Go)

構造的(TypeScript・Duck typing)

型の区別

名前で決まる

フィールドの形で決まる

Point2D vs PointXY

別の型(代入不可)

同じ形なら同じ型

コンパイル時

型の間違いを確実に検出

予期しない「型が一致」が起こり得る

実行時コスト

なし(型情報はコンパイル時に消える)

なし

struct と名義的型システム

図8-1: 名義的型システム(Mojo・Rust・Go)と構造的型システム(TypeScript・Duck typing)の比較

名義的型システムの利点は、意図の明示です。 Point2DPointXY が偶然同じフィールドを持っていても、それらを互いに代入できる設計上の理由はありません。 コンパイラが「混同しないように」強制することで、大規模なコードで型の取り違えが起きにくくなります。

コラム: Rust・Go との比較

Rust と Go も名義的型システムを採用しています。 struct Foo { x: i32 }struct Bar { x: i32 } は Rust でも別の型です。

Go は例外的に、インターフェースだけは構造的型システムで判定します。 「メソッドを持っていれば実装したとみなす」という Go 独自の設計です。

Mojo は trait(後述)を使ってこの問題を解決しており、インターフェースの適合は明示的に宣言します。

8.2.2 組み込み型の一覧

Mojo の主な組み込み型を一覧で示します。

表8-3: 組み込み型の一覧

サイズ(x86)

用途

Int

8 バイト(64 bit)

汎用整数。ポインタサイズと同じ

Float64

8 バイト

IEEE 754 倍精度浮動小数点

Float32

4 バイト

単精度。GPU での行列演算によく使う

Bool

1 バイト

True / False

String

可変長(ヒープ)

UTF-8 文字列。所有権管理対象

List[T]

可変長(ヒープ)

動的配列。要素型 T はコンパイル時に確定

各型について、Python との主な違いを補足します。

Int:固定サイズの整数

Python の int は任意精度(何桁でも扱える)ですが、Mojo の Int64 bit 固定です。 x86-64 ではポインタと同じサイズで、レジスタ 1 本に収まります。 1_000_000_000_000 のような大きな整数は扱えますが、数百桁の巨大な数は扱えません。

ビット幅を明示したい場合は Int8 / Int16 / Int32 / Int64(符号あり)や UInt8 / UInt16 / UInt32 / UInt64(符号なし)を使います。 GPU カーネルや低レベル処理でビット幅を制御するときに有用です。

Float64 / Float32:精度の選択

Python の float は常に IEEE 754 倍精度(64 bit) ですが、Mojo では Float32(単精度 32 bit)も明示的に選べます。 GPU の行列演算や機械学習の重みでは Float32 が標準で、メモリ使用量とスループットの両面で有利です。

String:所有権を持つ UTF-8 文字列

Python の文字列はイミュータブルで、インタープリタが内部でインターン(共有)します。 Mojo の Stringヒープ上にデータを持ち、所有権で管理されます。 第6・7章のアセンブリ出力で、文字列オブジェクトが「データポインタ、長さ、タグ、関数ポインタ 2 本」というレイアウトでスタック上に展開される様子を見たのがこの型です。

スコープを抜けると __deinit__ が呼ばれ、ヒープが返却されます。 Python の GC とは異なり、解放タイミングはコンパイル時に確定します。

List[T]:均一型の動的配列

Python の list はどんな型の値でも混在させられますが、Mojo の List[T]要素型 T をコンパイル時に確定させます。 List[Int] には Int しか入れられません。 これにより、要素アクセスのたびに型チェックが不要になり、C の配列に近いパフォーマンスが得られます。

8.2.3 SIMD 型とアセンブリ

SIMD は最もアセンブリに近いところで動く型です。

def main():
    var v4 = SIMD[DType.float64, 4](1.0, 2.0, 3.0, 4.0)
    var bumped = v4 + 10.0
    var v1 = SIMD[DType.float64, 1](2.5)

    print(bumped[0], bumped[3], v1[0])

リスト8-3: types_simd_basics.mojo

SIMD[DType.float64, 4] は 64 bit 浮動小数点を 4 つひとかたまりとして扱います。 このコードをコンパイルすると、加算 v4 + 10.0 が次のアセンブリになります。

; var bumped = v4 + 10.0
1d4:  movapd   0x22144(%rip), %xmm0   ; [10.0, 10.0] を 128 bit レジスタに load
1dc:  addpd    %xmm0, %xmm2           ; xmm2 の上位 2 要素を同時加算
1e0:  addpd    %xmm0, %xmm1           ; xmm1 の下位 2 要素を同時加算

addpdadd packed double)は 1 命令で 2 つの Float64 を同時に加算します。 4 要素のために 2 命令で完結します。

表8-4: SIMD 型とアセンブリ

方法

命令数(4 要素加算)

命令

スカラーループ(Python/動的型)

4 命令

addsd(1 要素ずつ)

SIMD[DType.float64, 4](SSE2)

2 命令

addpd(2 要素ずつ)

AVX 有効時

1 命令

vaddpd ymm(4 要素まとめて)

SIMD 型による並列演算

図8-2: スカラー演算(4 命令)・SIMD/XMM(2 命令)・AVX/YMM(1 命令)の比較。右下に x86-64 と ARM NEON のレジスタ対応も示す。

コラム: SIMD と CPU レジスタの対応(x86-64)

x86-64 には浮動小数点演算専用のベクタレジスタがあります。

  • XMM レジスタ(128 bit): Float64 × 2 または Float32 × 4 を格納できる

  • YMM レジスタ(256 bit): Float64 × 4 または Float32 × 8(AVX が必要)

  • ZMM レジスタ(512 bit): Float64 × 8 または Float32 × 16(AVX-512 が必要)

SIMD[DType.float64, 4] は 256 bit(Float64 × 4)なので、AVX がある CPU なら YMM レジスタ 1 本に収まります。 今回の .asm は SSE2 ベースライン(x86_64-apple-macosx15.0)なので、XMM 2 本+ addpd 2 命令になっています。

GPU では同じ概念がさらに大規模に展開されます(32 スレッドが SIMD で動く warp)。

コラム: ARM の SIMD(NEON)

Apple Silicon(M1/M2/M3)や多くのスマートフォン向け ARM プロセッサには、NEON(Advanced SIMD)というベクタ演算機構があります。

レジスタ構成

ARM は V0〜V31 の 32 本、各 128 bit のベクタレジスタを持ちます。 x86-64 の XMM レジスタに相当します。

  • V0.2D(128 bit): Float64 × 2 を格納

  • V0.4S(128 bit): Float32 × 4 を格納

x86-64 との命令対応

操作

x86-64(SSE2)

ARM(NEON)

Float64 × 2 の加算

addpd xmm0, xmm1

fadd v0.2d, v1.2d, v2.2d

Float32 × 4 の加算

addps xmm0, xmm1

fadd v0.4s, v1.4s, v2.4s

定数を全レジスタにセット

movapd

dup v0.2d, x0

SIMD[DType.float64, 4] の ARM での展開

NEON のレジスタ幅は 128 bit です。 Float64 × 4 は 256 bit なので、x86 の SSE2 と同様に V レジスタ 2 本+ fadd 2 命令に展開されます。

; ARM NEON での Float64 × 4 加算(概略)
fadd  v0.2d, v0.2d, v2.2d   ; 上位 2 要素
fadd  v1.2d, v1.2d, v3.2d   ; 下位 2 要素

コラム(続き): NEON の制約と Mojo の SIMD

x86 の AVX(256 bit YMM レジスタ)に相当する 256 bit NEON は存在しません。 ARM の NEON は 128 bit 固定です。 サーバー向けの ARM には可変長ベクタ命令 SVE(Scalable Vector Extension)がありますが、Apple Silicon は NEON のみです。

Mojo の SIMD 型はターゲット CPU に合わせてコンパイラが最適な命令を選択するため、同じソースコードを x86-64 と ARM の両方でコンパイルできます。

8.2.4 型変換は明示が基本

Python は intfloat が混在する式を自動的に float に昇格させますが、Mojo はそれを行いません。 型変換が必要なときは、プログラマが明示的に書く必要があります。 コンパイラが「どこで変換コストが発生するか」を正確に把握できるようにするための設計です。

def main():
    var n: Int = 7
    var half = Float64(n) / 2.0
    print(half)

リスト8-5: types_explicit_cast.mojo

Python の / は自動的に float 除算になりますが、Mojo では型が混在していると コンパイルエラーになります。 Float64(n) と明示することで変換が起きます。 これはコンパイラが「いつ変換コストが発生するか」を正確に把握するための設計です。

8.3 Operators

Mojo の算術、比較、論理演算子は Python とほぼ同じです。 + - * / // % **== != < > and or not は動作も構文も変わりません。

この節では Mojo 独自の意味を持つ ^ に絞って説明します。 Python では ^ はビット XOR の記号ですが、Mojo ではそれに加えて所有権転送という全く異なる用途があります。

8.3.1 ^ の 2 つの意味

^ は書かれた位置によって意味が変わります。 2 つの値の間(二項演算子) として使えばビット XOR、値の直後(後置演算子) として使えば所有権転送です。

def consume_list(var xs: List[Int]):
    print("len:", len(xs))


def main():
    print("XOR:", 5 ^ 3)

    var data: List[Int] = [1, 2, 3]
    consume_list(data^)

    var s = String("ha")
    print("repeat:", s * 3)

リスト8-7: operators_xor_and_move.mojo

表8-5: ^ の 2 つの意味

書き方

読み方

動作

a ^ b(二項演算子)

ビット XOR

5 ^ 30b0101 XOR 0b00116

expr^(後置演算子)

transfer(所有権転送)

consume_list(data^)data の所有権を関数に渡し切る

同じ記号が 2 つの意味を持つのは見た目の紛らわしさがありますが、二項か後置かで明確に区別できます。

8.4 Control flow

Mojo の制御構文は Python の構文をほぼそのまま踏襲しています。 if / elif / elsewhilebreak / continue、ループの else は動作も見た目も変わりません。

Python と異なる点は for ループの変数の扱いです。 通常の for x in list: はコピーを受け取るため元のリストは変わりませんが、for ref x in list: と書くと参照として受け取り、ループ内で書き換えると元のリストに反映されます。

8.4.1 if / for / while

構文は Python とほぼ同じです。

表8-6: if / for / while

構文

Python との違い

if

完全に同じ。elif / else も同様

for x in list:

コピーでループする(x を変えても元が変わらない)

for ref x in list:

参照でループする(x を変えると元も変わる)

while

完全に同じ

break / continue

完全に同じ

ループの else

break しなかった場合のみ実行(Python と同じ)

8.4.2 for ref :インプレース更新

次のコードは List[Int] の要素を走査し、奇数の要素を 1 減らして偶数にそろえます。 for ref v in values: で各要素の参照を取得し、条件に合う場合は v = v - 1 で直接書き換えています。

def main():
    var values: List[Int] = [1, 4, 7, 3, 6, 11]
    for ref v in values:
        if v % 2 != 0:
            v = v - 1
    print("after evenize (len):", len(values))

リスト8-8: control_flow_for_ref_inplace.mojo

for ref v in values:ref を忘れると、v はコピーになります。 元のリストを変更したいときは ref が必要です。

8.5 Errors

Mojo のエラー処理は Python の try / except / else / finally 構文を踏襲しています。 Python を知っていれば構文はほぼ読めますが、2 点が異なります。

  1. raises の明示:エラーを投げる可能性のある関数はシグネチャに raises を書かなければなりません。Python のように「投げるかもしれない」が暗黙になることはありません。

  2. raise e^ による再送出:エラーを捕捉した後に再送出するときは raise e^ と書きます。^ は所有権転送(8.3.1 参照)で、コピーを避けて安全に再送出します。

8.5.1 エラー処理のしくみ

Mojo のエラー処理のしくみを他の言語と比較しながら整理します。

表8-7: エラー処理のしくみ

要素

Python

Mojo

投げる

raise ValueError(...):シグネチャに現れない

raise Error(...):関数に raises が必要

受け取る

try / except e:

同じ構文

成功時

else: ブロック(オプション)

同じ

後片付け

finally: ブロック

同じ

再送出

raise e(所有権コピーが起きる)

raise e^(所有権を転送して再送出)

8.5.2 コード例

process_recordid の値に応じて 2 種類のエラーを投げます。 id > 999 は「レコード不在」として内側の except でその場で処理し、id < 0 は「致命的なエラー」として raise e^ で外側の try ブロックへ再送出します。 else: は例外が起きなかったときだけ、finally: は常に実行されます。

def process_record(id: Int) raises -> String:
    if id < 0:
        raise Error("invalid record ID: must be non-negative")
    if id > 999:
        raise Error("record not found")
    return String("record_") + String(id)


def main():
    try:
        var ids: List[Int] = [5, 0, 1001, -3, 42]
        for i in range(len(ids)):
            var id = ids[i]
            var result: String
            try:
                print(String("try  => id: "), id)
                if id == 0:
                    continue
                result = process_record(id)
            except e:
                if id < 0:
                    print("except => fatal, re-raise:", e)
                    raise e^
                print("except => handled:", e)
            else:
                print(String("else => success: "), result)
            finally:
                print(String("finally => done with id: "), id)
    except e:
        print("outer caught:", e)

リスト8-10: errors_try_except_reraise.mojo

表8-8: コード例

コードの箇所

何を示しているか

raises -> String:

この関数がエラーを返す可能性を明示

内側の try/except

id > 999 はその場で処理、id < 0raise e^ で外側へ

raise e^

エラーオブジェクトの所有権を転送して再送出。コピーなし

else:

except に入らなかった(成功した)ときだけ実行

finally:

成功・失敗を問わず必ず実行

外側の try/except

内側で処理しきれなかったエラーを最後に受け取る

エラー処理の比較

図8-3: Python と Mojo のエラー処理比較(上)と process_record の制御フロー(中)。id < 0raise e^ で外側へ再送出、id > 999 はその場で処理。

コラム: raise e^ の ^ が必要な理由

raise eraise e^ の違いは所有権です。

raise e はエラーオブジェクトをコピーして再送出しようとします。 しかし Mojo のエラー型が Copyable でない場合、コンパイルエラーになります。

raise e^ は所有権を転送します。 コピーコストがなく、コンパイラも「e はこれ以降使えない」と確定できます。 再送出する場合は常に raise e^ が推奨されます。

8.6 まとめ

本章で扱った内容を以下にまとめます。

表8-9: まとめ

要素

押さえるべきこと

struct(名義的)

フィールドが同じでも名前が違えば別の型。コンパイル時に確定

SIMD[DType.T, N]

N 個の T 型をまとめた型。加算が addpd(2 要素同時)等になる

型変換

暗黙変換に頼らず Float64(n) 等で明示が基本

^(後置)

所有権転送。for refraise e^ でも同じ概念が使われる

for vs for ref

コピーか参照か。インプレース更新には ref が必要

raisestry/except

エラーの可能性をシグネチャで明示。再送出は raise e^