A library for duck type checking during compilation. It provides the satisfyTrait function to verify at compile time whether a type satisfies the contract defined by a trait.
Similar concepts in other languages:
| Language | Concept | Keyword |
|---|---|---|
| Rust | Trait | trait / impl |
| C++ | Concept | concept / requires |
| Haskell | Typeclass | class / instance |
| Swift | Protocol | protocol |
| Go | Interface (structural) | interface |
| Java | Interface (nominal) | interface / implements |
Run the following command to add vftrait to your project:
zig fetch --save git+https://github.com/Verafahn/vftrait#masterThen add the module in your build.zig:
const vftrait = b.dependency("vftrait", .{}).module("vftrait");
exe.root_module.addImport("vftrait", vftrait);Define a trait as a struct whose declarations describe the contract.
Then call satisfyTrait to check whether a type implements the trait.
const vftrait = @import("vftrait");
// 1. Define the trait
const Printable = struct {
pub fn format(self: *const @This(), writer: anytype) !void {
_ = self;
_ = writer;
}
};
// 2. Implement the trait implicitly (no `impl` keyword required)
const Point = struct {
x: f32,
y: f32,
pub fn format(self: *const @This(), writer: anytype) !void {
try writer.print("Point({{.x = {d}, .y = {d}}})", .{ self.x, self.y });
}
};
// 3. Check at compile time
comptime {
if (!vftrait.satisfyTrait(Printable, Point)) {
@compileError("Point must implement Printable");
}
}For example, a specific usage example:
const vftrait = @import("vftrait");
const std = @import("std");
pub const Drawable = struct {
pub fn draw(self: *const @This()) void {
_ = self;
}
};
pub const Circle = struct {
radius: f32,
pub fn draw(self: *const @This()) void {
std.debug.print("Circle(radius={})", .{self.radius});
}
};
pub const Point = struct {
x: f32,
y: f32,
pub fn draw(self: *const @This()) void {
std.debug.print("Point(x={}, y={})", .{ self.x, self.y });
}
};
pub fn draw(shape: anytype) void {
vftrait.assertSatisfyTrait(Drawable, @TypeOf(shape));
shape.draw();
return;
}
pub fn main(init: std.process.Init) !void {
_ = init;
const circle = Circle{ .radius = 5.0 };
draw(circle);
const point = Point{ .x = 1.0, .y = 2.0 };
draw(point);
// draw(10); // Error: @TypeOf(10) does not satisfy Drawable
}The trait struct can contain four kinds of declarations:
| Category | Zig Declaration | Example |
|---|---|---|
| Associated constant | pub const name: T = ...; |
pub const max_size: usize = 1024; |
| Associated type | pub const Name = some_type; |
pub const Item = struct {}; |
| Associated function | pub fn name(...) ... { } |
pub fn fromInt(n: i32) Self { } |
| Method | pub fn name(self: *Self, ...) ... { } |
pub fn clone(self: *const Self) Self { } |
- Constants: Must have exactly the same type.
- Types: Must be of the same kind (e.g.,
structvsstruct,errorvserror).
Nested types are not recursively checked – only the top‑level kind is compared. - Functions and methods: Must have the same calling convention and arity, and each parameter type and return type must be isomorphic (structurally equivalent) to the corresponding associated type in the trait.
When an associated type in the trait is set to the built‑in type, that position accepts any type:
const vftrait = @import("vftrait");
const Trait = struct {
pub const Value = type; // Accepts any type
pub fn get(self: *const @This()) Value {
_ = self;
}
};
const IntHolder = struct {
pub const Value = i32;
pub fn get(self: *const @This()) i32 {
_ = self;
return 42;
}
};
const StrHolder = struct {
pub const Value = []const u8;
pub fn get(self: *const @This()) []const u8 {
_ = self;
return "hi";
}
};
comptime {
@compileLog(vftrait.satisfyTrait(Trait, IntHolder)); // true
@compileLog(vftrait.satisfyTrait(Trait, StrHolder)); // true
}This also works for u32, struct, union, error, enum, pointers, optionals, and any other type.
- Zig ≥ 0.16.0
MIT