Language
日本語
English

Caution

JavaScript is disabled in your browser.
This site uses JavaScript for features such as search.
For the best experience, please enable JavaScript before browsing this site.

  1. Home
  2. Zig Dictionary
  3. Interface Pattern with comptime

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 / MethodDescription
comptime T: typeReceives a type as a compile-time argument. The caller's type is used as-is.
anytypeShorthand 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 forUnrolls 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 .