I’ve been using this pattern for awhile now, and it’s really nice. I’ve also found value in treating indices as first class values that are just as important as the types they point to.
For example, instead of doing this:
const Container = struct {
foos: []const Foo,
bars: []const Bar,
const Foo = struct {
abc: u32,
def: u32,
const Index = enum(u32) { _ };
};
const Bar = struct {
ghi: u32,
jkl: u32,
const Index = enum(u32) { _ };
};
pub fn getFoo(container: *const Container, idx: Foo.Index) Foo {
return container.foos[@backingInt(idx)];
}
pub fn getBar(container: *const Container, idx: Bar.Index) Bar {
return container.bars[@backingInt(idx)];
}
};
I find that it is better to do this:
const Container = struct {
foos: []const Foo,
bars: []const Bar,
const Foo = struct {
abc: u32,
def: u32,
const Index = enum(u32) {
_,
pub fn get(idx: @This(), container: *const Container) Foo {
return container.foos[@backingInt(idx)];
}
};
};
const Bar = struct {
ghi: u32,
jkl: u32,
const Index = enum(u32) {
_,
pub fn get(idx: @This(), container: *const Container) Bar {
return container.bars[@backingInt(idx)];
}
};
};
};
This provides the following ergonomics:
Container’s namespace is smaller. Less things to look at and think about when dealing with that typefoo_idx.get(container)is nicer to type thancontainer.getFoo(foo_idx). In terms of (english) grammar, in the first example,foo_idxis the subject andcontaineris the object of the preposition: “foo_idx gets Foo from container”. I have found that by treatingfoo_idxas the subject, my code naturally leans toward using indices instead of pointers- You can easily add new methods onto the index type that you wouldn’t really be able to do otherwise. For example, if
containerstored a MultiArrayList ofFoo, then you could have individual getters for each of the properties:
const Container = struct {
foos: std.MultiArrayList(Foo),
const Foo = struct {
abc: Abc,
def: Def,
// Foo is some sort of intrusive linked list
next: Foo.Index,
const Index = enum(u32) {
none = std.math.maxInt(u32),
_,
pub fn abc(idx: @This(), container: *const Container) Abc {
return container.foos.items(.abc)[@backingInt(idx)];
}
pub fn def(idx: @This(), container: *const Container) Def {
return container.foos.items(.def)[@backingInt(idx)];
}
pub fn ghi(idx: @This(), container: *const Container) Ghi {
return container.foos.items(.ghi)[@backingInt(idx)];
}
pub fn next(idx: @This(), container: *const Container) Foo.Index {
return container.foos.items(.next)[@backingInt(idx)];
}
};
};
};
This makes using indexes even more ergonomic, since you can do
pub fn something(container: *Container, foo: Foo.Index) void {
var curr = foo;
while (curr != .none) : (curr = curr.next(container)) {
const abc = curr.abc(container);
const def = curr.def(container);
abc.foo(def); // do stuff
}
}
Here are some examples of code I’ve written that uses this approach:
- zirp’s node and token index types
- Virtual file system implementation I wrote, this makes use of the intrusive linked list thing I mentioned. I wrote this code awhile ago, though; If I rewrote it today I would definitely have getters on the
Indextype for all of the fields onEntry!
(sorry no cursed code here, but this has been on my mind for awhile and now seemed like a good time to talk about it lol. Maybe I’ll write a blog post about this eventually)