(メタプログラミング)= # メタプログラミング ## この章で学ぶこと - コンパイル時評価の基本 - `[]` と `()` の違い - traits / generics / constraints の考え方 - materialization と reflection の入口 Mojo のメタプログラミングの核心は、**コンパイル時に決められることを先に決め、実行時コストを消す**ことです。 - `comptime if` で選ばれなかった枝はバイナリに残らない - `comptime for` で展開されたループは実行時ループではない - `[]` で渡した型・値は、特化されたコードを生成する材料になる 「**何がコンパイル時に消えるのか**」を意識すると、この章の各節がつながりやすくなります。 ## Metaprogramming 次の例は **`comptime if`** です。`branch` は **コンパイル時にだけ**意味を持ち、**選ばれた枝のコードだけ**が生成されます。 ```{literalinclude} ../../../src/part2/ch09/meta_comptime_if.mojo :language: mojo ```

リスト-1: meta_comptime_if.mojo

`comptime branch = 1` は実行時変数ではなく、**コンパイル時の分岐材料**です。実行バイナリには **`print(10)` 側だけ**が残る、と考えるとイメージしやすいです。 詳細: [Metaprogramming](https://docs.modular.com/mojo/manual/metaprogramming/) 出典: [Mojo Manual — metaprogramming](https://docs.modular.com/mojo/manual/metaprogramming/) > **補足:** GPU やデータ配置の話では、「形をコンパイル時に決める」ことが特に重要になります。 ## compile-time evaluation ここでは、**コードの一部をコンパイル時に実行する** という考え方を見ます。 ### 何をするのか Mojo では、`comptime` を使って、 **実行時ではなくコンパイル時にだけ動く処理** を書けます。 よく出てくるものは次の通りです。 - `comptime` ブロック - `comptime if` - `comptime for` ### どう考えればよいか これは、プログラムを実行する前の段階で、 - 定数を決める - 型を組み立てる - 分岐を先に確定する ための仕組みです。 つまり、**あとで決める必要がないものは、先に決めてしまう** ということです。 ここで大事なのは、**何がコンパイル時に評価され、何が実行時に評価されるか** を区別することです。 **`comptime for`** は、ループを **コンパイル時に展開**するイメージです。次の例では `range(3)` の各 `i` について **`print` がコンパイル時に処理**され、実行時には結果の機械語だけが残ります。 ```{literalinclude} ../../../src/part2/ch09/comptime_for_unroll.mojo :language: mojo ```

リスト-2: comptime_for_unroll.mojo

実行時の `for` とは別物なので、**ループ上限に comptime な値**を使う、などの制約がある場合があります(詳細はマニュアル)。 詳細: [compile-time evaluation](https://docs.modular.com/mojo/manual/metaprogramming/comptime-evaluation/) 出典: [Mojo Manual — comptime-evaluation](https://docs.modular.com/mojo/manual/metaprogramming/comptime-evaluation/) > **補足:** この節は、デバッグやエラーメッセージの理解にもつながります。 ## Parameters この節で大事なのは、**`[]` と `()` は役割が違う** ということです。 ### 基本 - `[]` は compile-time parameter - `()` は runtime argument つまり、 - `[]` はコンパイル時に決まる - `()` は実行時に渡す という違いがあります。 ### どう使い分けるか `[]` には、型・値・別名などを渡せます。 これは、コードの形そのものをコンパイル時に決めたいときに使います。 一方で `()` は、実行中に与える普通の引数です。 この違いを意識すると、Mojo のコードはかなり読みやすくなります。 ### なぜ大事なのか `[]` を使うと、コンパイラはその値に合わせて特化したコードを作れます。 そのため、抽象的に書いても実行時には速くしやすくなります。 `lanes` は **`width` を `[]` で渡す**ことで、**呼び出しごとに別の特化**が可能になります。`scale` は **`()` の実行時引数**です。 ```{literalinclude} ../../../src/part2/ch09/params_width_runtime_value.mojo :language: mojo ```

リスト-3: params_width_runtime_value.mojo

`lanes[8](2)` は **「幅 8 に特化した関数」に、実行時の 2 を渡す**、と読みます。 実際にコンパイルされたコードは以下のようになり、**幅 8 に特化した関数** が生成されていることがわかります。 ```{literalinclude} ../../../src/part2/ch09/params_width_runtime_value.asm ```

リスト-4: params_width_runtime_value.asm

詳細: [Parameters](https://docs.modular.com/mojo/manual/parameters/) 出典: [Mojo Manual — parameters](https://docs.modular.com/mojo/manual/parameters/) > **補足:** generics と似て見えますが、parameters は「コンパイル時に決まる情報」を直接扱う感覚です。 ## Traits trait は、**型がどんな振る舞いを持つかを表す約束** です。 ### ざっくり言うと trait は、 **この型はこういう操作ができます** と示すための仕組みです。 ポイントは次の通りです。 - trait はメソッドの契約を表す - 実際の実装は `struct` 側に書く - 共通の振る舞いをそろえやすくなる ### たとえば 標準では、次のような trait が出てきます。 - `Copyable` - `Movable` - `Stringable` これらは、 - コピーできるか - ムーブできるか - 文字列として扱いやすいか といった性質をそろえるために使われます。 Python の interface や protocol を思い出すと、入りやすいです。 **`Copyable`** を付けると、**コピー用の API**(`.copy()` など)が使える、という約束になります。 ```{literalinclude} ../../../src/part2/ch09/traits_copyable_label.mojo :language: mojo ```

リスト-5: traits_copyable_label.mojo

`Label` は **フィールドの並びと trait** で「どう複製できるか」が決まります。別の trait も、**満たすメソッドを struct 側に実装**する、というパターンです。 詳細: [Traits](https://docs.modular.com/mojo/manual/traits/) 出典: [Mojo Manual — traits](https://docs.modular.com/mojo/manual/traits/) > **補足:** 最初は「trait は振る舞いの条件」と考えるだけで十分です。 ## Generics generics は、**型をあとから差し替えられる形でコードを書く仕組み** です。 ### 基本 たとえば `T` のような型パラメータを使って、 複数の型に対応できる関数や型を書けます。 さらに、`T: Writable` のように、 **この trait を満たす型だけ使える** という条件も付けられます。 ### 何がうれしいのか - 同じ形のコードを何度も書かなくてよい - 型ごとに安全な制約を付けられる - 使った組み合わせごとにコードが生成される ここで大事なのは、**抽象的に書いても、実行時には具体的な型に合わせたコードになる** ことです。 また、数値そのものをパラメータにして、長さや幅を固定する書き方もあります。 これが、SIMD や配列サイズの話とつながります。 `byte_len` は **`T: Writable`** な型だけを受け取ります。`String` は **`Writable`** を満たすので、この呼び出しが成立します。 ```{literalinclude} ../../../src/part2/ch09/generics_writable_len.mojo :language: mojo ```

リスト-6: generics_writable_len.mojo

`String(x)` で **文字列化**してから `len` しています。別の `Writable` 型を渡すと、**別の特化**が生成される、という読み方ができます。 これも実際にコンパイルされたコードは以下のようになり、**特化した関数** が生成されていることがわかります。 ```{literalinclude} ../../../src/part2/ch09/generics_writable_len.asm ```

リスト-7: generics_writable_len.asm

詳細: [Generics](https://docs.modular.com/mojo/manual/generics/) 出典: [Mojo Manual — generics](https://docs.modular.com/mojo/manual/generics/) > **補足:** generics は parameters とセットで読むと理解しやすいです。 ## Constraints constraints は、**使ってよい型や条件をさらに細かくしぼる仕組み** です。 ### 何をするのか - `where` 句で追加条件を書く - comptime の条件で分岐する - `comptime_assert` でコンパイル時に失敗させる ### なぜ必要か 抽象的なコードは便利ですが、何でも受け入れると誤用が起きやすくなります。 そこで constraints を使って、 **このコードはこういう条件のときだけ使える** と明示します。 すると、間違いを実行前に見つけやすくなります。 `Buf[size: Int where size > 0]` は **`size` が正のときだけ**型が成立します。`Buf[0]` のような誤用は **コンパイルエラー**にできます。 ```{literalinclude} ../../../src/part2/ch09/constraints_buf_positive.mojo :language: mojo ```

リスト-8: constraints_buf_positive.mojo

`Self.size` は **その struct のパラメータ `size`** を指します。`where` の条件は、**ジェネリクスや最適化**の前提をコンパイラに伝える役割もあります。 詳細: [Constraints](https://docs.modular.com/mojo/manual/metaprogramming/constraints/) 出典: [Mojo Manual — constraints](https://docs.modular.com/mojo/manual/metaprogramming/constraints/) > **補足:** ここは少し高度です。必要になった時点で深掘りすれば十分です。 ## Materialization この節は少しわかりにくいですが、 **コンパイル時の情報を、実行時に使える値へ落とし込む仕組み** だと捉えるとよいでしょう。 ### イメージ たとえば、リテラルや comptime の値を、 実際に使う値として形にする場面があります。 そのときに materialization の考え方が出てきます。 ### まず押さえること - コンパイル時の情報を実行時の値へつなぐ - `materialize()` などの仕組みがある - 数値型や SIMD の幅の決定とも関わる ここは最初から完璧に理解しなくても大丈夫です。 まずは、**コンパイル時の情報をそのまま終わらせず、実際の値として使う場面がある** とわかれば十分です。 リテラルや `Int` の値を、**別の数値型の演算**に載せるときは、**明示的な変換**(`Float64(n)` など)で **materialize** する、とマニュアルでも説明されます。 ```{literalinclude} ../../../src/part2/ch09/materialize_float_from_int.mojo :language: mojo ```

リスト-9: materialize_float_from_int.mojo

`n` は実行時の整数でも、**`Float64(n)`** で **浮動小数の式**に載せ替えられます。より高度な **`materialize()`** は、型や SIMD の話と合わせてマニュアルを参照してください。 詳細: [Materialization](https://docs.modular.com/mojo/manual/metaprogramming/materialization/) 出典: [Mojo Manual — materialization](https://docs.modular.com/mojo/manual/metaprogramming/materialization/) > **補足:** 型やリテラルの話と一緒に読むと、つながりが見えやすくなります。 ## Reflection reflection は、**型の情報を調べる仕組み** です。 ### Mojo の reflection の特徴 Mojo では、reflection は主に **コンパイル時限定** です。 たとえば次のような情報を調べられます。 - 型名 - フィールド情報 - 型に関する構造 ### Python との違い Python では、実行時にかなり自由に型情報を調べられます。 一方で Mojo では、**実行時リフレクションは基本的に強く使わない** 設計です。 これは、性能や予測しやすさを保つためです。 つまり、Mojo の reflection は、 **何でも実行中に調べるためのものではなく、コンパイル時の補助として使うもの** と理解するとよいでしょう。 `struct_field_count` は **コンパイル時に**フィールド数を求めます。`comptime n = ...` の結果を **実行時の戻り値**として返す、という橋渡しもできます。 ```{literalinclude} ../../../src/part2/ch09/reflection_field_count.mojo :language: mojo ```

リスト-10: reflection_field_count.mojo

`Point` のフィールドは `x` と `y` の **2 つ**なので、`2` が表示されます。**フィールド名の列挙**なども `std.reflection` にあります(マニュアル参照)。 詳細: [Reflection](https://docs.modular.com/mojo/manual/reflection/) 出典: [Mojo Manual — reflection](https://docs.modular.com/mojo/manual/reflection/) > **補足:** Python の reflection と同じ感覚で読むとずれやすいので注意です。 ## この章を一文で言うと **Mojo のメタプログラミングは、コンパイル時に型や条件を先に決めて、安全で速いコードを作るための仕組みです。** ## まとめ - Mojo ではコンパイル時に多くのことを決められる - `[]` はコンパイル時、`()` は実行時の情報を渡す - trait は振る舞いの約束を表す - generics は型を抽象化しつつ、安全に再利用する仕組み - constraints は誤用を早く防ぐための条件づけ - materialization はコンパイル時の情報を実際の値につなぐ - reflection は主にコンパイル時に型情報を調べるために使う この章は少し抽象的ですが、 **「先に決められることは先に決める」** という一本の考え方で読むと、かなり整理しやすくなります。