Addendum (March 2022): There is a new recommended pattern for making interfaces in Zig. Check out this blog post for more information.
Zig doesn't have a formal interface (java, c#) or trait (rust) construct. However, they're very important when a function needs to take one of multiple concrete types which have a certain behavior. There is still a way to do this in zig, and I've built a contrived example to share.
A real case for interfaces
An important concept in Zig is an Allocator
. With Zig opposed to C, you choose your allocator. A few are available for use in the standard library and each use different strategies. This allocator you choose must be sent along to certain functions which need to allocate data. This requires the Allocator
struct to be implemented by concrete allocators.
Example
I just attempted to create a list of structs each which have different concrete types. Then I looped through each item in the list and called a function to see the code get executed in the concrete struct.
A contrived case for interfaces
My interface or base struct is Dog
. It will have one method: bark(volume: u32) void
. This will be implemented by concrete dogs (breeds), particularly Beagle
and Retriever
. We'll then construct a [_]Dog
array and call bark(123)
on each item within it.
The interface (actually a struct)
const Dog = struct {
// Fields
barkFn: fn (self: *const Dog, volume: u32) void,
// Methods
pub fn bark(self: *const Dog, volume: u32) void {
self.barkFn(self, volume);
}
};
There seems to be two very similar functions: a bark
and a barkFn
. The important distinction is that bark
is a function on the Dog struct itself. As in if you have an instance of a Dog
you can call myDog.bark(123);
. barkFn
however is a field which can be set. We'll do that in our concrete structs
Concrete structs
const Retriever = struct {
// Fields
dog: Dog = Dog{ .barkFn = bark },
// Methods
fn bark(dog: *const Dog, volume: u32) void {
std.log.info("Rewf! Rewf!\n", .{});
}
};
The Retriever
struct has one field and one function as well. The field is a Dog
struct (aka the interface) which has a default value wherein Dog
's barkFn
field is assigned to Retriever
's bark
function. A bit of a tongue twister, but it makes sense. Then Retriever
's bark
function is the actual concrete implementation where we get to hear a retriever.
We'll make the beagle implementor a bit more complicated by adding a field which is used in its bark
function. I'll add age
where a younger beagle will bark a bit differently than an older beagle.
const Beagle = struct {
// Fields
dog: Dog = Dog{ .barkFn = bark },
age: u8,
// Methods
fn bark(dog: *const Dog, volume: u32) void {
const self = @fieldParentPtr(Beagle, "dog", dog);
if (self.age < 2) {
std.log.info("Whimper!\n", .{});
} else {
std.log.info("Bark! Barroooo!\n", .{});
}
}
};
The magic here, as far as I'm concerned, is the @fieldParentPtr
built in function. It allows us to move from a Dog
struct to its Beagle
owner. Doing that, we now have access to the age
field on Beagle
.
Using our dogs
const std = @import("std");
pub fn main() void {
const dogs = [_]Dog{ (Beagle{ .age = 1 }).dog, (Retriever{}).dog };
for (dogs) |dog| {
dog.bark(123);
}
}
This simply creates an array of dogs, and calls the generic method bark(123)
on each one.
Conclusion
I'm very new to the language and this was my first time constructing code like this. I felt like I was doing some mental gymnastics at points, but after working through it once it makes sense. I mostly like the result other than a couple parts.
I don't really like having to get the "interface" out of the concrete instantiated struct: (Beagle{ .age = 1 }).dog
. Also creating both a bark
and barkFn
in Dog
is a bit annoying. I understand that they could have different names, or that the bark
function could call 0 or many field-functions. Still, just a bit more verbose than what I'm used to.
It's neat to be able to do it all with just plain structs and references to them though. This post did not talk on more complicated things like generics or multiple inheritance. Here's a blog post that does go over creating a generic Iterator interface.