8. 言語基礎(2)— 型・演算子・制御・エラー
8.1. この章で学ぶこと
型の基本(
struct・リテラル・型変換)演算子の基本(
^の意味に注意)制御構文とエラー処理(
try/except/raises)
この章では、Mojo の基本的な書き方をまとめて確認します。 前の章と同じく、わからない言葉があればマニュアルも見ながら読み進めてください。
本章のコード例は Mojo 0.26 系 で確認しています。
uv 経由で Mojo を入れている場合は、手元の版をそろえるために uv run mojo build や uv 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 では、次のような型をよく使います。
整数や浮動小数
BoolStringTupleList/Dict/SetOptional
どれも、ふだんのプログラムでよく出てくる基本的な型です。
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 は、同じ種類の値をまとめて持つ型です。
ふつうの Int や Float64 は 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. リテラル
数値や文字列をそのまま書いたものを リテラル と言います。
たとえば次のようなものです。
423.14"hello"
Mojo では、こうしたリテラルは必要に応じて Int や Float64 などの実際の型として使われます。
def main():
var n: Int = 42
var x: Float64 = 1.25
print(n, x)
リスト-4: types_literals.mojo
8.2.5. 暗黙の型変換(数値の型昇格)
Mojo では、数値の型が違っていても自動で全部そろえてくれる、とはあまり考えない方が安全です。 整数と浮動小数を混ぜるときは、自分で型をそろえるのが基本です。
たとえば Int を Float64 と一緒に使いたいなら、Float64(n) のように明示的に変換します。
def main():
var n: Int = 7
var half = Float64(n) / 2.0
print(half)
リスト-5: types_explicit_cast.mojo
Float64(n) は、n を Float64 に変換する書き方です。
「どの型で計算したいかを自分で決める」 と理解するとよいでしょう。
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
出典: 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 項で使う
→ ビット XORexpr^のように後ろに付ける
→ 値をムーブする
つまり、二項演算子か、接尾辞かで読み方が変わります。
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はビット XORconsume_list(data^)はdataを関数にムーブ
です。
所有権を受け取る側は、現行のマニュアルでは var 引数で書く形がよく使われます。
詳細: Operators, expressions, and dunder methods
補足: 比較演算や細かい演算子の仕様は、必要になったときにマニュアルで確認すれば十分です。
8.4. Control flow
Mojo の制御構文は、かなり Python に近いです。
そのため、Python の経験がある人は読みやすいはずです。
if/elif/elsewhileforbreak/continuefor/whileのelse
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
for や while の else は、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 つ目のループは、for … else の流れを確認するための例です。
詳細: Control flow
出典: Mojo Manual — control-flow
補足:
matchなどの新しい構文は、版によって変わることがあるのでマニュアルも確認してください。
8.5. Errors
8.5.1. エラー処理の考え方
Mojo のエラー処理は、Python と少し考え方が違います。
Mojo では、エラーを 「失敗を表す値」 として扱いやすい設計になっています。
そのため、処理の負担を増やしにくく、高速なコードを書きやすいのが特徴です。
8.5.2. 基本の構文
エラー処理には、次の構文を使います。
tryexceptelsefinally
それぞれの役割は次のとおりです。
except
エラーが出たときの処理を書くelse
エラーが出なかったときの処理を書くfinally
最後に必ず実行したい処理を書く
いったん受け取ったエラーを外へ投げ直したいときは、raise e^ を使います。
8.5.3. 型付きエラー
Mojo では、エラーにも型を持たせることができます。
structでエラー型を作る列挙型や
Variantで複数の失敗をまとめる
という形で、どんな失敗かを型で表せるのが特徴です。
8.5.4. Never と with
Never や raises Never は、この関数は失敗しないことを表します。
また、with を使うと、ファイルやロックなどを安全に扱えます。
処理が終わったあとに、きちんと後片づけしたいときに便利です。
8.5.5. コード例
以下では、try / except / else / finally と raise 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. コードの読み方
内側の
try:id > 999のエラーはその場で処理し、id < 0はraise e^で外側に投げ直す。外側の
try:内側で処理しきれなかったエラーを最後に受け取る。finallyは成功・失敗を問わず必ず実行される。
この例で try / except / else / finally と raise e^ の役割を一度に確認できます。
詳細: Errors, error handling, and context managers
補足: スタックトレースや詳しいデバッグ方法は、必要になったときにマニュアルを見るのがおすすめです。