Which is considered best practice in zig?

Hi, so I recently got into zig (and systems programming as a whole) about 3 months ago and let’s just say I don’t know enough to know what’s considered best practice.

So let’s say you have a generic data structure… Something like an ArrayList and you want to write a function that performs an operation on two ArrayList, let’s say like an addition operation of sorts that adds the elements of two ArrayList and produces a new one, which implementation would be considered best practice;

fn add(comptime T: type, a: ArrayList(T), b: ArrayList(T)) ArrayList(T) {}

Or

fn(a: anytype, b: anytype) anytype {} where we infer the type using @typeOf and @typeName to determine if they’re compatible to be added

The first one looks kinda redundant to me cause a and b would be of the same type but it looks like what’s consistent with the standard library (at least I’ve seen such pattern in many functions) and the second one looks much more convenient but doesn’t look like what a good zig programmer should do.

I would be more than happy if someone could provide some answers to my question… Thank you

You cannot return anytype from a function, so the second one is helpfully also impossible :slight_smile:

1 Like

But doing it this way is doable. fn(a: anytype, b: anytype) @TypeOf(a, b)

This question still doesn’t have a clear standard answer for me.

Even the most common comptime T: type, x: T parameter list trades redundancy for readability, and for such simple cases, the core members have already indicated that the first practice is preferred over the anytype approach.

But once it’s not just the simple comptime T: type, x: T, things get complicated.

ArrayList just happens to only need one parameter T to construct, so it looks like fn(comptime T: type, x: ArrayList(T), y: ArrayList(T)) ArrayList(T) can stay clean.

Once we’re dealing with more complex generics, like ArrayHashMap,

fn(
    comptime K: type, 
    comptime V: type, 
    comptime Context: type, 
    comptime store_hash: bool, 
    x: ArrayHashMap(K, V, Context, store_hash), 
    y: ArrayHashMap(K, V, Context, store_hash)
) ArrayHashMap(K, V Context, store_hash)

quickly becomes unbearable.

But in this situation, using anytype also makes it hard to do type checking. Later, I tended to do this:

pub fn ArrayHashMapWithOperations(
    comptime K: type,
    comptime V: type,
    comptime Context: type,
    comptime store_hash: bool,
) type {
    return struct {
        pub ArrayHashMap = std.ArrayHashMap(K, V, Context, store_hash);
        pub fn operate(x: ArrayHashMap , y: ArrayHashMap ) ArrayHashMap  {
            ...
        }
    };
}

Simply put, it’s about creating a namespace wrapper around these complex generics, so that type definitions and related operations are accessed within the wrapper’s namespace right from the start.