第6章 概要・関数・変数
6.1 この章で学ぶこと
この章で学ぶ内容は以下のとおりです。
学ぶこと |
ポイント |
|---|---|
関数の書き方と読み方 |
|
変数とスコープ |
|
アセンブリとの接続 |
|
所有権の入り口 |
|
6.2 概要:最初に押さえること
Mojo で最初に理解しておく基本的な考え方を整理します。
ポイント |
説明 |
|---|---|
エントリポイント |
|
ブロック |
|
変数 |
|
関数定義 |
|
型と構造体 |
型のまとまりは |
エラー |
|
6.2.1 最小サンプル
上記の基本概念を一度に確認できる最小構成のサンプルです。
型注釈付き変数宣言(var n: Int)、型推論(var doubled)、ジェネリックなリスト(List[Int])、文字列オブジェクト(String)という 4 種類の変数を宣言し、最後に print() で出力します。
def main():
var n: Int = 42
var doubled = n * 2
var xs: List[Int] = [1, 2, 3]
var label = String("count")
print(String("n="), n, String(" sum="), doubled, String(" "), label, String("="), len(xs))
リスト6-1: language_basics_minimal.mojo
このコードは次章の逆アセンブル(第7章 コンパイル結果のアセンブリを読む)で詳細に追います。 ここでは「どんな要素が含まれるか」を概観しておきます。
コードの要素 |
ポイント |
|---|---|
|
型注釈付き宣言。スタック上に 8 バイト( |
|
右辺から型推論。コンパイラが |
|
|
|
文字列オブジェクトをスタック上に構築。データは静的シンボル |
6.3 関数
6.3.1 def と fn:今は def が基本
Mojo では関数の定義に def を使います。
以前は fn も使われていましたが、Mojo 1.0.0 で完全に削除されました。
def と fn の違いと、削除に至った経緯を比較します。
|
|
|
|---|---|---|
型注釈 |
省略できる(推論が働く) |
引数・戻り値ともに必須 |
|
暗黙的に例外が伝播する |
コンパイルエラー(明示が必要) |
引数の既定 |
不変の読み取り専用参照(小さい値はレジスタ渡し) |
同じ規約。 |
状態 |
Mojo 1.0 で唯一の関数定義キーワード |
Mojo 1.0.0 で完全に削除された |
コラム: なぜ fn は削除されたか
初期の Mojo(〜0.9 系)では、fn が「システムプログラミング向けの厳格な関数」、def が「Python 互換の緩やかな関数」として共存していました。
しかし def が所有権、型推論、raises を正しく扱えるようになり、fn との差が実質的になくなりました。
2 つの関数定義キーワードを維持するコストが設計的に不合理なため、fn はまず非推奨になり、Mojo 1.0.0 で完全に削除されました。
Mojo 1.0.0 では fn を書くとコンパイルエラーになります。def のみが使えます。
6.3.2 () と []:実行時 vs コンパイル時
() と [] の使い分けは、Mojo の関数を読むうえで中心となる概念です。
Python では、関数に渡せる値はすべて「実行時の引数」です。 関数を呼び出す瞬間まで、どんな値が渡されるかコンパイラにはわかりません。
def repeat(msg, count): # msg も count も実行時に決まる
for _ in range(count):
print(msg)
Mojo はこれを2種類に分けます。実行時に確定する値は () に、コンパイル時に確定する値は [] に渡します。
def repeat[count: Int](msg: String): # count はコンパイル時、msg は実行時
for _ in range(count):
print(msg)
[] に渡した値はビルドの時点で確定するため、コンパイラはその値に特化した機械語を生成できます。
repeat[2](...) なら「2回繰り返す」専用のコードが作られ、実行時にループ回数を判定する必要がありません。
これを整理すると以下のとおりになります。
記法 |
確定タイミング |
意味 |
|---|---|---|
|
実行時(arguments) |
呼び出し時に値が渡される。毎回異なる値が来てよい |
|
コンパイル時(parameters) |
コンパイル時に値が確定。その値専用のコードが生成される |
上記の repeat[count: Int] を含む、[] と () の使い分けを示すサンプルコードです。
オーバーロード(show)、デフォルト引数(greet)、raises(pick_nonzero)も合わせて確認できます。
def show(a: Int):
print("int", a)
def show(s: String):
print("str", s)
def repeat[count: Int](msg: String):
for _ in range(count):
print(msg)
def greet(name: String = "world"):
print(String("Hello, "), name)
def pick_nonzero(x: Int) raises -> Int:
if x == 0:
raise Error("zero not allowed")
return x
def main():
show(1)
show(String("hi"))
repeat[2](String("Hello"))
greet()
greet(String("Mojo"))
try:
_ = pick_nonzero(0)
except e:
print("caught:", e)
リスト6-2: functions_overload_params.mojo
repeat[2](String("Hello")) の [2] は実行時引数ではありません。
コンパイラは count = 2 と確定した状態でループを展開し、最適化できます。
6.3.3 オーバーロードとアセンブリ
show(a: Int) と show(s: String) は同名でも 別の関数です。
コンパイラは引数の型から呼び出し先を選択し、異なるシンボルを生成します。
実際に、生成されたアセンブリのコードを見ると、それぞれ別の関数(ラベル)が呼び出されているのが分かります。
; show(1) の呼び出し
callq "_show(::Int)" ; Int 版のシンボル
; show(String("hi")) の呼び出し
callq "_show(::String)" ; String 版のシンボル
実行時に「どちらを呼ぶか」を判定するコードはありません。
コンパイル時に解決済みです。
これが Python の isinstance チェックとの違いです。
コラム: オーバーロード解決の規則
Mojo がオーバーロードを解決するときの優先順位は次のとおりです。
引数の型が完全に一致するもの
型変換(conversion)1 回で一致するもの
暗黙変換が必要なもの
あいまいな場合(どちらも同じ優先度で一致する)はコンパイルエラーになります。 Python のような「名前で上書き」ではなく、型を見た静的解決です。
6.3.4 例外
Mojo の例外は raise Error("メッセージ") で発生させ、try/except で受け取ります。
Python と構文は似ていますが、違いが 1 つあります。
エラーを返す可能性がある関数は、シグネチャに raises を明記しなければなりません。
raises がない関数から raise するとコンパイルエラーになります。
Python との違いをまとめます。
Python |
Mojo |
|
|---|---|---|
エラーを投げる宣言 |
不要(どこでも |
|
呼び出し元の義務 |
なし( |
|
コンパイル時の確認 |
なし |
|
def pick_nonzero(x: Int) raises -> Int: # raises が必要
if x == 0:
raise Error("zero not allowed")
return x
def main() raises: # main も raises が必要
try:
_ = pick_nonzero(0)
except e:
print("caught:", e)
リスト6-3: raises_try_except.mojo
6.4 変数
6.4.1 スタック上の変数レイアウト
var n: Int = 42 は、スタック上の特定アドレスに 8 バイトを確保します。第7章 コンパイル結果のアセンブリを読むで示すアセンブリ出力 language_basics_minimal.asm を見ると、次のように現れます。
; var n: Int = 42
7: movq $0x2a, 0x1e8(%rsp) ; 0x2a = 42、スタック offset 0x1e8 に格納
; var doubled = n * 2 (コンパイル時に 84 に確定済み)
13: movq $0x54, 0x1f0(%rsp) ; 0x54 = 84、スタック offset 0x1f0 に格納
スタック上の配置をイメージすると次のようになります。
図6-1: スタック上の変数配置
変数のオフセットはコンパイル時に確定しています。 実行時に「どこに置くか」を計算しません。
6.4.2 var のブロックスコープ
Python では if ブロック内で変数を作っても、その変数は関数全体で有効です(関数スコープ)。
Mojo の var はこれと異なり、宣言したブロックの内側でのみ有効です。
if を抜けると変数は消え、それ以降参照するとコンパイルエラーになります。
次のコードで、var x = 1 が if ブロック内にのみ存在することを確認できます。
def block_var():
if True:
var x = 1
print("inside", x)
# ここでは x は使えない(ブロックスコープ)
def main():
block_var()
リスト6-4: variables_block_var.mojo
Python |
Mojo |
|
|---|---|---|
スコープの単位 |
関数( |
ブロック( |
例 |
|
|
Python エンジニアの注意点 |
ブロックで変数を作っても外で使える |
|
6.4.3 暗黙宣言(関数スコープ)
var なしの代入は 関数スコープになります。
Python に近い感覚で使えます。
def implicit_scope():
y = 10
if True:
y = 20
print("y after if:", y)
def main():
implicit_scope()
リスト6-5: variables_implicit_scope.mojo
6.4.4 所有権の移動:^(transfer)
Mojo では、変数は値の「所有者」です。
^(transfer 演算子)を付けて関数に渡すと、所有権ごと移動します。
移動後の変数は使えなくなり、コンパイラがこれを静的に検出します。
^ がない場合は自動的にコピーが発生します。
所有権の仕組み全般(borrowed、inout、owned 引数の違いや、__copyinit__/__moveinit__ の役割)は 第10章 値・所有権・ライフサイクル で詳しく扱います。
ここでは ^ を使うと「コピーせず渡せる」という入口だけ押さえておきます。
def take(var s: String):
print(s)
def main():
var msg = String("hi")
take(msg^)
リスト6-6: variables_ownership_transfer.mojo
このコードで所有権がどう移動するかを追ってみます。
var msg = String("hi"):msgがStringオブジェクトの所有者になります。この時点でメモリ上に文字列が確保され、msgだけがその値を「持って」います。take(msg^):^を付けて渡すと、msgの所有権ごとtakeに移動します。この行を過ぎるとmsgは無効になり、以降msgを参照するとコンパイルエラーになります。コピーは起きません。def take(var s: String)::受け取ったsが新しい所有者です。takeの中ではsを自由に読み書きできます。takeが返るとsはスコープを外れ、Stringのメモリが解放されます。
^ を付けない take(msg) と書いた場合は、msg のコピーが作られて take に渡されます。
msg 自体はその後も有効です。
^ は「このまま渡して、元の変数は捨てる」という明示的な意思表示です。
コード |
意味 |
|---|---|
|
|
|
|
|
|
^ がないとコピーが発生します。
^ を付けると「コピーせずに移動」になります。
コピーコストが大きい型(大きな List や String)では ^ を意識すると無駄なメモリ確保を避けられます。
6.5 まとめ
本章で扱った内容を以下にまとめます。
要素 |
押さえるべきこと |
|---|---|
|
|
|
|
オーバーロード |
同名関数を型で区別。コンパイル時に解決済みで実行時コストなし |
|
エラーを返す可能性をシグネチャで明示。呼び出し元も対応が必要 |
|
|
|
所有権を移動する。コピーなしに値を渡し切る |
スタック配置 |
|