ざっくり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 }

関数の型

関数にも型があります。具体的には関数名と仮引数を省いたfn (仮引数の型名, ...) 戻り値の型名です。これを使って関数を別の関数の引数に設定できます。

配列の要素をソートする関数sortにて、引数condに設定する関数により結果を昇順(lessThan)とするか降順(greaterThan)とするか変えられるように定義してみます。これを実行する際はsort(T, &a, lessThanもしくはgreaterThan)となります。cond: fn (type, T, T) boollessThangreaterThanの型が一致していることを確認してください。

ソートする関数(ソート順を関数で設定)
// ソート昇順(sortの引数、関数の型はfn (type, T, T) bool)
fn lessThan(comptime T: type, a: T, b: T) bool {
    return a < b;
}

// ソート降順(sortの引数、関数の型はfn (type, T, T) bool)
fn greaterThan(comptime T: type, a: T, b: T) bool {
    return a > b;
}

// 配列の要素をソートする関数(選択ソート)
// condが関数の引数(結果が昇順か降順かを設定)
fn sort(comptime T: type, a: []T, cond: fn (type, T, T) bool) void {
    for (a, 0..) |_, i| {
        var pos = i;
        for (i + 1..a.len) |j| {
            // 引数の関数(lessThanかgreaterThan)で要素を比較
            if (cond(T, a[j], a[pos])) pos = j;
        }
        if (pos != i) {
            const w = a[i];
            a[i] = a[pos];
            a[pos] = w;
        }
    }
}

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

    // 要素をソートする配列
    const a = [_]u8{ 8, 17, 11, 1, 15, 15, 18, 9, 3, 16, 18, 19, 7, 15, 3, 1 };
    try stdout.print("a = {any}\n", .{a});

    // 昇順にソート
    var al = a;
    sort(u8, &al, lessThan);    // &は配列の要素が変更されることを表す
    try stdout.print("sort, lessThan:    {any}\n", .{al});

    // 降順にソート
    var ag = a;
    sort(u8, &ag, greaterThan); // &は配列の要素が変更されることを表す
    try stdout.print("sort, greaterThan: {any}\n", .{ag});
}
結果
a = { 8, 17, 11, 1, 15, 15, 18, 9, 3, 16, 18, 19, 7, 15, 3, 1 }
sort, lessThan:    { 1, 1, 3, 3, 7, 8, 9, 11, 15, 15, 15, 16, 17, 18, 18, 19 }
sort, greaterThan: { 19, 18, 18, 17, 16, 15, 15, 15, 11, 9, 8, 7, 3, 3, 1, 1 }
s

まとめ

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

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

Discussion