はじめに
2026 年 8 月 18 日、Modular 社がプログラミング言語「Mojo🔥」のコンパイラをオープンソース化しました。
これまで Mojo は、標準ライブラリは 2024 年からオープンソースだったものの、肝心のコンパイラ本体は非公開のままでした。今回、Apache 2.0 ライセンス(LLVM 例外付き)のもとでコンパイラ・ツーリングまで含めて公開され、GitHub のmodularリポジトリで誰でもソースを読めるようになっています。
ちょうど今月、今後期待できそうな言語として Mojo が話題になっていたので、実際に触ってみることにしました。この記事は、Python 経験者が Mojo の環境構築から基礎文法、簡単な CLI アプリの実装までを一通り体験する記事です。
1. 環境を作る
Pixi のインストール
Mojo の公式インストール方法は uv と Pixi の両方が案内されています。今回は Pixi を使います。Pixi は Rust の Cargo や npm に近い、プロジェクト単位で依存関係と実行環境を管理するツールで、conda のエコシステム(conda-forge)をベースに、Python・Rust・Node.js など多言語のパッケージを扱えます。
$ curl -fsSL https://pixi.sh/install.sh | sh
プロジェクト作成と Mojo のインストール
$ pixi init life -c https://conda.modular.com/max/ -c conda-forge && cd life
$ pixi add mojo
$ pixi run mojo --version
Mojo 1.0.0 (ed45d567)
バージョン 1.0.0 が表示されました。Mojo 1.0 のリリースは 2026 年 8 月 11 日で、コンパイラの OSS 化(8 月 18 日)はちょうどその 1 週間後です。
VSCode 拡張機能
シンタックスハイライトや LSP、デバッガまで揃っている公式拡張機能があるので、入れておくと快適です。
2. なぜ速いのか: インタプリタとコンパイラの違い
Mojo のコードは見た目が Python そっくりです。
def main():
print("Hello, World!")
hello.mojoとして保存し、ビルドして実行してみます。
$ mojo build hello.mojo
$ ./hello
Hello, World!
ただし中身の実行のされ方は Python と全く違います。どちらもまずソースコードを解析しますが、大きく異なるのはその後の実行方法です。
- インタプリタ(CPython): ソースコードの意味を解析しながら、その場で動作を実行する。CPU の機械語ではなく Python VM 向けの中間言語(バイトコード)に変換したうえで、VM が 1 命令ずつ解釈しながら動かす
- コンパイラ(Mojo): ソースコードを、CPU が直接実行できる機械語に丸ごと変換してから実行する。VM のような仲介者を挟まない
ちなみに Mojo のコンパイラは、LLVM プロジェクトのコンパイラ基盤「MLIR」をベースに構築されています。ソースコードは AST を経由せず直接 MLIR の中間表現になり、複数段階の変換を経て LLVM IR、そして機械語になります。Mojo の開発者 Chris Lattner 氏は LLVM の生みの親であり、MLIR の立ち上げにも関わった人物です。
Chris Lattner, FOSDEM 2011(colorized by Waldyrious, CC BY-SA 4.0, via Wikimedia Commons)
3. 実測: 円周率の計算で速度を比較する
理屈だけでなく、実際にどれくらい速いのか測ってみます。ライプニッツの公式(円周率 = 4 × (1 - 1/3 + 1/5 - 1/7 + ...))を使って、同じロジックを Python と Mojo で書き、limitを 10 倍ずつ増やしながら実行時間を比較しました。比較に使ったバージョンは Python 3.11.9、Mojo 1.0.0 です(実行環境: Apple M4 Pro / macOS)。
import time
print("開始")
limit = 10
for _ in range(0, 9):
begin = time.perf_counter()
n = 1
l = 0.0
while n < limit:
l += (1.0 / n) - (1.0 / (n + 2))
n += 4
print(l * 4, time.perf_counter() - begin, "秒")
limit *= 10
print("終了")
from std.time import perf_counter
def main():
print("開始")
var limit: Int = 10
for _ in range(0, 9):
var begin = perf_counter()
var n: Int = 1
var l: Float64 = 0.0
while n < limit:
l += (1.0 / Float64(n)) - (1.0 / Float64(n + 2))
n += 4
print(l * 4, perf_counter() - begin, "秒")
limit *= 10
print("終了")
$ mojo build pi.mojo
$ ./pi
limitごとの実行時間を比較すると、以下のようになりました(円周率の近似値は両言語で完全に一致しています)。ブレを考慮して 3 回計測した中央値を載せています。
| limit | Python | Mojo | 倍率 |
|---|---|---|---|
| 10^7 | 0.2807 秒 | 0.0018 秒 | 約 157 倍 |
| 10^8 | 2.4497 秒 | 0.0180 秒 | 約 136 倍 |
| 10^9 | 24.7299 秒 | 0.1933 秒 | 約 128 倍 |
limit = 10^9では約 128 倍、Mojo の方が速い結果になりました。
これは言語一般の性能差を示すものではなく、あくまで今回の数値計算ループを手元の環境で測定した結果です。
4. 基礎文法をおさえる
CLI アプリを作る前に、Python と Mojo で挙動が違う部分を中心に、基礎文法を確認します。
変数: varとcomptime
Mojo の変数宣言はvarを使います。型注釈は:の後ろ、値は=の後ろという位置のルールがあります(TypeScript と同じ並びです)。
var num_rows = 8 # 型は自動推論
var num_cols: Int = 8 # 型を明示
Python と違い、関数の外(グローバルスコープ)に書き換え可能なvarを置くことはできません。
var count = 0 # ファイル直下に書くとエラー
global variables are not supported; move this into a function body or use 'comptime' to declare a constant
コンパイル時に値を決定できる定数であれば、comptimeでグローバルに置けます。
comptime GREETING: String = "Hello"
comptimeは「コンパイル時に値が確定していないといけない」という制約があります。
エラー処理: raisesとtry/except
例外を投げる可能性がある関数には、シグネチャにraisesを明示する必要があります。
def calculate_average(temps: List[Float64]) raises -> Float64:
if len(temps) == 0:
raise Error("気温データがありません")
var total = 0.0
for temp in temps:
total += temp
return total / Float64(len(temps))
呼び出す側はtry/exceptで受け止めるか、自分もraisesを付けて上位に伝播させる必要があります。
def main() raises:
try:
var avg = calculate_average(temps)
print("平均気温:", avg)
except e:
print("エラー:", e)
raisesが無い関数の中でraiseしようとすると、次のようなエラーになります。
'raise' requires a surrounding 'try' block or the enclosing function to declare 'raises'
構造体: struct
Mojo には(2026 年 8 月現在)Python のclassに相当するものがまだありません。将来的にサポート予定とのことですが、今は代わりにstructを使います。
@fieldwise_init
struct Person(Copyable):
var name: String
var age: Int
def main():
var mine = Person("Taro", 30)
print(mine.name, mine.age)
@fieldwise_initは、フィールドをそのまま受け取るだけの__init__を自動生成してくれるデコレータです。単純な代入以上のこと(デフォルト値を用意する、値を検証するなど)をしたい場合は、自分で__init__を書きます。
Listは暗黙にコピーされない
Listのような型は、デフォルトでは暗黙的にコピーできません。
var first_list: List[Int] = [1, 2, 3]
var second_list: List[Int] = first_list # エラー
value of type 'List[Int]' cannot be implicitly copied, it does not conform to 'ImplicitlyCopyable'
コピーしたい場合は.copy()を明示的に呼びます。
var second_list: List[Int] = first_list.copy()
5. 在庫管理 CLI を作ってみる
ここまでの内容を踏まえて、struct を実際に使ったサンプルコードを書いてみます。商品(Item)を struct で表現し、一覧・入庫・削除ができる在庫管理ツールを作りました。
def read_item_index(inventory: Inventory) raises -> Int:
for i in range(len(inventory.items)):
print(i + 1, ":", inventory.items[i].name)
var choice_str = input("→ ")
var choice: Int
try:
choice = Int(choice_str)
except e:
raise Error("数字を入力してください")
if choice < 1 or choice > len(inventory.items):
raise Error("存在しない番号です")
return choice - 1
@fieldwise_init
struct Item(Copyable):
var name: String
var stock: Int
struct Inventory(Copyable):
var items: List[Item]
# 引数なしで空の在庫を作りたいので、@fieldwise_initではなく手書き
def __init__(out self):
self.items = List[Item]()
def add_item(mut self, item: Item):
self.items.append(item.copy())
def list_items(self):
for item in self.items:
print(item.name, item.stock)
def add_stock(mut self, index: Int, qty: Int):
self.items[index].stock += qty
def remove_item(mut self, index: Int) raises -> String:
var name = self.items[index].name
_ = self.items.pop(index)
return name
def main() raises:
var inventory = Inventory()
inventory.add_item(Item("りんご", 50))
inventory.add_item(Item("ぶどう", 5))
while True:
print("--------------------")
print("1: 一覧 2: 入庫 3: 削除 4: 終了")
var choice_str = input("→ ")
if choice_str == "4":
print("終了します")
break
elif choice_str == "1":
inventory.list_items()
elif choice_str == "2":
try:
print("どの商品に入庫しますか?")
var index = read_item_index(inventory)
print("何個入庫しますか?")
var qty_str = input("→ ")
var qty: Int
try:
qty = Int(qty_str)
except e:
raise Error("数字を入力してください")
inventory.add_stock(index, qty)
print(inventory.items[index].name, "に", qty, "個入庫しました")
except e:
print("エラー:", e)
elif choice_str == "3":
try:
print("どのアイテムを削除しますか?")
var index = read_item_index(inventory)
var name = inventory.remove_item(index)
print(name, "を削除しました")
except e:
print("エラー:", e)
else:
print("1〜4の番号を入力してください")
設計のポイントがあります。ライブラリ由来のエラーを自分の言葉に翻訳する、という点です。Int()での変換に失敗すると、Mojo 標準ライブラリが生成する次のようなメッセージがそのまま出ます。
String is not convertible to integer with base 10: 'ぶどう'
これは開発者向けの技術的なメッセージなので、try/exceptで一度捕まえて「数字を入力してください」という利用者向けのメッセージに詰め替えています。
実行結果
$ mojo build item.mojo
$ ./item
--------------------
1: 一覧 2: 入庫 3: 削除 4: 終了
→ 1
りんご 50
ぶどう 5
--------------------
1: 一覧 2: 入庫 3: 削除 4: 終了
→ 2
どの商品に入庫しますか?
1 : りんご
2 : ぶどう
→ 1
何個入庫しますか?
→ 10
りんご に 10 個入庫しました
--------------------
1: 一覧 2: 入庫 3: 削除 4: 終了
→ 4
終了します
一覧・入庫・削除がひと通り動く、簡単な在庫管理ツールができました。
おわりに
コンパイラのオープンソース化というタイミングで、Python 経験者の目線で Mojo の基礎を一通り触ってみました。
classはまだ無く、Web 向けのエコシステムも発展途上です。現状は AI/ML のホットパスや、GPU カーネルの高速化が本命のユースケースなのかもしれません。
オープンソース化されたので、今後の動向を追いかけていきたいと思います。
参考