Exploring comptime
Let's create a sum
function that takes in a slice of numbers (any type of number!!) and returns the sum of all the numbers! How can we do that?
const std = @import("std");
fn sum(comptime T: type, values: []const T) T {
var result: T = 0;
for (values) |value| {
result += value;
}
return result;
}
pub fn main() void {
const some_i32s = [_]i32{ 1, 2, 3, 4, 5 };
std.debug.print("sum of i32s: {}\n", .{sum(i32, &some_i32s)});
const some_f32s = [_]f32{ 1.0, 2.0, 3.0, 4.0, 5.0 };
std.debug.print("sum of f32s: {}\n", .{sum(f32, &some_f32s)});
const some_u64s = [_]u64{ 1, 2, 3, 4, 5 };
std.debug.print("sum of u64s: {}\n", .{sum(u64, &some_u64s)});
}
Woah, what is comptime
? And why is the type of T
like type
itself?
What is comptime?
I'm sure many of us have heard of macros. They exist in languages like C, Rust or even Lisp (in quite a different form), and they serve as a way of executing some code at compile-time instead of runtime.
We might have also heard of generics in Java and Rust, or even templates in C++ (not sure if I'm committing a sin to lump these together), in order to write code that works across all types with certain constraints.
If you're a user of Go, you might've also used go generate
to write repeated code for you.
Well, Zig has a solution that encompasses all three use-cases mentioned above, and that is comptime! What the comptime feature in Zig allows you to do, is simply write Zig code (not any other special language ala C++ template metaprogramming) that is executed at compile-time, instead of runtime!
Pre-computing values
Looking at our earlier example, our sum
function is pure, and can be pre-computed. Let's try to get Zig to precompute the results instead of computing the results at runtime.
const std = @import("std");
fn sum(comptime T: type, values: []const T) T {
var result: T = 0;
for (values) |value| {
result += value;
}
return result;
}
pub fn main() void {
const some_i32s = [_]i32{ 1, 2, 3, 4, 5 };
std.debug.print("sum of i32s: {}\n", .{comptime sum(i32, &some_i32s)});
const some_f32s = [_]f32{ 1.0, 2.0, 3.0, 4.0, 5.0 };
std.debug.print("sum of f32s: {}\n", .{comptime sum(f32, &some_f32s)});
const some_u64s = [_]u64{ 1, 2, 3, 4, 5 };
std.debug.print("sum of u64s: {}\n", .{comptime sum(u64, &some_u64s)});
}
Wow! Do you notice what changed? By simply adding the comptime
keyword in front of our call to sum
, the function was called at compile-time instead of runtime.
Generics
Our sum
function is already a pretty good example of using comptime to create the effect of generics. Let's go one step further and create a data structure that supports generics.
const std = @import("std");
fn Vector2D(comptime T: type) type {
return struct {
x: T,
y: T,
};
}
pub fn main() void {
const vector_of_i32s = Vector2D(i32){ .x = 1, .y = 2 };
const vector_of_f32s = Vector2D(f32){ .x = 1.0, .y = 2.0 };
std.debug.print("vector_of_i32s: {any}\n", .{vector_of_i32s});
std.debug.print("vector_of_f32s: {any}\n", .{vector_of_f32s});
}
Notice how we can return struct
s from functions in Zig! This definitely wouldn't work at runtime, since types don't have a representation at runtime (unless we use runtime reflection). So Zig is actually running the Vector2D
function at comptime here, and treating the resulting structs as types to be constructed.
Remember that this is all Zig code, which means we can go one step further and make the length generic as well!
const std = @import("std");
fn Vector(comptime T: type, comptime len: usize) type {
return struct {
values: [len]T,
};
}
const Vector_i32_2D = Vector(i32, 2);
const Vector_f32_4D = Vector(f32, 4);
pub fn main() void {
const vector_of_2_i32s = Vector_i32_2D{ .values = [_]i32{ 1, 2 } };
const vector_of_4_f32s = Vector_f32_4D{ .values = [_]f32{ 1.0, 2.0, 3.0, 4.0 } };
std.debug.print("vector_of_2_i32s: {any}\n", .{vector_of_2_i32s});
std.debug.print("vector_of_4_f32s: {any}\n", .{vector_of_4_f32s});
}
Here, both parameters to Vector
play a part in defining what kind of struct
the resulting type will be!
Last updated