(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)
// 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});
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]
)
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]
});
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)
}
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});
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)