Zigにはinterfaceという機能が無い代わりに、型変換によって共通のインターフェースを表現する。

パターン1、allocatorで使われているような「ファットポインタ」で表現する。 メリットとして、それぞれの構造体は完全に分離しているので粗結合度を上げることができる。

デメリットとしては、

  • 以下の場合、PersonインスタンスはそのままtoString()を叩くことは出来ない。必ず、Stringerに変換しないといけない(引数が@This()と対応してないとメンバ関数とはみなされない為)
  • 読んだり書くのがちょっと大変
const Stringer = struct {
    const VTable = struct {
        toString: *const fn (ptr: *anyopaque) []const u8,
    };
    ptr: *anyopaque,
    vt: *const VTable,
    pub fn toString(self: *Stringer) []const u8 {
        return self.vt.toString(self.ptr);
    }
};

const Person = struct {
    name: []const u8,
    fn stringer(self: *Person) Stringer {
        return Stringer{
            .ptr = self,
            .vt = &.{ .toString = Person.toString },
        };
    }
    fn toString(ptr: *anyopaque) []const u8 {
        const self: *Person = @ptrCast(@alignCast(ptr));
        return self.name;
    }
};

パターン2、共用体で表現するスタイル。Zig財団のロリスさんはこの方法をおすすめしている。 こちらではPersonインスタンスもそのままtoString()できる。何より実にわかりやすい。

デメリットとしては、

  • 関数毎にサポートする型をすべてハードコードしなければいけないので、消したり生やしたりをするのが大変

ただ、実際ハードコードされていたほうが読む側にとっては追いやすく楽なこともある。何よりこのパターンの場合は単純なので、保守する側は自信を持ちやすい。更にzigコンパイラはむしろ条件分岐のカバーを補足するので、面倒だがそこは指摘するという優秀さがある。
なので、バランスが難しいところ。例えば、多くても5つ程度の型のみをサポートする、って感じが良いんじゃないだろうか。多分、こういうのが「zigらしいコーディング」ってやつかもしれない。

const Stringer = union(enum) {
    person: Person,
    pub fn toString(self: Stringer) []const u8 {
        switch (self) {
            .person => |*p| return p.toString(),
        }
    }
};

const Person = struct {
    name: []const u8,
    fn stringer(self: Person) Stringer {
        return Stringer{ .person = self };
    }
    fn toString(self: Person) []const u8 {
        return self.name;
    }
};

ちなみに最後に、zigは継承の仕組みを持たないが、メタ的にmixinっぽいことはできる。
使う方法としてはusingnamespaceとジェネリクスを組み合わせる。これはまさにマクロっぽい手法。
上手く汎化しないといけないので、使い方はちょっと難しそう。

fn AbstractMath(comptime Self: type, comptime T: type) type {
    return struct {
        pub fn add(self: Self, b: T) T {
            return self.value + b;
        }
        pub fn sub(self: Self, b: T) T {
            return self.value - b;
        }
    };
}

const Int = struct {
    value: i32,
    usingnamespace AbstractMath(@This(), i32);
};

const Float = struct {
    value: f64,
    usingnamespace AbstractMath(@This(), f64);
};

実際には使いやすいとは言えないと思うけど、以下みたいな感じで「インターフェースをimplementsする」みたいなことは擬似的にできる。

pub fn Stringer(comptime Self: type) type {
    return struct {
        pub fn string(self: Self) []const u8 {
            if (!@hasDecl(Self, "stringImpl")) @compileError("Not implemented");
            return self.stringImpl();
        }
    };
}

const Person = struct {
    name: []const u8,
    age: u32,
    usingnamespace Stringer(Person);
    fn stringImpl(self: Person) []const u8 {
        var buf: [256]u8 = undefined;
        return std.fmt.bufPrint(&buf, "Person: name: {s}, age: {d}", .{ self.name, self.age }) catch return "";
    }
};

ちなみにこの機能は複雑さを伴う理由から削除される可能性が高いらしい。やるなら今だけかも?

パッケージマネージャあった

ちなみに先日パッケージマネージャまだ出来てない?と書いたけどついてた。ただしまだ完璧に出来てないって感じなのと、使い方はまだ良くわかってない。 端的に言うと、build.zig.zonってやつに追加すれば良い。ただし、build.zigに手動で設定が必要。ここはちゃんとわかればカスタマイズ製が良さそうなんだけど、現状はやや厄介なところ(将来はもうちょっと自動化されるかも?)。 とりあえず外部ライブラリ使えたのに感動したのと、挙動自体はかなり今後に期待できそうなやつだった。