Lesson 14 / 25

Generic Types and Functions

Write generic data structures with functions that return types.

Functions that return types

A generic function takes a comptime T: type parameter, or uses anytype to infer the parameter's type at each call: fn max(comptime T: type, a: T, b: T) T. A generic data structure is a function that returns a type: fn Stack(comptime T: type) type { return struct { ... }; }, used as Stack(u32). Inside, @This() names the returned struct, conventionally as const Self = @This();. The standard library uses this everywhere: std.ArrayList(T), std.AutoHashMap(K, V), std.PriorityQueue(T, Context, compareFn). Because generics are just compile-time function calls, you can add constraints and errors in plain code: check @typeInfo(T) and call @compileError with a helpful message if a type is unsupported. Instantiations with the same arguments are memoised, so Stack(u32) is always the same type. Zig has no interfaces or traits keyword; runtime polymorphism uses explicit vtables (a struct of function pointers plus a context pointer), as std.mem.Allocator and std.Io.Writer do, or tagged unions when the set of implementations is closed.

A generic bounded stack

A function returning a struct type, with comptime capacity.

const std = @import("std");

fn BoundedStack(comptime T: type, comptime capacity: usize) type {
    return struct {
        items: [capacity]T = undefined,
        len: usize = 0,

        const Self = @This();

        pub fn push(self: *Self, value: T) error{Full}!void {
            if (self.len == capacity) return error.Full;
            self.items[self.len] = value;
            self.len += 1;
        }

        pub fn pop(self: *Self) ?T {
            if (self.len == 0) return null;
            self.len -= 1;
            return self.items[self.len];
        }

        pub fn peek(self: *const Self) ?T {
            return if (self.len == 0) null else self.items[self.len - 1];
        }
    };
}

fn largest(comptime T: type, values: []const T) ?T {
    if (values.len == 0) return null;
    var best = values[0];
    for (values[1..]) |v| best = @max(best, v);
    return best;
}

test "bounded stack" {
    var s: BoundedStack(u32, 2) = .{};
    try s.push(10);
    try s.push(20);
    try std.testing.expectError(error.Full, s.push(30));
    try std.testing.expectEqual(@as(?u32, 20), s.pop());
    try std.testing.expectEqual(@as(?u32, 10), s.peek());
}

test "largest" {
    try std.testing.expectEqual(@as(?i32, 9), largest(i32, &.{ 3, 9, -2 }));
    try std.testing.expectEqual(@as(?f64, null), largest(f64, &.{}));
}

A mould factory

BoundedStack is not a stack; it is a factory that makes stack moulds. Ask for a mould for u32 with room for 2, and the compiler casts it once; every later request for the same mould gets the same one.

Quick check: How are generic data structures usually written in Zig?

  • With a template keyword
  • With macros
  • With inheritance
  • As functions that take comptime parameters and return a type
Answer

As functions that take comptime parameters and return a type — For example, fn Stack(comptime T: type) type { return struct { ... }; }.