Generics in Zig and Rust

(730 words)

I was talking the other day about creating a SparseArray data type in Zig, using a function that takes two types and returns a type. This is elegantly simple to view and understand. I said at the end of that post that new languages like Zig don’t have to follow the legacy approach that C++ does with < and > templates. I was going to include Rust in that set of modern languages until I remembered that Rust does the whole <> thing.

// Rust code
struct Point<T> {
    x: T,
    y: T,
}

Whilst in Zig, that would be:

// Zig code
fn Point (T: type) type {
    return struct {
        x: T,
        y: T,
    };
}

Now, you could argue that the Zig approach has more symbols and is more complex. You could also argue that the Rust code requires more inference. How you would you use these.

// Rust
fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}
// Zig
pub fn main () {
    const integer = Point(i32) { .x = 5, .y = 10 };
    const float = Point(f32) { .x = 1, .y = 4 };

The rust code just creates points. Because the types of the member variables determines the actual type used, the type of the Point defined does not have to be explicit. This will lead to bugs where you expected a floating point based Point but got an integer version instead.

In Zig, everything is explicit. We create two different types, Point(i32) and Point(f32) and then use this to create two explicitly typed variables. There can be no mistake here. Oh, have you also noticed that we don’t need to add .0 at the end of the initialisation values? No need, as a comptime_int will automatically be promoted to an f32, because that type is already explicitly defined as the type of the x field of this structure.

Of course, this can get worse for Rust.

// Rust
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

Oh no! Two < > brackets around types. Just to define a function to get access to a field of this structure. Ignoring the ‘can’t you just trust software engineers to access members` problem at this point, this starts to get confusing to me.

// Zig
fn Point (T: type) type {
    return struct {
        x: T,
        y: T,

        pub fn get_x (self: @This()) T {
            return self.x;
        }
    };
}

I’ve included the whole structure definition here as well, mainly because you have to define member functions inside the structure definition, rather than in some other file that is someplace else in your source tree. No <> anywhere in sight. No short term memory overload problems as we try to remember what fields this templated structure has.

I’m a simple person, with a small short term memory. Don’t try to be clever. Just give me a way to describe member functions on structures that I define a compile time.

Oh, and why does self not have a type in Rust? Oh, that’s because we have to define another keyword to denote that. In Zig, we can call that variable anything we want, it just has to be the first parameter to the function. Prefer this over self, yeah, just use this. What about item or container or entity? Yeap, all possible. Naming things shouldn’t be imposed by the compiler.

I’m not having a go at Rust. Rust made design decisions that I’m sure looked right at the time they were made. I have no problems with those decisions. All I’m really saying is that I prefer the concept that types are variable at compile time, can be passed to functions and returned from functions, and that with just that simple mechanism you can create some really neat data types. For example, the standard library of Zig has an StaticBitSet type and when you say you only need to store a small number of items will degenerate to using an integer as its backing store rather than a complex array.

// Zig standard library source code
pub fn StaticBitSet(comptime size: usize) type {
    if (size <= @bitSizeOf(usize)) {
        return IntegerBitSet(size);
    } else {
        return ArrayBitSet(usize, size);
    }
}

This is the power of compile time types.

Zig