8. 言語基礎(2)— 型・演算子・制御・エラー

8.1. この章で学ぶこと

  • 型の基本(struct・リテラル・型変換)

  • 演算子の基本(^ の意味に注意)

  • 制御構文とエラー処理(try / except / raises

この章では、Mojo の基本的な書き方をまとめて確認します。 前の章と同じく、わからない言葉があればマニュアルも見ながら読み進めてください。

本章のコード例は Mojo 0.26 系 で確認しています。 uv 経由で Mojo を入れている場合は、手元の版をそろえるために uv run mojo builduv run mojo run を使うのがおすすめです。
版によって書き方が変わることがあるので、最新情報は Mojo Manual を確認してください。

8.2. Types

8.2.1. struct

Mojo では、多くの型を struct で定義します。 struct は、データに名前を付けてまとめる仕組みだと捉えるとよいでしょう。

たとえば「名前」と「年齢」を持つ型を作りたいときは、struct でその形を決めます。 そしてプログラムでは、その 型名 を使って値を扱います。

Mojo の型は基本的に 名前で区別されます。 これを 名義的(nominal) と言います。

  • フィールドの形が似ていても

  • 別の名前で定義した struct なら

  • 別の型として扱われる

ということです。

# 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)

リスト-1: types_struct_nominal.mojo

8.2.2. よく使われる型

Mojo では、次のような型をよく使います。

  • 整数や浮動小数

  • Bool

  • String

  • Tuple

  • List / Dict / Set

  • Optional

どれも、ふだんのプログラムでよく出てくる基本的な型です。

def main():
    var i: Int = 42
    var x: Float64 = 3.25
    var s: String = String("mojo")
    var ok: Bool = True
    var pair: Tuple[Int, String] = (7, String("seven"))
    var xs: List[Int] = [1, 2, 3]

    print(i, x, s, ok, pair[0], len(xs))

リスト-2: types_builtin_kinds.mojo

8.2.3. SIMD 型

SIMD は、同じ種類の値をまとめて持つ型です。 ふつうの IntFloat64 は 1 個の値ですが、SIMD は複数の値をひとかたまりで持てます。

たとえば SIMD[DType.float64, 4] は、Float64 を 4 個まとめたような型です。

+* などの演算をすると、各要素に対して同じ計算が行われます。 そのため、まとめて計算したい場面で便利です。

SIMD は、長さが自由に変わる List とは違い、要素数が型に含まれるのがポイントです。 CPU の SIMD 命令や GPU の処理につながる型として読むとイメージしやすくなります。

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])

リスト-3: types_simd_basics.mojo

8.2.4. リテラル

数値や文字列をそのまま書いたものを リテラル と言います。

たとえば次のようなものです。

  • 42

  • 3.14

  • "hello"

Mojo では、こうしたリテラルは必要に応じて IntFloat64 などの実際の型として使われます。

def main():
    var n: Int = 42
    var x: Float64 = 1.25
    print(n, x)

リスト-4: types_literals.mojo

8.2.5. 暗黙の型変換(数値の型昇格)

Mojo では、数値の型が違っていても自動で全部そろえてくれる、とはあまり考えない方が安全です。 整数と浮動小数を混ぜるときは、自分で型をそろえるのが基本です。

たとえば IntFloat64 と一緒に使いたいなら、Float64(n) のように明示的に変換します。

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

リスト-5: types_explicit_cast.mojo

Float64(n) は、nFloat64 に変換する書き方です。 「どの型で計算したいかを自分で決める」 と理解するとよいでしょう。

8.2.6. List[T] の初期化

Mojo 0.26 系 では、List は次のように書くとよいでしょう。

var xs: List[Float64] = [a, b, c]

左辺で 要素の型 を書き、右辺に ブラケットのリテラル を書きます。

型推論に任せて

var xs = [a, b, c]

と書くこともできますが、学習中は List[Float64] のように明示した方が読みやすいです。

def main():
    var temps: List[Float64] = [20.5, 22.3]
    var count = len(temps)
    print("avg sample:", temps[0] / Float64(count))

リスト-6: types_cast_and_list.mojo

詳細: Types · Operators · List

出典: Mojo Manual — types / Mojo Manual — operators / API — collections.list.List

補足: SIMD と DType は、あとで出てくる GPU やメモリ配置の話にもつながります。

8.3. Operators

Mojo の演算子は、基本的には Python に近い感覚で読めます。
ただし、^ は意味が 2 つあるので注意が必要です。

  • +* など、よくある演算子が使える

  • SIMD に対する演算は、各要素ごとに行われる

  • 文字列では +in などが使える

  • 独自の型では dunder メソッドで演算子を定義できる

8.3.1. ^ の 2 つの意味

^ は、書き方によって意味が変わります。

  • a ^ b のように 2 項で使う
    ビット XOR

  • expr^ のように後ろに付ける
    値をムーブする

つまり、二項演算子か、接尾辞かで読み方が変わります。

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)

リスト-7: operators_xor_and_move.mojo

たとえば、

  • 5 ^ 3 はビット XOR

  • consume_list(data^)data を関数にムーブ

です。

所有権を受け取る側は、現行のマニュアルでは var 引数で書く形がよく使われます。

詳細: Operators, expressions, and dunder methods

出典: Mojo Manual — operators

補足: 比較演算や細かい演算子の仕様は、必要になったときにマニュアルで確認すれば十分です。

8.4. Control flow

Mojo の制御構文は、かなり Python に近いです。
そのため、Python の経験がある人は読みやすいはずです。

  • if / elif / else

  • while

  • for

  • break / continue

  • for / whileelse

8.4.1. if とループ

if は条件分岐、while は条件が真の間くり返す構文、for は順番に要素を取り出す構文です。

break はループを途中で抜ける、continue はその回の残りを飛ばして次へ進む、という意味です。

8.4.2. for ref

for ref v in values: のように書くと、v は各要素への参照になります。
このとき、ループの中で v を書き換えると、元のリストの値も変わります。

つまり for ref は、要素をその場で更新したいときに使います。

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: control_flow_for_ref_inplace.mojo

8.4.3. ループの else

forwhileelse は、break しなかったときだけ実行されるブロックです。
ここも Python と同じ感覚で読めます。

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

    for i in range(3):
        print(i, end=", ")
    else:
        print("\nloop completed without break")

リスト-9: control_flow_loop_else.mojo

1 つ目のループは、インデックスで要素を書き換える例です。
2 つ目のループは、forelse の流れを確認するための例です。

詳細: Control flow

出典: Mojo Manual — control-flow

補足: match などの新しい構文は、版によって変わることがあるのでマニュアルも確認してください。

8.5. Errors

8.5.1. エラー処理の考え方

Mojo のエラー処理は、Python と少し考え方が違います。
Mojo では、エラーを 「失敗を表す値」 として扱いやすい設計になっています。

そのため、処理の負担を増やしにくく、高速なコードを書きやすいのが特徴です。

8.5.2. 基本の構文

エラー処理には、次の構文を使います。

  • try

  • except

  • else

  • finally

それぞれの役割は次のとおりです。

  • except
    エラーが出たときの処理を書く

  • else
    エラーが出なかったときの処理を書く

  • finally
    最後に必ず実行したい処理を書く

いったん受け取ったエラーを外へ投げ直したいときは、raise e^ を使います。

8.5.3. 型付きエラー

Mojo では、エラーにも型を持たせることができます。

  • struct でエラー型を作る

  • 列挙型や Variant で複数の失敗をまとめる

という形で、どんな失敗かを型で表せるのが特徴です。

8.5.4. Neverwith

Neverraises Never は、この関数は失敗しないことを表します。

また、with を使うと、ファイルやロックなどを安全に扱えます。
処理が終わったあとに、きちんと後片づけしたいときに便利です。

8.5.5. コード例

以下では、try / except / else / finallyraise e^ を使った例を見ます。

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)

リスト-10: errors_try_except_reraise.mojo

process_record は、id を受け取って文字列を返す関数です。
ただし、不正な id が来たときは raise Error(...) でエラーにします。

そのため、関数定義では raises を付けています。

def process_record(id: Int) raises -> String:

これは、「この関数は失敗することがある」 と明示するためです。

8.5.6. コードの読み方

  • 内側の tryid > 999 のエラーはその場で処理し、id < 0raise e^ で外側に投げ直す。

  • 外側の try:内側で処理しきれなかったエラーを最後に受け取る。

  • finally は成功・失敗を問わず必ず実行される。

この例で try / except / else / finallyraise e^ の役割を一度に確認できます。

詳細: Errors, error handling, and context managers

出典: Mojo Manual — errors

補足: スタックトレースや詳しいデバッグ方法は、必要になったときにマニュアルを見るのがおすすめです。