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.