ざっくりZig - 関数

2024/04/15に公開

ざっくりZig - 関数

関数(かんすう:Function)は同じ内容の処理を何度も実行できるようにまとめて定義したものです。Zigにはprintをはじめとして多くの関数があらかじめ定義されていて、すぐに実行できるようになっています。しかし、関数はこうした所与のものだけでなく、プログラムに必要な処理として新たに定義できるようになっています。

処理の流れは、引数(ひきすう:Argument)というデータを別の関数からもらい、定義された処理を行った後、その結果(戻り値あるいは返り値)を呼び出した関数に返します。引数は0~複数個を設定できますが、戻り値は1つのみです。ただし複数の値を同時に持つタプルや構造型(どちらも別途紹介予定)あるいは配列でも返せます。書き方は以下の通りです。

fn 関数名(引数: 型名, ...) 戻り値の型名 {
    // ... 定義された処理 ...
    return 結果;
}

例として1からnまでの整数の合計(三角数)を計算する関数を示します。triangular1triangular2の2つを作成しましたが、どちらも引数の値が同じなら結果は同じです。このような場合は後者のように短く作成するのがよいでしょう。

const std = @import("std");

// 1..nまでの整数の合計を計算する関数(1)
fn triangular1(n: usize) usize {
    var s: usize = 0;
    for (1..n + 1) |i| {
        s += i;
    }
    return s;
}

// 1..nまでの整数の合計を計算する関数(2) - 結果は(1)と同じ
fn triangular2(n: usize) usize {
    return n * (n + 1) / 2;
}

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();

    // 関数triangular1に異なる引数を渡す
    try stdout.print("triangular1\n", .{});
    try stdout.print("1から10までの総和 = {}\n", .{triangular1(10)});
    try stdout.print("1から20までの総和 = {}\n", .{triangular1(20)});
    try stdout.print("1から30までの総和 = {}\n", .{triangular1(30)});

    try stdout.print("\n", .{});

    // 関数triangular2に異なる引数を渡す
    try stdout.print("triangular2\n", .{});
    try stdout.print("1から10までの総和 = {}\n", .{triangular2(10)});
    try stdout.print("1から20までの総和 = {}\n", .{triangular2(20)});
    try stdout.print("1から30までの総和 = {}\n", .{triangular2(30)});
}
結果
triangular1
1から10までの総和 = 55
1から20までの総和 = 210
1から30までの総和 = 465

triangular2
1から10までの総和 = 55
1から20までの総和 = 210
1から30までの総和 = 465

型を変数に代入

関数に定義する引数や戻り値の型名はu16など直接明示することもできますが、型を代入された変数で示すこともできます。この変数は最初に代入する際にも使用できるため、同じ変数を使うと型名を統一できます。また、変数名の前に?をつけてオプション付きにすることもできます。

const std = @import("std");

// 引数と戻り値の型名を代入
const T = u8;

fn multiply1(a: T, b: T) ?T {
    return a * b;
}

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();

    var i: T = 1;       // 変数に代入された型名を使用
    while (i < 10) {
        var j: T = 1;   // 変数に代入された型名を使用
        while (j < 10) {
            if (j > 1) try stdout.print(" | ", .{});
            try stdout.print("{} * {} = {d:>2}", .{ i, j, multiply1(i, j).? });
            j += 1;
        }
        try stdout.print("\n", .{});
        i += 1;
    }
}
結果
1 * 1 =  1 | 1 * 2 =  2 | 1 * 3 =  3 | 1 * 4 =  4 | 1 * 5 =  5 | 1 * 6 =  6 | 1 * 7 =  7 | 1 * 8 =  8 | 1 * 9 =  9
2 * 1 =  2 | 2 * 2 =  4 | 2 * 3 =  6 | 2 * 4 =  8 | 2 * 5 = 10 | 2 * 6 = 12 | 2 * 7 = 14 | 2 * 8 = 16 | 2 * 9 = 18
3 * 1 =  3 | 3 * 2 =  6 | 3 * 3 =  9 | 3 * 4 = 12 | 3 * 5 = 15 | 3 * 6 = 18 | 3 * 7 = 21 | 3 * 8 = 24 | 3 * 9 = 27
4 * 1 =  4 | 4 * 2 =  8 | 4 * 3 = 12 | 4 * 4 = 16 | 4 * 5 = 20 | 4 * 6 = 24 | 4 * 7 = 28 | 4 * 8 = 32 | 4 * 9 = 36
5 * 1 =  5 | 5 * 2 = 10 | 5 * 3 = 15 | 5 * 4 = 20 | 5 * 5 = 25 | 5 * 6 = 30 | 5 * 7 = 35 | 5 * 8 = 40 | 5 * 9 = 45
6 * 1 =  6 | 6 * 2 = 12 | 6 * 3 = 18 | 6 * 4 = 24 | 6 * 5 = 30 | 6 * 6 = 36 | 6 * 7 = 42 | 6 * 8 = 48 | 6 * 9 = 54
7 * 1 =  7 | 7 * 2 = 14 | 7 * 3 = 21 | 7 * 4 = 28 | 7 * 5 = 35 | 7 * 6 = 42 | 7 * 7 = 49 | 7 * 8 = 56 | 7 * 9 = 63
8 * 1 =  8 | 8 * 2 = 16 | 8 * 3 = 24 | 8 * 4 = 32 | 8 * 5 = 40 | 8 * 6 = 48 | 8 * 7 = 56 | 8 * 8 = 64 | 8 * 9 = 72
9 * 1 =  9 | 9 * 2 = 18 | 9 * 3 = 27 | 9 * 4 = 36 | 9 * 5 = 45 | 9 * 6 = 54 | 9 * 7 = 63 | 9 * 8 = 72 | 9 * 9 = 81

引数に型名を設定(comptime限定)

型名のためだけに変数を使うのはもったいないというのであれば、引数に型名を設定するという方法もあります。ただし、型名はコンパイル時点で確定していなくてはなりません。ポイントは、型の引数はtype型とし、引数の前にcomptimeと書いておくことです。これはZigで定義済みの関数でもよく利用されています。

関数内で使用できる型が限定される場合は@typeInfo(T)の値をもとに処理を振り分けます。(@typeInfo)

// 引数に型名を設定(comptime限定)
fn multiply2(comptime T: type, a: T, b: T) ?T {
    return switch (@typeInfo(T)) {
        .Int => a * b,      // 整数型のみ処理を行う
        else => null,
    };
}

// .........
try stdout.print("{} * {} = {d:>2}", .{ i, j, multiply2(u8, i, j).? });
// .........

@TypeOfで型名を設定

@TypeOfは引数の値が持つ型を得られる関数ですが、これを利用すると引数と戻り値の型を呼び出し時点で設定できるようになります。ポイントは型名を取得する引数の型をanytypeにすることと、処理を受け付ける型のみswitch (@typeInfo(@TypeOf(...))) { ... }で振り分けて処理を行うことです。
(@TypeOf)

// @TypeOf, @typeInfoで処理を振り分け
fn multiply3(a: anytype, b: @TypeOf(a)) ?@TypeOf(a) {
    return switch (@typeInfo(@TypeOf(a))) {
        .Int => a * b,      // 整数型のみ処理を行う
        else => null,
    };
}

// .........
try stdout.print("{} * {} = {d:>2}", .{ i, j, multiply3(i, j).? });
// .........

引数や戻り値の配列の長さを設定(comptime限定)

コンパイル時点で値が確定していなければなりませんが、引数や戻り値の配列の長さを引数の値を元にして設定できます。ポイントは配列の長さを規定する引数にcomptimeをつけることです。

const std = @import("std");

// 引数の配列の要素(0~3)を変換
const dict = [_][:0]const u8{ "zero", "one", "two", "three" };

// aの長さに応じて戻り値の配列の長さが決まる(comptime限定)
fn translateNumber(comptime a: []const u8) [a.len][:0]const u8 {
    var result: [a.len][:0]const u8 = undefined;
    for (0..a.len) |i| {
        result[i] = dict[a[i]];
    }
    return result;
}

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();

    const a = [_]u8{ 2, 3, 1, 0, 2, 1 };    // それぞれの要素が文字列に変換される
    try stdout.print("{any}\n{s}\n", .{ a, translateNumber(a[0..]) });
}
結果
{ 2, 3, 1, 0, 2, 1 }
{ two, three, one, zero, two, one }

まとめ

  • 関数(かんすう:Function)は新たに定義できる
  • 処理の流れは引数(ひきすう:Argument)を受け取って処理を行い、その結果を呼び出し元の関数に返す
  • 引数は複数定義できるが、返せる結果は1つのみ(ただし複数のデータを持つタプルや構造型の値で返せる)
  • 引数の値が同じなら結果も同じになるとき、処理の内容はより短いほうが良い
  • 型名は変数に代入できる
  • 関数の引数に型名(type型)を設定できる(comptime限定)
    @typeInfoで引数の型に応じた処理の振り分けができる
  • 型名を@TypeOfで定義すると引数や戻り値の型が呼び出し時点のものとなる
    @typeInfo@TypeOfで引数の型に応じた処理の振り分けができる
  • 引数や戻り値が配列のとき、その長さを指定できる(comptime限定)

エラー(error, try, catch, if...else, ビルトイン関数) >
< 共用型(union)と列挙型(enum)
ざっくりZig 一覧

Discussion