Zig Interfaces for the Uninitiated

Zig's standard library takes a peculiar approach to generic interfaces. Since Zig doesn't have language features like inheritance or Go's interfaces, it uses a mechanism that mostly exists in C. However, like most things in C, it's better in Zig.

It may be that some day in the near future, Issue 130 will be resolved and this whole document will be obsolete. In June of 2020, Zig is a new and fast-moving language. Don't be surprised if it changes. Until that day, however, the thing this tutorial describes is what we have.

A Useful Example

Zig doesn't currently have an equivalent to Python's itertools, but it does have a common pattern for iterators:


while (iter.next()) |val| {
    // Do something with val
}
      

Several data structures in the standard library follow a similar pattern, so let's pull it into a formal Iterator struct that we can operate on.


/// Iterators can be iterated with while loops. The next
/// function returns null when the iterator is exhausted.
pub fn Iterator(comptime T: type) type {
    return struct {
        const Self = @This();
        nextFn: fn (self: *Self) ?T,
        resetFn: fn (self: *Self) void,
        pub fn next(self: *Self) ?T {
            return self.nextFn(self);
        }
        pub fn reset(self: *Self) void {
            return self.resetFn(self);
        }
    };
}
      

That's a generic iterator that yields a series of Ts as its next method is called. I included a reset too, just in case someone wants to start over. So the interface is two functions, next and reset, that need to be implemented with a nextFn and a resetFn.

The next step is to make something that implements Iterator.

Implementing an Interface

Let's pick a super-simple use case for our first iterator. Something finite and familiar. Range is a common iterator that counts up to a value by some interval. It should do nicely.

Basic Range

The first thing to do is figure out what our Range's interface will be. It needs a start value, an end value, and a step value. It also needs to know what type all these values are going to be. And, of course, it needs the functions used by Iterator: next and reset. Let's put the iterated-over type in an argument to the Range type itself, and initialize the rest at runtime.


pub fn Range(comptime T: type) type {
    return struct {
        const Self = @This();

        start: T,
        step: T,
        end: T,
        next_val: T,

        pub fn init(start: T, end: T, step: T) Self {
            return Self{
                .next_val = start,
                .start = start,
                .end = end,
                .step = step,
            };
        }

        pub fn next(self: *Self) ?T {
            const rv = self.next_val;
            if (self.step < 0) {
                if (rv <= self.end) {
                    // Done counting down
                    return null;
                }
            } else {
                if (rv >= self.end) {
                    // Done counting up
                    return null;
                }
            }
            self.next_val += self.step;
            return rv;
        }

        pub fn reset(self: *Self) void {
            self.next_val = self.start;
        }
    };
}
      

This struct has all the parts we need: next and reset, as well as a way to initialize it. However, note that both these methods take an instance of Range(T) (that's what @This() means) but our Iterator interface functions nextFn and resetFn need pointers to instances of Iterator. We have to tell Zig what a Range Iterator looks like.

Iterating

In order to use Range as an Iterator, we have to instantiate an Iterator with the particulars of Range. That means adding and initializing a property.


pub fn init(start: T, end: T, step: T) Self {
    return Self{
        .iterator = Iterator(T){ .nextFn = next, .resetFn = reset },
        .next_val = start,
        .start = start,
        .end = end,
        .step = step,
    };
}
      

That new version of init will initialize our new property, but there's a problem. next and reset accept Range pointers, not Iterator pointers.

@fieldParentPtr

Of course, for a Range, we'll need to pass an Iterator to next. But how will that Iterator get at the Range's state like next_val and step? Iterator can't know about them, since that would make it a pretty lousy generic interface. Zig has a built-in function for just this purpose: @fieldParentPtr. Given a pointer to a struct field, along with that field's name and the struct's type, it will return a pointer to the struct. Let's take a look at the new next method that uses it.


pub fn next(iterator: *Iterator(T)) ?T {
        const self = @fieldParentPtr(Self, "iterator", iterator);
        const rv = self.next_val;
        if (self.step < 0) {
            if (rv <= self.end) {
                // Done counting down
                return null;
            }
        } else {
            if (rv >= self.end) {
                // Done counting up
                return null;
            }
        }
         self.next_val += self.step;
        return rv;
}
      

Not much has changed. We moved self moved from the function arguments to the first line of the body and replaced the argument an iterator. Everything else is exactly the same.

Completed Range

Let's tie the whole thing together along with some tests and documentation.


/// Generic iterator interface. Call next to get the next element or
/// null if the iterator's exhausted. Call reset to start iteration
/// over from the beginning.
pub fn Iterator(comptime T: type) type {
    return struct {
        const Self = @This();
        nextFn: fn (self: *Self) ?T,
        resetFn: fn (self: *Self) void,
        pub fn next(self: *Self) ?T {
            return self.nextFn(self);
        }
        pub fn reset(self: *Self) void {
            return self.resetFn(self);
        }
    };
}

/// Half-open range type. Call next() on its iterator to get values
/// out. Example usage:
///
/// var range = try Range(u32).init(0, 10, 1);
/// var iter = &range.iterator;
/// while (iter.next()) |n| {
///     std.debug.warn("{}\n", .{n});
/// }
pub fn Range(comptime T: type) type {
    return struct {
        iterator: Iterator(T),
        next_val: T,
        start: T,
        step: T,
        end: T,
        const Self = @This();

        /// Return the next element in the range, or null if the range
        /// has been exhausted.
        pub fn next(iterator: *Iterator(T)) ?T {
            const self = @fieldParentPtr(Self, "iterator", iterator);
            const rv = self.next_val;
            if (self.step < 0) {
                if (rv <= self.end) {
                    return null;
                }
            } else {
                if (rv >= self.end) {
                    return null;
                }
            }

            self.next_val += self.step;
            return rv;
        }

        /// Reset the range back to its start.
        pub fn reset(iterator: *Iterator(T)) void {
            const self = @fieldParentPtr(Self, "iterator", iterator);
            self.next_val = self.start;
        }

        /// Initialize. Returns error if step size is invalid.
        pub fn init(start: T, end: T, step: T) !Self {
            if (step == 0) {
                return error.ZeroStepSize;
            }
            return Self{
                .next_val = start,
                .start = start,
                .end = end,
                .step = step,
                .iterator = Iterator(T){
                    .nextFn = next,
                    .resetFn = reset,
                },
            };
        }
    };
}

const std = @import("std");

const testing = std.testing;
test "range ascend" {
    var range = try Range(u32).init(0, 10, 1);
    var iter = &range.iterator;
    var correct: u32 = 0;
    while (iter.next()) |n| {
        testing.expectEqual(correct, n);
        correct += 1;
    }
    testing.expectEqual(correct, 10);
    testing.expectEqual(iter.next(), null);
}

test "range descend" {
    var range = try Range(i32).init(10, 0, -1);
    var iter = &range.iterator;
    var correct: i32 = 10;
    while (iter.next()) |n| {
        testing.expectEqual(correct, n);
        correct -= 1;
    }
    testing.expectEqual(correct, 0);
    testing.expectEqual(iter.next(), null);
}

test "range skip" {
    var range = try Range(u32).init(0, 10, 2);
    var iter = &range.iterator;
    var correct: u32 = 0;
    while (iter.next()) |n| {
        testing.expectEqual(correct, n);
        correct += 2;
    }
    testing.expectEqual(correct, 10);
    testing.expectEqual(iter.next(), null);
}

test "range runtime" {
    var start: u32 = 0;
    while (start < 10) : (start += 1) {
        var range = try Range(u32).init(start, 10, 1);
        var iter = &range.iterator;
        var correct: u32 = start;
        while (iter.next()) |n| {
            testing.expectEqual(correct, n);
            correct += 1;
        }
        testing.expectEqual(correct, 10);
        testing.expectEqual(iter.next(), null);
    }
}
      

And test it...


$ zig test iterators.zig 
All 4 tests passed.
      

There's an important thing to note about the usage. When it assigns iter to range.iterator it takes the address. If instead it just assigned var iter = range.iterator; the code would compile fine. However, there would be weird bugs. It would look like it's iterating over random memory. It'd look like that because it would be iterating over random memory.

var iter = range.iterator; makes a copy of the iterator field in the range struct. So when we go to call @fieldParentPtr on it, we'll be telling Zig to give us the instance of Range(u32) a few bytes before the address of our copy. But the copy isn't inside a Range(u32)! So instead, we use var iter = &range.iterator; to get a copy of the address of the iterator, instead of a copy of the iterator itself. The address is nicely inside our Range(32) so everything works pleasantly.

Fold

Now that we have a generic iterator, let's do something with it. fold (sometimes called reduce) is a function that takes an iterator, a function, and an initial value and returns a value. We could implement fold for arbitrary slices in Zig, but what if we want to fold a more complex data structure like a HashMap? Well, for that we'd have to write a custom implementation. Or, instead, we could make a generic Iterator out of the HashMap and pass that to our generic fold function.


/// Apply fun to successive elements in iter. Initial value of acc is
/// init.
pub fn fold(
    comptime T: type,
    fun: fn (acc: T, val: T) T,
    iter: *Iterator(T),
    init: T,
) T {
    var accumulator: T = init;
    while (iter.next()) |val| {
        accumulator = fun(accumulator, val);
    }
    return accumulator;
}
      

Since we already have a data structure that implements Iterator (i.e. Range) we'll use that to test our fold implementation.


fn add(a: u32, b: u32) u32 {
    return a + b;
}

test "fold over range" {
    var range = try Range(u32).init(1, 10, 1);
    var iter = &range.iterator;
    const total = fold(u32, add, iter, 17);
    testing.expectEqual(total, 17 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9);
}
      

It's not a traditional functional implementation, but Zig isn't a traditional functional language. A future version of the Zig compiler will be able to guarantee tail call optimization, but until then this will do.

Finally...

While exploring iterators, what we really learned was how idiomatic Zig uses runtime polymorphism to have interchangeable data structures. Notable examples in the standard library include std.mem.Allocator and std.rand.Random. Hopefully this has given you a deeper understanding of how these interfaces are used and how to implement data structures that match them.

A somewhat more complete version of the iterators library is available on sourcehut.