ざっくりZig - structの組込(usingnamespace)

2024/07/26に公開

struct(構造型)は1つの型を表しますが、ある型に含まれる変数や関数を別の型に組み込んで新たな型を定義できます。これにより、異なる型でも共通の関数を持たせることができます。そのために利用するのがusingnamespaceです。ただし、共通の関数では組み込まれた型に対応する処理を定義しておかなくてはなりません。

usingnamespaceによるstructの組込

usingnamespaceはあるstructに定義されている変数や引数なしの関数を別のstructに組み込むことを表します。

usingnamespaceによる変数と関数の組み込み
const std = @import("std");

const A = struct {
    const VA = "A";
    fn fa() void {
        std.debug.print("fa\n", .{});
    }
};

const S = struct {
    usingnamespace A;   // VA, faがSに組み込まれる
};

S.fa();  // "fa"
std.debug.print("{s}\n", .{S.VA});  // S.VA = A

組み込みは@importによるソースコードの読み込みでも可能です。

usingnamespace @import(...)による組み込み
const S = struct {
    usingnamespace @import("usingnamespace_fb.zig");
};

S.fb();  // "fb"
std.debug.print("{s}\n", .{S.VB});  // S.VB = B
usingnamespace_fb.zig
pub const VB = "b";

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

usingnamespace struct {...}のように直接組み込む型を示すこともできます。

usingnamespace struct {...}による組込
const S = struct {
    usingnamespace struct {
        const VC = "C";
        fn fc() void {
            std.debug.print("fc\n", .{});
        }
    };
};

S.fc();  // "fc"
std.debug.print("{s}\n", .{S.VC});  // S.VC = C

メソッドとなる関数の組み込み

では、fn f(self: Self) ...のような関数はどのように組み込めばよいのでしょうか。以下のイテレータのnextusingnamespaceで組み込むようにしてみます。

イテレータの例
const std = @import("std");

pub fn main() void {
    const ArrayIteratorU8 = struct {
        data: []const u8,
        index: usize = 0,

        const Self = @This();

        // この関数をusingnamespaceで組み込むには?
        fn next(self: *Self) ?u8 {
            if (self.index == self.data.len) {
                return null;
            }
            else {
                defer self.index += 1;
                return self.data[self.index];
            }
        }
    };

    const data = [_]u8{1, 2, 3};

    var ai = ArrayIteratorU8{ .data = &data };
    while(ai.next()) |v| { std.debug.print("v = {}\n", .{v}); }

    // 結果
    // v = 1
    // v = 2
    // v = 3
}

それには、まずnextを含むstructを返す関数を定義し、これをusingnamespaceで実行します。

組み込む関数を返す関数をusingnamespaceで実行
fn IterateArray(comptime Self: type) type {
    return struct {
        fn next(self: *Self) ?u8 {
            // ..... (snip) .....
        }
    };
}

pub fn main() void {
    const ArrayIteratorU8 = struct {
        data: []const u8,
        index: usize = 0,

        const Self = @This();

        usingnamespace IterateArray(Self);  // これによりnext関数が組み込まれる
    };

    // ..... (snip) .....
}

IterateArrayは結局のところArrayIteratorU8から配列の型を指定できるArrayIteratorに汎用化する関数と構造は同じです。

イテレータに型を指定できるよう汎用化する
const std = @import("std");

// 引数にTを追加
fn IterateArray(comptime Self: type, comptime T: type) type {
    return struct {
        fn next(self: *Self) ?T {
            // ..... (snip) .....
        }
    };
}

// イテレータの型を指定できるよう汎用化
fn ArrayIterator(comptime T: type) type {
    return struct {
        data: []const T,
        index: usize = 0,

        const Self = @This();

        usingnamespace IterateArray(Self, T);   // 引数にTを追加
    };
}

pub fn main() void {
    const data = [_]u8{1, 2, 3};
    var ai = ArrayIterator(u8){ .data = &data };
    while(ai.next()) |v| { std.debug.print("v = {}\n", .{v}); }

    // 結果
    // v = 1
    // v = 2
    // v = 3
}

異なる型に同じ関数を組み込む

これを応用して、異なる型に同じ関数を組み込む場合、組み込む側の関数でそれぞれの型への対応が必要になります。

ここではフィールドx, yを持つPoint2D型とフィールドx, y, zを持つPoint3D型に対し、各フィールドの値がすべて同じであればtrueそうでなければfalseを返すeql関数[1]を組み込めるようにしてみます。この関数では引数のフィールドの数が異なる場合があるため、std.meta.fields関数[2]で各フィールドを取得しつつその値を比較しています。

異なる型に同じ関数を組み込み
const std = @import("std");

// Point2D, Point3Dに組み込まれる関数
fn PointFn(comptime Self: type) type {
    return struct {
        // SelfはPoint2D, Point3Dのどちらか
        fn eql(self: Self, other: Self) bool {
            // 引数の型によってフィールドの数が異なる可能性がある
            // inlineはstd.meta.fields関数を使用するためのインライン化
            return inline for (std.meta.fields(Self)) |field| {
                if (@field(self, field.name) != @field(other, field.name)) break false;
            }
            else true;
        }
    };
}

fn Point2D(comptime T: type) type {
    return struct {
        x: T,
        y: T,
        usingnamespace PointFn(@This());    // eql関数を組み込む
    };
}

fn Point3D(comptime T: type) type {
    return struct {
        x: T,
        y: T,
        z: T,
        usingnamespace PointFn(@This());    // eql関数を組み込む
    };
}

pub fn main() void {
    const print = std.debug.print;

    const PointF2D = Point2D(f16);  // 各フィールドの型をf16に設定

    const PF2A = PointF2D { .x = 1.5, .y = -3.2 };
    const PF2B = PointF2D { .x = -3.2, .y = 1.5 };
    const PF2C = PointF2D { .x = 1.5, .y = -3.2 };

    print("PF2A.eql(PF2B) = {}\n", .{PF2A.eql(PF2B)});
    print("PF2A.eql(PF2C) = {}\n", .{PF2A.eql(PF2C)});

    const PointF3D = Point3D(f16);  // 各フィールドの型をf16に設定

    const PF3A = PointF3D { .x = 1.5, .y = -3.3, .z = -4.7 };
    const PF3B = PointF3D { .x = -3.3, .y = 1.5, .z = -9.2 };
    const PF3C = PointF3D { .x = 1.5, .y = -3.3, .z = -4.7 };

    print("PF3A.eql(PF3B) = {}\n", .{PF3A.eql(PF3B)});
    print("PF3A.eql(PF3C) = {}\n", .{PF3A.eql(PF3C)});
}
結果
PF2A.eql(PF2B) = false
PF2A.eql(PF2C) = true
PF3A.eql(PF3B) = false
PF3A.eql(PF3C) = true

共通のインターフェースを組み込む

これまで組み込む関数の内容はすべて共通でしたが、ここでは関数の内容を変えられるようにしてみます。

まずイテレータとして組み込むnext関数ではself.nextFnを実行するだけにします。ポイントはnextFn関数の引数にselfを設定していることです。

組み込む関数の定義
fn Iterator(comptime Self: type, comptime T: type) type {
    return struct {
        fn next(self: *Self) ?T {
            return self.nextFn(self);
        }
    };
}

次に、nextFnというフィールドを持つ型を定義し、init関数でこれに実際の処理を行う関数を設定できるようにします。

イテレータのnext関数はinitで設定
fn ArrayIterator(comptime T: type) type {
    return struct {
        data: []const T,
        index: usize = 0,
        nextFn: *const fn (*@This()) ?T,    // next関数の内容

        usingnamespace Iterator(@This(), T);    // next関数の組み込み

        fn init(arr: []const T, comptime M: type) @This() {
            return .{ .data = arr, .nextFn = M.next };  // nextFnにM.nextを設定
        }
    };
}

これにより共通のnext関数を組み込みつつ、その内容はinit関数で設定できるようになります。では実際に配列の要素を先頭から取得するArrayIteratorと末尾が取得するArrayIteratorのそれぞれをinit関数で設定してみます。

next関数の内容をinit関数で設定
pub fn main() void {
    const print = std.debug.print;

    const data = [_]u8{ 1, 2, 3 };
    const ArrayIteratorU8 = ArrayIterator(u8);

    // 配列の先頭の要素から順に取得するイテレータ
    var ai_u8_count_up = ArrayIteratorU8.init(&data, struct {
        fn next(self: *ArrayIteratorU8) ?u8 {
            if (self.index == self.data.len) return null;
            defer self.index += 1;
            return self.data[self.index];
        }
    });

    while(ai_u8_count_up.next()) |v| {
        print("ai_u8_count_up.next() = {?}\n", .{v});
    }

    // 配列の末尾の要素から順に取得するイテレータ
    var ai_u8_count_down = ArrayIteratorU8.init(&data, struct {
        fn next(self: *ArrayIteratorU8) ?u8 {
            if (self.index == self.data.len) return null;
            defer self.index += 1;
            return self.data[self.data.len - self.index - 1];
        }
    });

    while(ai_u8_count_down.next()) |v| {
        print("ai_u8_count_down.next() = {?}\n", .{v});
    }
}
結果
ai_u8_count_up.next() = 1
ai_u8_count_up.next() = 2
ai_u8_count_up.next() = 3
ai_u8_count_down.next() = 3
ai_u8_count_down.next() = 2
ai_u8_count_down.next() = 1

まとめ

  • usingnamespaceによってある型に含まれる変数や関数を別の型に組み込むことができる。
  • 組み込む方法は定義済みのstructを持つ変数、@import、直接structを設定のそれぞれが可能
  • fn f(self: Self) ...のような関数を持つstructを組み込む場合はSelfの型を設定したstructを返す関数をusingnamespaceで実行する
  • 異なる型に共通する関数を組み込む場合は、関数のほうでそれぞれの型に対応する処理を定義しておく
  • 組み込む関数の内容を変更できるようにするには、関数の内容を定義するフィールドを定義し、init関数でフィールドの関数を実行できるよう設定する

< キャスト(明示的な型変換)
ざっくりZig 一覧


脚注
  1. 同じはたらきをする関数にstd.meta.eqlがあります。 ↩︎

  2. std.meta.fields ↩︎

Discussion