DEV Community

Cover image for Zig vs Go: generics
Paolo Carraro
Paolo Carraro

Posted on

Zig vs Go: generics

(you can find previous post about same topic here)

Go introduces generics only starting from version 1.18 following numerous requests from the community: from this version, Go allows parameterizing the types of functions and structures in order to avoid code redundancy.
Zig does not provide a syntax for generics but uses one of its peculiar features, namely the generation and/or manipulation of code at compile time (metaprogramming) with the use of the comptime keyword. The advantage is to obtain a binary without overhead for type checking during runtime.

Using generics in functions

Let's see how to define a function that accepts multiple types

// Go
func doubleNumber[T constraints.Integer | constraints.Float](a T) T {
    return a * T(2)
}
...
doubledFloat := doubleNumber[float32](5.3)
fmt.Println(doubledFloat)
// type parameter inferred
doubledInteger := doubleNumber(5)
fmt.Println(doubledInteger)
Enter fullscreen mode Exit fullscreen mode
// Zig 
fn doubleNumber(comptime T: type, a: T) T {
    return a * 2;
}
...
const doubledFloat = doubleNumber(f32, 5.3);
std.debug.print("{}\n", .{doubledFloat});
const doubledInteger = doubleNumber(i32, 5);
std.debug.print("{}\n", .{doubledInteger});
Enter fullscreen mode Exit fullscreen mode

Note that in Go the generic is defined through its constraints which are interfaces that allow the compiler to check if a type does not respect the interface, while Zig evaluates any compilation errors during the compile-time phase when it actually tries to create the function for the type indicated by the caller.

Using generics in structure definitions

In Go, also for structs we can indicate one or more types with the same syntax and rules we use for functions.

// Go
type Stack[T any, Q any] struct {
    Items      []T
    AlterItems []Q
    Index      int
}
...
intStack := Stack[int8, float32]{
    Items:      []int8{1, 2, 3, 4, 5},
    AlterItems: []float32{1.1, 2.1, 3.1, 4.7, 5.3},
    Index:      4,
}
fmt.Println(
  intStack.Items[intStack.Index], 
  intStack.AlterItems[intStack.Index]
)

strStack := Stack[string, bool]{
    Items:      []string{"hello", "gophers"},
    AlterItems: []bool{false, true},
    Index:      0,
}
fmt.Println(
  strStack.Items[strStack.Index], 
  strStack.AlterItems[strStack.Index]
)
Enter fullscreen mode Exit fullscreen mode

In Zig, to obtain a struct with parameterized type fields, use a function that returns type and is therefore resolved at compile time.

// Zig 
fn Stack(comptime T: type, comptime Q: type) type {
    return struct {
        items: []const T,
        alterItems: []const Q,
        index: usize,
    };
}
...
const intArr = [_]i32{ 1, 2, 3, 4, 5 };
const floatArr = [_]f32{ 1.1, 2.1, 3.1, 4.7, 5.3 };
const intStack = Stack(i32, f32){
    .items = intArr[0..],
    .alterItems = floatArr[0..],
    .index = 4,
};
std.debug.print("{d} {d}\n", 
    .{ intStack.items[intStack.index],
       intStack.alterItems[intStack.index]
    });

const strArr = [_][]const u8{ "hello", "ziguanas" };
const boolArr = [_]bool{ false, true };
const strStack = Stack([]const u8, bool){
    .items = &strArr,
    .alterItems = &boolArr,
    .index = 1,
};
std.debug.print("{s} {}\n", 
    .{ strStack.items[strStack.index],
       strStack.alterItems[strStack.index]
    });
Enter fullscreen mode Exit fullscreen mode

Structs as generics

In Go, constraints are interfaces, so in a function that accepts structs as parameters, they must at least implement the methods used within the function block

type IdTrackable interface {
    GetID() string
}

type Article struct {
    ID       string
    Name     string
    Category string
}
func (a Article) GetID() string { return a.ID }

type SKU struct {
    ID        string
    ArticleID string
    Available bool
}
func (s SKU) GetID() string { return s.ID }

func findById[T IdTrackable](items []T, id string) (*T, error) {
    for _, item := range items {
        if item.GetID() == id {
            return &item, nil
        }
    }
    return nil, errors.New("not found")
}
...
articles := []Article{
    {
        ID:       "a1",
        Name:     "Laptop",
        Category: "Electronics",
    },
    {
        ID:       "a2",
        Name:     "Book",
        Category: "Education",
    },
}

article, err := findById(articles, "a1")
if err == nil {
    fmt.Printf("Found article: %+v\n", *article)
}
Enter fullscreen mode Exit fullscreen mode

In Zig, this function with the generic will be resolved and duplicated at compile time as many times as a different type is passed in its call.

// Zig
const Article = struct {
    id: []const u8,
    name: []const u8,
    category: []const u8,
};

const SKU = struct {
    id: []const u8,
    articleId: []const u8,
    available: bool,
};

fn findById(comptime T: type, items: []const T, idx: []const u8) !T {
    for (items) |item| {
        if (std.mem.eql(u8, @field(item, "id"), idx)) {
            return item;
        }
    }
    return error.NotFound;
}
...
const articles = [_]Article{
    .{
        .id = "a1",
        .name = "Laptop",
        .category = "Electronics",
    },
    .{
        .id = "a2",
        .name = "Book",
        .category = "Education",
    },
};

const art = try findById(Article, &articles, "a2");
std.debug.print("Found article: {}\n", .{art});
Enter fullscreen mode Exit fullscreen mode

The use of metaprogramming proposed by Zig, with its own syntax and rules, reveals many other potentials beyond the resolution of generics that are currently not achievable with Go.

Top comments (0)