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.