関数

Basis
最終更新:

ある程度の規模のプログラムには、複数の場所から実行する必要がある処理が必ず出てくる。それを名前付きで再利用する手段がなければ、コードをコピーするしかなくなる — そしてコピーは将来的にバグの温床になる。関数(function)はこの問題を解決する。処理のまとまりに名前を付けることで、何度でも、どこからでも、コードを繰り返さずに呼び出せるようになる。

関数とは

関数とは、名前を持つ再利用可能なコードのまとまりだ。入力値を受け取り、一連の処理を実行し、出力値を返す(いずれも省略可能)。関数は一度定義しておき、その処理が必要になるたびに呼び出す(call)。

料理のレシピカードに例えるとわかりやすい。カードのタイトルが関数名、材料リストが引数リスト、手順が本体、最終的に完成する料理が戻り値だ。

関数の宣言

Zig では、関数宣言は次のような形になる:

fn name(param1: Type1, param2: Type2) ReturnType {
    // 本体
}
  • fn はすべての関数宣言を始めるキーワード。
  • name は呼び出し時に使う識別子。Zig の慣習では関数名に camelCase を使う。
  • 括弧内の引数リスト(parameter list)が入力を宣言する。各引数はコロンで区切られた名前と型を持つ。
  • ReturnType は関数が返す値の型。
  • 本体は {} の間に書く処理のブロック。

最小限の具体例を示す:

fn add(a: i32, b: i32) i32 {
    return a + b;
}

add は32ビット符号付き整数を2つ受け取り、その和を返す。return キーワードは値を呼び出し元に返し、その時点で関数を終了させる。

Zig では値を返すために明示的な return が必要だ。 一部の言語とは異なり、ブロック内の最後の式の値が自動的に返されることはない。return を書かなかった場合、コンパイラは「関数の制御フローが値を返さずに末尾に到達する」というエラーを出す。

pub キーワード

デフォルトでは、関数はプライベート(private)— 宣言されたファイルの中からしか見えない。fn の前に pub を付けるとパブリック(public)になり、このファイルをインポートした別のファイルから呼び出せるようになる:

pub fn add(a: i32, b: i32) i32 {
    return a + b;
}

経験則として、別のファイルから呼び出す具体的な理由がない限り、関数はプライベート(pub なし)で宣言しよう。公開するインターフェイスを小さく保つことで、モジュールは理解しやすくなり、後のリファクタリングも楽になる。

void 戻り値型

関数が値を返す必要がない場合 — たとえばターミナルに出力するだけの関数 — 戻り値の型は void にする。return 文は完全に省略してよいし、早期終了のために return; だけ書くこともできる:

const std = @import("std");

fn greet(name: []const u8) void {
    std.debug.print("Hello, {s}!\n", .{name});
}

pub fn main() void {
    greet("world"); // Hello, world!
}

void は値ではない — 「この関数は有用な値を何も返さない」という意味だ。void 関数の結果を変数に代入しようとするとコンパイルエラーになる。

関数の呼び出し

関数を呼び出すには、関数名の後に引数を括弧で囲んで書く。引数の順序と型は引数リストと一致させる必要がある:

const std = @import("std");

fn square(n: i32) i32 {
    return n * n;
}

pub fn main() void {
    const result = square(7);
    std.debug.print("{}\n", .{result}); // 49
}

square(7) の呼び出しは値 7n に渡し、本体を実行し、return49 を返す。その値が result に束縛される。

関数呼び出しは式(expression)だ。返された値を別の呼び出しにそのまま渡したり、算術に使ったり、変数に格納したりできる。

const std = @import("std");

fn double(n: i32) i32 {
    return n * 2;
}

pub fn main() void {
    std.debug.print("{}\n", .{double(double(3))}); // 12
}

main 関数:エントリーポイント

すべての実行可能な Zig プログラムは main 関数を定義しなければならない。プログラムを実行したときに OS が最初に呼び出す関数だ。他のすべての関数は、main(または main が呼び出すもの)がいずれ呼び出すことで初めて到達できる。

const std = @import("std");

pub fn main() void {
    std.debug.print("program started\n", .{});
}

main は Zig のビルドシステムが見つけられるよう pub にしなければならない。最もシンプルな形では引数を取らず、戻り値の型は通常 void(エラーを返す可能性がある場合は !void — エラーハンドリングのチェックポイントで扱うパターン)だ。

引数はデフォルトでイミュータブル

関数本体の中で、各引数は const バインディングのように振る舞う。読み取ったり、渡したり、計算に使ったりはできるが、再代入はできない:

fn tryToMutate(x: i32) void {
    x = x + 1; // コンパイルエラー: 定数には代入できない
}

ミュータブルなローカルコピーが必要な場合は、本体の中で var を宣言して引数で初期化する:

fn countDown(start: i32) void {
    const std = @import("std");
    var n: i32 = start;
    while (n > 0) : (n -= 1) {
        std.debug.print("{}\n", .{n});
    }
}

動くコード全体の例

ここまでの内容をまとめた小さなプログラムを示す:

const std = @import("std");

fn clamp(value: i32, min: i32, max: i32) i32 {
    if (value < min) return min;
    if (value > max) return max;
    return value;
}

fn printClamped(value: i32) void {
    const result = clamp(value, 0, 100);
    std.debug.print("clamp({}) = {}\n", .{ value, result });
}

pub fn main() void {
    printClamped(-5);  // clamp(-5) = 0
    printClamped(50);  // clamp(50) = 50
    printClamped(120); // clamp(120) = 100
}

clamp は境界ロジックをカプセル化する。printClampedclamp と出力を一つの名前付き操作にまとめる。main は意図の短いリストとして読める。各関数は一つのことをする。

複数の戻り値

Zig の関数が返せる値はちょうど1つだ。複数のデータを返す必要があるときの慣用的なアプローチは、それらの値をまとめた構造体(struct)を返すことだ。もう一つよくあるパターンはエラーユニオン(error union、ErrorSet!Type と書く)で、値を返すか何かがうまくいかなかったことを知らせるか、どちらかになる。両パターンともあとのチェックポイントで登場する。今は、戻り値の型がプリミティブ値1つに限られないということだけ覚えておこう。

comptime 引数

Zig の関数は comptime キーワードで引数を宣言することでジェネリックにできる。comptime はコンパイラにその引数の値を実行時ではなくコンパイル時に解決するよう伝える。これにより、型レベルのプログラミングやゼロオーバーヘッドの抽象化が可能になる。詳細な仕組みは comptime のチェックポイントで扱う。今は、Zig のジェネリクスが独立したテンプレート構文やジェネリック構文ではなくこのメカニズムで実現されていると知っておけばよい。

関数を小さく単機能に保つ

関数は一つのことを明確に行うべきだ。単一責任(single responsibility)と呼ばれることもあるこの原則は厳格なルールではなく指針だ。長い関数の途中に // --- 第2部 --- のようなコメントを書いていたら、それは二つの名前付き関数に分割するサインだ。

小さく単機能な関数は:

  • 読みやすい — 良い名前が関数の働きを要約してくれるので、呼び出し側は本体を読まなくてもコードを理解できる。
  • テストしやすい — 小さな関数は単独で検証できる。
  • 変更しやすい — ロジックの一部を修正・改善しても、同じ本体にある無関係な部分を壊すリスクがない。

マジックナンバーの行数制限はない。テストは「この関数がすることを正直に説明する名前を付けられるか」だ。

まとめ

  • 関数は再利用可能なロジックのブロックに名前を付ける。fn name(params) ReturnType { } で宣言する。
  • pub は関数を他のファイルから見えるようにする。省略すると関数は自分のファイル内でのみプライベートになる。
  • 引数name: Type のペアで宣言され、本体の中ではイミュータブル。ミュータブルなコピーが必要なら var をローカルで宣言する。
  • return value; で結果を返す。Zig は最後の式を暗黙的に返さない — return は常に必要だ。
  • 関数が有用な値を返さない場合、戻り値の型は voidreturn は省略できる。
  • main はプログラムのエントリーポイント。pub にする必要があり、基本形では引数を取らない。
  • 複数の値を返すには構造体を使う。値またはエラーを返すにはエラーユニオンを使う — どちらも後のチェックポイントで扱う。
  • 関数は comptime 引数を受け取ることでジェネリックにできる。詳細は comptime のチェックポイントにある。
  • 関数を小さく単機能に保とう。関数ごとに明確な責任が一つあると、コードは読みやすく、テストしやすく、変更しやすくなる。