# Metaprogramming Patterns

### Metaprogramming Patterns

Metaprogramming means writing code that helps create, inspect, or specialize other code.

In Zig, metaprogramming is mostly built from a few simple tools:

| Tool | Purpose |
|---|---|
| `comptime` | Run code during compilation |
| `comptime T: type` | Pass a type into a function |
| `anytype` | Let the compiler infer a generic parameter |
| `inline for` | Expand repeated code at compile time |
| `@typeInfo` | Inspect a type |
| `@field` | Access a field by name |
| `@hasDecl` | Check whether a type has a declaration |
| `@compileError` | Stop compilation with a custom error |

Zig does not usually need a separate macro language. The metaprogramming language is Zig itself.

#### Pattern 1: Type Functions

A type function is a function that returns a type.

```zig
fn Box(comptime T: type) type {
    return struct {
        value: T,
    };
}
```

Usage:

```zig
const IntBox = Box(i32);
const BoolBox = Box(bool);
```

This pattern is the base of Zig generic data structures.

You pass information known at compile time, and the function returns a concrete type.

#### Pattern 2: Generic Methods

A generated type can contain methods that use the compile-time type.

```zig
fn Pair(comptime T: type) type {
    return struct {
        first: T,
        second: T,

        pub fn swap(self: *@This()) void {
            const tmp = self.first;
            self.first = self.second;
            self.second = tmp;
        }
    };
}
```

Usage:

```zig
var p = Pair(i32){
    .first = 10,
    .second = 20,
};

p.swap();
```

`@This()` refers to the concrete generated type.

For `Pair(i32)`, it means the `Pair(i32)` type.

#### Pattern 3: Compile-Time Validation

Use `@compileError` to reject unsupported input early.

```zig
fn requireInteger(comptime T: type) void {
    switch (@typeInfo(T)) {
        .int => {},
        else => @compileError("expected an integer type"),
    }
}
```

Usage:

```zig
comptime {
    requireInteger(u32);
}
```

This succeeds.

```zig
comptime {
    requireInteger(bool);
}
```

This fails during compilation.

This pattern is useful in libraries. Instead of letting users get a long confusing type error, you can give them a direct message.

#### Pattern 4: Type-Based Dispatch

Use `switch` or `if` on a type.

```zig
fn defaultValue(comptime T: type) T {
    return switch (@typeInfo(T)) {
        .bool => false,
        .int => 0,
        .float => 0.0,
        else => @compileError("unsupported type"),
    };
}
```

Usage:

```zig
const a = defaultValue(bool);
const b = defaultValue(u32);
const c = defaultValue(f64);
```

The compiler chooses the correct branch.

There is no runtime type check.

#### Pattern 5: Field Iteration

Use `@typeInfo` and `inline for` to inspect struct fields.

```zig
fn fieldCount(comptime T: type) usize {
    return switch (@typeInfo(T)) {
        .@"struct" => |info| info.fields.len,
        else => @compileError("expected a struct type"),
    };
}
```

Usage:

```zig
const User = struct {
    id: u64,
    name: []const u8,
    active: bool,
};

const count = fieldCount(User);
```

The result is `3`, known at compile time.

Field iteration is the base for serializers, validators, formatters, and mappers.

#### Pattern 6: Accessing Fields by Name

`@field` lets you access a field when the field name is known as data.

```zig
const std = @import("std");

fn printFields(comptime T: type, value: T) void {
    switch (@typeInfo(T)) {
        .@"struct" => |info| {
            inline for (info.fields) |field| {
                const field_value = @field(value, field.name);
                std.debug.print("{s} = {}\n", .{ field.name, field_value });
            }
        },
        else => @compileError("expected a struct type"),
    }
}
```

Usage:

```zig
const User = struct {
    id: u64,
    active: bool,
};

pub fn main() void {
    const user = User{
        .id = 1,
        .active = true,
    };

    printFields(User, user);
}
```

The compile-time loop generates direct field access.

Conceptually, the compiler creates code like this:

```zig
std.debug.print("{s} = {}\n", .{ "id", user.id });
std.debug.print("{s} = {}\n", .{ "active", user.active });
```

#### Pattern 7: Checking for Declarations

`@hasDecl` checks whether a type has a declaration.

```zig
fn requireInit(comptime T: type) void {
    if (!@hasDecl(T, "init")) {
        @compileError("type must provide init");
    }
}
```

This is useful when writing generic code that expects a type to provide certain functions.

```zig
fn make(comptime T: type) T {
    requireInit(T);
    return T.init();
}
```

If `T` has no `init`, compilation fails.

This is a lightweight way to express an interface-like requirement.

#### Pattern 8: Generic Functions with `anytype`

`anytype` lets the compiler infer the concrete type.

```zig
fn printTwice(value: anytype) void {
    const std = @import("std");
    std.debug.print("{} {}\n", .{ value, value });
}
```

Usage:

```zig
printTwice(10);
printTwice(true);
```

The compiler checks each call separately.

This is useful for small generic helpers where writing `comptime T: type` would add noise.

For larger APIs, explicit type parameters are often clearer.

#### Pattern 9: Compile-Time Tables

You can build fixed lookup tables during compilation.

```zig
fn makeSquares() [8]u32 {
    comptime var values: [8]u32 = undefined;

    inline for (0..8) |i| {
        values[i] = @intCast(i * i);
    }

    return values;
}

const squares = makeSquares();
```

The final table is:

```zig
.{ 0, 1, 4, 9, 16, 25, 36, 49 }
```

The work is done during compilation.

At runtime, the program already has the data.

#### Pattern 10: Compile-Time Configuration

A compile-time flag can remove unused code.

```zig
fn Logger(comptime enabled: bool) type {
    return struct {
        pub fn log(message: []const u8) void {
            if (enabled) {
                @import("std").debug.print("{s}\n", .{message});
            }
        }
    };
}
```

Usage:

```zig
const DebugLog = Logger(true);
const SilentLog = Logger(false);
```

For `Logger(false)`, the compiler can remove the print path.

This is useful for debug logging, feature flags, tracing, and optional checks.

#### Pattern 11: Static Interface Checks

Zig does not have traditional interfaces, but you can check for required declarations.

```zig
fn requireReset(comptime T: type) void {
    if (!@hasDecl(T, "reset")) {
        @compileError("type must define reset");
    }
}
```

Use it with a generic function:

```zig
fn resetOne(value: anytype) void {
    const T = @TypeOf(value.*);
    requireReset(T);
    value.reset();
}
```

This expects a pointer to a value whose type has a `reset` method.

Example type:

```zig
const Counter = struct {
    value: u32,

    pub fn reset(self: *@This()) void {
        self.value = 0;
    }
};
```

This works:

```zig
var c = Counter{ .value = 10 };
resetOne(&c);
```

A type without `reset` fails at compile time.

#### Pattern 12: Generated Wrappers

You can generate wrappers around a type.

```zig
fn OptionalBox(comptime T: type) type {
    return struct {
        value: ?T = null,

        pub fn set(self: *@This(), value: T) void {
            self.value = value;
        }

        pub fn get(self: @This()) ?T {
            return self.value;
        }
    };
}
```

Usage:

```zig
var box = OptionalBox(u32){};
box.set(42);
```

This pattern appears in containers, caches, parsers, and API adapters.

#### Pattern 13: Struct-Based Configuration

A common Zig style is to pass a compile-time config struct.

```zig
fn Buffer(comptime config: anytype) type {
    return struct {
        data: [config.capacity]u8 = undefined,
        len: usize = 0,
    };
}
```

Usage:

```zig
const Small = Buffer(.{
    .capacity = 64,
});
```

This is clearer than passing many separate compile-time arguments.

You can also validate the config:

```zig
fn Buffer(comptime config: anytype) type {
    if (config.capacity == 0) {
        @compileError("capacity must be greater than zero");
    }

    return struct {
        data: [config.capacity]u8 = undefined,
        len: usize = 0,
    };
}
```

#### Pattern 14: Reflection-Based Validation

You can inspect a struct and reject fields that do not meet your rules.

```zig
fn requireOnlyIntegers(comptime T: type) void {
    switch (@typeInfo(T)) {
        .@"struct" => |info| {
            inline for (info.fields) |field| {
                switch (@typeInfo(field.type)) {
                    .int => {},
                    else => @compileError("all fields must be integers"),
                }
            }
        },
        else => @compileError("expected a struct type"),
    }
}
```

Usage:

```zig
const Point = struct {
    x: i32,
    y: i32,
};

comptime {
    requireOnlyIntegers(Point);
}
```

This succeeds.

A struct with a `[]const u8` field would fail.

#### Pattern 15: Specializing for Performance

Sometimes you can remove runtime checks by moving a choice to compile time.

```zig
const Mode = enum {
    checked,
    unchecked,
};

fn add(comptime mode: Mode, a: u32, b: u32) u32 {
    return switch (mode) {
        .checked => a + b,
        .unchecked => a +% b,
    };
}
```

Usage:

```zig
const x = add(.checked, 10, 20);
const y = add(.unchecked, 10, 20);
```

The compiler knows the mode for each call.

The final code does not need a runtime `switch`.

#### Use Metaprogramming Sparingly

Metaprogramming can make code powerful, but it can also make code harder to read.

Prefer simple runtime code when the choice depends on runtime data.

Use metaprogramming when the choice is structural:

types

field names

array sizes

static options

build flags

protocol layouts

fixed tables

Avoid metaprogramming when it only makes ordinary code look clever.

#### Mental Model

Zig metaprogramming has a simple shape:

```zig
fn Tool(comptime input: type) type {
    return struct {
        // generated structure
    };
}
```

Or:

```zig
fn useType(comptime T: type, value: T) void {
    const info = @typeInfo(T);

    switch (info) {
        .@"struct" => |struct_info| {
            inline for (struct_info.fields) |field| {
                // generated field-specific code
            }
        },
        else => @compileError("unsupported type"),
    }
}
```

The compiler runs the compile-time part, checks it, and emits direct runtime code.

That is the central pattern: use Zig to shape Zig before the program runs.

