What is anytype?
Zig enables users to parameterize functions with the use of the keyword anytype
. This keyword, like its name suggests, allows the user to pass any type as a parameter that will then be deduced at comptime.
Using anytype
We’ll begin by making a function that can take anytype
as a parameter.
pub fn foo(arg: anytype) void {
// implementation details
}
// later ...
const bar: usize = 42;
foo(bar); // arg gets deduced as usize
const baz: isize = 42;
foo(baz); // arg gets deduced as isize
Due to type deduction, there will now be two separate versions of foo
that get created - one for arg: usize
and arg: isize
.
The keyword anytype
can also be used in interesting ways with member functions:
const MyType = struct {
pub fn foo(_: anytype) void {
std.debug.print("\nCalled Foo\n", .{});
}
};
pub fn main() !void {
// foo's first argument is deduced as opaque.
// We're calling it directly against the class.
MyType.foo(opaque{});
// the instance x of MyType calls foo as a member
// function. This is equivalent to calling foo(&x).
// anytype is deduced as `*const @This()`
const x: MyType = .{};
x.foo();
}
As we can see, anytype
is a very powerful utility that acts as a catch-all for types provided to it.
Considerations when using anytype
Like most powerful things, it can be abused. Heavy reliance on anytype
can make function declarations difficult to read, requiring the user to dig into function implementations to understand how a type is used. This is often referred to as duck typing
. According to Wikipedia:
In computer programming, duck typing is an application of the
duck test—"If it walks like a duck and it quacks like a duck, then
it must be a duck"—to determine whether an object can be used
for a particular purpose.
So what does this teach us about anytype
? To answer this, we’ll summon the duck. Imagine you have a function that tries to save an object at some point. It accomplishes this by calling a save
function on the object instance:
fn doStuffAndSave(obj: anytype) void {
// do some stuff... important stuff probably...
obj.save(); // a wild duck appears!
}
Here we can see that doStuffAndSave
assumes that obj
has a save
function. Fundamentally, doStuffAndSave
treats obj
like it’s a saveable object. Like most things, this has a cost and a benefit. The benefit is it allows us to quickly create types that need little to no introduction to be used. This comes at a cost of readability.
However, this does not imply that anytype
should not be used - rather, it should be used strategically.
Alternatives to anytype
Partial Specialization
To understand our options better, let’s look at the declaration of the eql
function in the standard library:
pub fn eql(comptime T: type, a: []const T, b: []const T) bool
Here, we can see that equal takes a parameter T
that denotes a type. Then, T
is propogated to the arguments a
and b
. Suppose we pass u8
as the first argument. This implies:
T -> u8
a: []const T -> a: []const u8
b: []const T -> b: []const u8
This clarifies the fact that eql
expects two slices of some type T
. In the example of eql
, it is relatively painless to specify what T
is and since it always expects slices, there’s no need to specify that a
or b
could be any type.
Interface Types
Let’s build an interface type that can act as an intermediary for our savable object using function pointers and *anyopaque
.
const SaveInterface = struct {
// pointer to the saveable object
obj_ptr: *anyopaque,
// pointer to the object's save function
func_ptr: *const fn (ptr: *anyopaque) void,
// member function that calls the func_ptr on the obj_ptr
pub fn save(self: SaveInterface) void {
self.func_ptr(self.obj_ptr);
}
};
const MyObject = struct {
// probably a lot of data members...
// ...
// our save function takes in an anyopaque pointer
// and casts it back to our MyObject type
pub fn saveMyObject(ptr: *anyopaque) void {
const self: *MyObject = @ptrCast(@alignCast(ptr));
// implementation of our save function...
}
pub fn saveable(self: *MyObject) SaveInterface {
return SaveInterface{
// our self pointer
.obj_ptr = self,
// pointer to our save function
.func_ptr = saveMyObject,
};
}
};
Now, let’s modify our doStuffAndSave
function to take in a SaveInterface
:
pub fn doStuffAndSave(obj: SaveInterface) void {
// really important stuff... I swear...
obj.save();
}
And now it can be used like this:
var obj: MyObject = .{};
doStuffAndSave(obj.saveable());
This pattern is quite common in Zig - in fact, this technique is used in the Allocator
interface.
Best practices with anytype
The examples provided above give us alternatives to anytype
, but are they strictly better? As all things go, everything has its tradeoffs. We can see the boilerplate that anytype
saves us from having to write. At the same time, what might be called boilerplate by some can also be called specificity by others. Here’s a few tips to use anytype
wisely:
-
Always use good variable names. Our example of
obj
was meant to demonstrate how much information can get lost when going off of type deduction alone. -
Prefer to use
anytype
where functions can be assessed quickly and the type requirements are not hard to find. Avoid making long chains ofanytype
that requires one to dig through many layers to assess what kind of duck we’re dealing with. -
Consider the alternatives. For a single type, an interface can be annoying but it can scale well and reduce the amount of comptime deduction that’s necessary. Likewise, if your function genuinely expects types of a specific character (like
eql
), then consider partial specialization instead.