ざっくりZig - structの組込(usingnamespace)
struct
(構造型)は1つの型を表しますが、ある型に含まれる変数や関数を別の型に組み込んで新たな型を定義できます。これにより、異なる型でも共通の関数を持たせることができます。そのために利用するのがusingnamespace
です。ただし、共通の関数では組み込まれた型に対応する処理を定義しておかなくてはなりません。
usingnamespaceによるstructの組込
usingnamespace
はあるstruct
に定義されている変数や引数なしの関数を別のstruct
に組み込むことを表します。
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
によるソースコードの読み込みでも可能です。
const S = struct {
usingnamespace @import("usingnamespace_fb.zig");
};
S.fb(); // "fb"
std.debug.print("{s}\n", .{S.VB}); // S.VB = B
pub const VB = "b";
pub fn fb() void {
std.debug.print("fb\n", .{});
}
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) ...
のような関数はどのように組み込めばよいのでしょうか。以下のイテレータのnext
をusingnamespace
で組み込むようにしてみます。
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
で実行します。
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
関数でこれに実際の処理を行う関数を設定できるようにします。
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
関数で設定してみます。
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
関数でフィールドの関数を実行できるよう設定する
-
同じはたらきをする関数にstd.meta.eqlがあります。 ↩︎
Discussion