🎓 A1Learner
🔍
0 of 55 lessons completed

Zig Generic Functions

A generic function is a function written for a type that the caller chooses. One definition works for many types, and the caller names the type at each call.

One function reads the first item of any type

The function takes the element type as a comptime argument. It then reads the first item of a slice of that type.

const std = @import("std");

fn first(comptime T: type, items: []const T) T {
    return items[0];
}

pub fn main() !void {
    const out = std.io.getStdOut().writer();
    const nums = [_]i32{ 9, 4, 7 };
    const words = [_][]const u8{ "zig", "code" };
    try out.print("{d}\n", .{first(i32, &nums)});
    try out.print("{s}\n", .{first([]const u8, &words)});
}

The largest item works for i32 and u8

The loop compares items with the same code for both types. Only the type argument changes between the two calls.

const std = @import("std");

fn largest(comptime T: type, items: []const T) T {
    var best = items[0];
    for (items[1..]) |item| {
        if (item > best) {
            best = item;
        }
    }
    return best;
}

pub fn main() !void {
    const out = std.io.getStdOut().writer();
    const nums = [_]i32{ 3, 9, 4 };
    const ratings = [_]u8{ 70, 95, 80 };
    try out.print("{d}\n", .{largest(i32, &nums)});
    try out.print("{d}\n", .{largest(u8, &ratings)});
}

A total is built in the caller's type

The sum starts at zero in type T. Each item is added with the same loop, whatever T is.

const std = @import("std");

fn total(comptime T: type, items: []const T) T {
    var sum: T = 0;
    for (items) |item| {
        sum += item;
    }
    return sum;
}

pub fn main() !void {
    const out = std.io.getStdOut().writer();
    const nums = [_]i32{ 3, 1, 4 };
    const small = [_]u8{ 10, 20, 5 };
    try out.print("{d}\n", .{total(i32, &nums)});
    try out.print("{d}\n", .{total(u8, &small)});
}

Example

const std = @import("std");

fn first(comptime T: type, items: []const T) T {
    return items[0];
}

fn largest(comptime T: type, items: []const T) T {
    var best = items[0];
    for (items[1..]) |item| {
        if (item > best) {
            best = item;
        }
    }
    return best;
}

fn total(comptime T: type, items: []const T) T {
    var sum: T = 0;
    for (items) |item| {
        sum += item;
    }
    return sum;
}

pub fn main() !void {
    const out = std.io.getStdOut().writer();
    const nums = [_]i32{ 5, 8, 2 };
    const marks = [_]u8{ 40, 60, 50 };
    try out.print("{d}\n", .{first(i32, &nums)});
    try out.print("{d}\n", .{largest(i32, &nums)});
    try out.print("{d}\n", .{total(i32, &nums)});
    try out.print("{d}\n", .{first(u8, &marks)});
    try out.print("{d}\n", .{largest(u8, &marks)});
    try out.print("{d}\n", .{total(u8, &marks)});
}

Key points

  • Generic function: a function that works with more than one type.
  • comptime T: type: a parameter that receives a type at compile time.
  • One body: the same code runs for each type the caller passes.
  • Call site: the caller writes the type first, as in first(i32, &nums).
  • Return type: a generic function can return a value of type T.

In this example, first, largest, and total are each defined once with a comptime type parameter. The first three calls pass i32 and read the numbers 5, 8, and 15 from the slice. The last three calls pass u8 and get 40, 60, and 150 from the marks. No function body was copied to handle the second type.

Note

Zig builds a version of the function for each type used.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

In the call first(i32, &nums), what does the caller pass for the comptime T: type parameter?