Interface Pattern with comptime
In Zig, you can achieve polymorphism without dynamic dispatch (vtable) by leveraging the comptime feature to resolve types at compile time. Because interface-like behavior is determined at compile time, there is no runtime overhead and type safety is preserved. This page explains the basic structure and implementation of interface patterns using Zig's comptime.
Syntax
// -----------------------------------------------
// Basic structure of a comptime interface pattern
// -----------------------------------------------
// Receives a type as a comptime argument, acting as an "interface"
fn doSomething(comptime T: type, value: T) void {
// Checks at compile time that T has a specific method
value.methodName();
}
// -----------------------------------------------
// @hasDecl — checks at compile time whether a type has a specific declaration
// -----------------------------------------------
comptime {
if (!@hasDecl(T, "methodName")) {
@compileError("Type T must implement methodName");
}
}
// -----------------------------------------------
// anytype — shorthand for receiving any type via comptime
// -----------------------------------------------
fn callMethod(value: anytype) void {
// anytype is shorthand for comptime T: type + value: T
value.method();
}
// -----------------------------------------------
// Pattern for defining a struct that acts as an "interface"
// -----------------------------------------------
// Defines a type with methods that represent an "interface"
const MyInterface = struct {
pub fn execute(self: @This()) void { _ = self; }
pub fn getName(self: @This()) []const u8 { _ = self; return ""; }
};
// Implementing types only need to have methods with matching names — no inheritance required
Syntax Reference
| Syntax / Method | Description |
|---|---|
comptime T: type | Receives a type as a compile-time argument. The caller's type is used as-is. |
anytype | Shorthand syntax for receiving any type via comptime. Can be written directly as a function argument. |
@hasDecl(T, "name") | Checks at compile time whether type T has a declaration (method, field, etc.) with the given name. |
@hasField(T, "name") | Checks at compile time whether type T has a field with the given name. |
@compileError("msg") | Emits an error message at compile time and stops compilation. Used to report interface violations. |
@TypeOf(value) | Retrieves the type of a value at compile time. |
@typeInfo(T) | Retrieves type metadata (details of structs, unions, enums, etc.) at compile time. |
comptime { ... } | Executes the code inside the block at compile time. Used for type checking and validation. |
inline for | Unrolls a loop at compile time. Used to iterate over a type's list of fields. |
Sample Code
psychopass_comptime_interface.zig
// psychopass_comptime_interface.zig
// A sample using PSYCHO-PASS characters to demonstrate
// the comptime interface pattern
const std = @import("std");
// -----------------------------------------------
// A function that enforces the "Inspector" interface.
// Receives a type at compile time via comptime T: type
// and verifies that the required methods are implemented using @hasDecl.
// -----------------------------------------------
fn assertInspectorInterface(comptime T: type) void {
// Checks that the getName() method exists
if (!@hasDecl(T, "getName")) {
@compileError("Inspector interface violation: getName() is not implemented");
}
// Checks that the getCrimeCoefficient() method exists
if (!@hasDecl(T, "getCrimeCoefficient")) {
@compileError("Inspector interface violation: getCrimeCoefficient() is not implemented");
}
// Checks that the report() method exists
if (!@hasDecl(T, "report")) {
@compileError("Inspector interface violation: report() is not implemented");
}
}
// -----------------------------------------------
// A generic function that works with any type implementing the Inspector interface.
// Using anytype allows it to accept any type with the same interface.
// -----------------------------------------------
fn dispatchInspector(inspector: anytype) void {
// Retrieves the type at compile time using @TypeOf() and validates the interface
assertInspectorInterface(@TypeOf(inspector));
// Calls methods through the interface.
// These calls are resolved at compile time, so no vtable is needed.
const name = inspector.getName();
const coeff = inspector.getCrimeCoefficient();
inspector.report();
const stdout = std.io.getStdOut().writer();
stdout.print(" [{s}] Crime Coefficient: {d}\n", .{ name, coeff }) catch {};
}
// -----------------------------------------------
// Tsunemori Akane — a rookie inspector in Division 1.
// Implements the Inspector interface.
// -----------------------------------------------
const AkanetsunemoriInspector = struct {
name: []const u8,
crime_coefficient: u32,
pub fn getName(self: @This()) []const u8 {
return self.name;
}
pub fn getCrimeCoefficient(self: @This()) u32 {
return self.crime_coefficient;
}
pub fn report(self: @This()) void {
const stdout = std.io.getStdOut().writer();
// Tsunemori Akane calmly analyzes the situation and reports
stdout.print(" Tsunemori Akane: Accurately assesses the scene and carries out the mission.\n", .{}) catch {};
_ = self;
}
};
// -----------------------------------------------
// Ginoza Nobuchika — a veteran enforcer in Division 1.
// Implements the same Inspector interface.
// -----------------------------------------------
const GinozaNobuchikaInspector = struct {
name: []const u8,
crime_coefficient: u32,
pub fn getName(self: @This()) []const u8 {
return self.name;
}
pub fn getCrimeCoefficient(self: @This()) u32 {
return self.crime_coefficient;
}
pub fn report(self: @This()) void {
const stdout = std.io.getStdOut().writer();
// Ginoza Nobuchika follows rules strictly and reports by the book
stdout.print(" Ginoza Nobuchika: Executes duties methodically, in accordance with regulations.\n", .{}) catch {};
_ = self;
}
};
// -----------------------------------------------
// Kogami Shinya — an enforcer in Division 1.
// Implements the same interface but with different behavior.
// -----------------------------------------------
const KogamiShin_yaInspector = struct {
name: []const u8,
crime_coefficient: u32,
pub fn getName(self: @This()) []const u8 {
return self.name;
}
pub fn getCrimeCoefficient(self: @This()) u32 {
return self.crime_coefficient;
}
pub fn report(self: @This()) void {
const stdout = std.io.getStdOut().writer();
// Kogami Shinya tends to act on his own judgment
stdout.print(" Kogami Shinya: Acts according to his own convictions.\n", .{}) catch {};
_ = self;
}
};
// -----------------------------------------------
// A generic "group briefing" function using comptime.
// No need to write a separate function per type — the logic is shared.
// -----------------------------------------------
fn conductBriefing(comptime T: type, members: []const T) void {
// Validates the interface once at compile time
assertInspectorInterface(T);
const stdout = std.io.getStdOut().writer();
stdout.print("--- Briefing Start ---\n", .{}) catch {};
for (members) |member| {
dispatchInspector(member);
}
stdout.print("--- Briefing End ---\n", .{}) catch {};
}
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("=== PSYCHO-PASS comptime Interface Pattern ===\n\n", .{});
// Creates an instance of Tsunemori Akane
const akane = AkanetsunemoriInspector{
.name = "Tsunemori Akane",
.crime_coefficient = 28,
};
// Creates an instance of Ginoza Nobuchika
const ginoza = GinozaNobuchikaInspector{
.name = "Ginoza Nobuchika",
.crime_coefficient = 85,
};
// Creates an instance of Kogami Shinya
const kogami = KogamiShin_yaInspector{
.name = "Kogami Shinya",
.crime_coefficient = 121,
};
// -----------------------------------------------
// Calls dispatchInspector() for each instance individually.
// Each call has its type fully resolved at compile time,
// so no dynamic dispatch occurs at all.
// -----------------------------------------------
try stdout.print("[Individual Dispatch]\n", .{});
dispatchInspector(akane);
dispatchInspector(ginoza);
dispatchInspector(kogami);
try stdout.print("\n", .{});
// -----------------------------------------------
// Processes a slice of the same type in a group briefing.
// conductBriefing is specialized at compile time via comptime T: type.
// -----------------------------------------------
try stdout.print("[Briefing (same-type slice)]\n", .{});
const akane_team = [_]AkanetsunemoriInspector{akane};
conductBriefing(AkanetsunemoriInspector, &akane_team);
try stdout.print("\n", .{});
// -----------------------------------------------
// Sample with Masaoka Tomomi — verifies the same pattern works with additional types.
// -----------------------------------------------
const MasakazuMasaokiEnforcer = struct {
name: []const u8,
crime_coefficient: u32,
pub fn getName(self: @This()) []const u8 {
return self.name;
}
pub fn getCrimeCoefficient(self: @This()) u32 {
return self.crime_coefficient;
}
pub fn report(self: @This()) void {
const out = std.io.getStdOut().writer();
out.print(" Masaoka Tomomi: Takes command of the scene, drawing on years of experience.\n", .{}) catch {};
_ = self;
}
};
// Sample with Karanomori Shion
const ShionKaranomoriAnalyst = struct {
name: []const u8,
crime_coefficient: u32,
pub fn getName(self: @This()) []const u8 {
return self.name;
}
pub fn getCrimeCoefficient(self: @This()) u32 {
return self.crime_coefficient;
}
pub fn report(self: @This()) void {
const out = std.io.getStdOut().writer();
out.print(" Karanomori Shion: Analyzes data and proposes optimal tactics.\n", .{}) catch {};
_ = self;
}
};
const masaoka = MasakazuMasaokiEnforcer{ .name = "Masaoka Tomomi", .crime_coefficient = 156 };
const shion = ShionKaranomoriAnalyst{ .name = "Karanomori Shion", .crime_coefficient = 45 };
try stdout.print("[Different types handled by the same function]\n", .{});
dispatchInspector(masaoka);
dispatchInspector(shion);
try stdout.print("\ncomptime interface pattern demo complete.\n", .{});
}
zig run psychopass_comptime_interface.zig === PSYCHO-PASS comptime Interface Pattern === [Individual Dispatch] Tsunemori Akane: Accurately assesses the scene and carries out the mission. [Tsunemori Akane] Crime Coefficient: 28 Ginoza Nobuchika: Executes duties methodically, in accordance with regulations. [Ginoza Nobuchika] Crime Coefficient: 85 Kogami Shinya: Acts according to his own convictions. [Kogami Shinya] Crime Coefficient: 121 [Briefing (same-type slice)] --- Briefing Start --- Tsunemori Akane: Accurately assesses the scene and carries out the mission. [Tsunemori Akane] Crime Coefficient: 28 --- Briefing End --- [Different types handled by the same function] Masaoka Tomomi: Takes command of the scene, drawing on years of experience. [Masaoka Tomomi] Crime Coefficient: 156 Karanomori Shion: Analyzes data and proposes optimal tactics. [Karanomori Shion] Crime Coefficient: 45 comptime interface pattern demo complete.
Overview
Zig's comptime interface pattern is a technique for enforcing duck typing at compile time, without the explicit interface declarations found in languages like Java or Go. A type is received via comptime T: type or anytype, and method existence is verified at compile time using @hasDecl(). If a type that does not satisfy the interface is passed, @compileError() immediately triggers a compile error, so no runtime errors can occur. As shown in dispatchInspector() and conductBriefing() in the sample code, calls are fully type-resolved at compile time, meaning no dynamic dispatch through a virtual table (vtable) or function pointer ever takes place — achieving zero-cost abstraction similar to C++ templates. In situations where dynamic dispatch is needed (type erasure, runtime polymorphism), a tagged union or a function-pointer-based interface struct is more appropriate. For related background knowledge, see comptime basics and @typeInfo and type reflection, which covers working with type metadata.
If you find any errors or copyright issues, please contact us.