I want to have a “system” that can contain a closed set of types of objects, my idea here is, that all the types of objects this “system” can hold must meet some requirements for them to be useful-for instance a “GameUpdate” system that can hold only objects which have an “update” function definition, so i came up with this:
const Dog = struct{
kind: DogKind,
pub fn speak(self: Dog) void{
std.log.info("woof woof i am {s}", @tagName(self.kind));
}
};
const DogKind = enum{
Dachshund,
Corgi,
Alaskan,
Bulldog,
};
const Cat = struct{
is_garfield: bool = false,
pub fn speak(self: Cat) void{
if (self.is_garfield){
std.log.info("give me lasagna");
return;
}
std.log.info("meow i am cat");
}
const AnimalUpdate = struct{
//the closed set of types i was referring to
const available_types = [_]type{Dog, Cat};
//here the pointers to ArrayLists of each of the above types are stored
storages: [AnimalUpdate.available_types.len]*anyopaque,
pub fn init(allocator: std.mem.Allocator) AnimalUpdate{
var animal_update: AnimalUpdate = undefined;
for (0..available_types.len) |type_index|{
const animals = try allocator.create(std.ArrayList(available_types[type_index]));
animals.* = .empty;
animal_update[type_index] = animals;
}
}
pub fn get(self: AnimalUpdate, comptime T: type) *std.ArrayList(T){
switch(T){
Dog => return @alignCast(@ptrCast(self.storages[0])),
Cat => return @alignCast(@ptrCast(self.storage[1])),
else => @compileError("unsupported type"),
}
}
pub fn update(self: AnimalUpdate) void{
for (0..available_types.len) |type_index|{
const animals = self.get(available_types[type_index]);
for (animals.items) |animal|{
animal.speak();
}
}
}
pub fn deinit(self: AnimalUpdate, allocator: std.mem.Allocator) !void{
for(0..available_types.len) |type_index|{
const animals = self.get(available_types[type_index]);
animals.deinit(allocator);
}
}
};
when trying to compile the code i get an error saying that the indexes i am trying to use in “update” “deinit” and “init” are runtime, while the “available_types” array is comptime, i also cant just use the index from the loop because then i could only get a “*anyopaque” so no type information which is something i need. i know i could just use a switch similar to one in the “get” function but use a number instead of type, but i would like to not use such a switch either way, because it would be horrible to work with it if i had many types, which is likely.
There are multiple ways to handle this. What the best way is depends on your exact use case. Since you seem to want a separate ArrayList per type, one way is to store those arraylists as fields in the AnimalUpdate struct and use inline for loops to iterate over them:
const AnimalUpdate = struct {
const available_types = [_]type{Dog, Cat};
dogs: std.ArrayList(Dog),
cats: std.ArrayList(Cat),
pub fn init() AnimalUpdate {
var animal_update: AnimalUpdate = undefined;
inline for (available_types) |T|{
animal_update.get(T).* = .empty;
}
return animal_update;
}
pub fn get(self: *AnimalUpdate, comptime T: type) *std.ArrayList(T){
const struct_info = @typeInfo(AnimalUpdate).@"struct";
inline for (struct_info.fields) |field| {
if (std.ArrayList(T) == field.type) {
return &@field(self, field.name);
}
}
@compileError("Unsupported type");
}
pub fn update(self: *AnimalUpdate) void {
inline for (available_types) |T| {
const animals = self.get(T);
for (animals.items) |animal| {
animal.speak();
}
}
}
pub fn deinit(self: *AnimalUpdate, allocator: std.mem.Allocator) !void {
inline for (available_types) |T| {
const animals = self.get(T);
animals.deinit(allocator);
}
}
};
Other ways that are also possible:
- Define an Animal struct as tagged union of the possible types. Store 1 list that contains these animals.
- Use a runtime interface that has a type erased pointer (
*anyopaque) and a function pointer to the speak or update function of that type. This is useful if the set of types in not known.
Thanks for help, but this isnt really an ideal solution, the two types are here just for an example, im probably going to use this for possibly tens of different types, so putting the ArrayList of each type as a separate field isnt a great solution. I also dont want to store any runtime information about types. so thanks for the reply but this isnt an ideal solution for my problem
I see. We can use some comptime code to generate a tuple of ArrayLists based on available_types.
const AnimalUpdate = struct {
const available_types = [_]type{Dog, Cat};
const Storage = blk: {
// Convert to [_]type{std.ArrayList(Dog), std.ArrayList(Cat)}
var array_list_types: [available_types.len]type = undefined;
for (&array_list_types, available_types) |*array_list_type, T| {
array_list_type.* = std.ArrayList(T);
}
break :blk @Tuple(&array_list_types);
};
storages: Storage,
pub fn init() AnimalUpdate {
var animal_update: AnimalUpdate = undefined;
inline for (available_types) |T|{
animal_update.get(T).* = .empty;
}
return animal_update;
}
pub fn get(self: *AnimalUpdate, comptime T: type) *std.ArrayList(T){
inline for (available_types, 0..) |animal_type, index| {
if (T == animal_type) {
return &self.storages[index];
}
}
@compileError("Unsupported type");
}
pub fn update(self: *AnimalUpdate) void {
inline for (available_types) |T| {
const animals = self.get(T);
for (animals.items) |animal| {
animal.speak();
}
}
}
pub fn deinit(self: *AnimalUpdate, allocator: std.mem.Allocator) !void {
inline for (available_types) |T| {
const animals = self.get(T);
animals.deinit(allocator);
}
}
};
Do you mean you don’t want to use a runtime interface with a vtable (like how std.mem.Allocator works for example) or are you referring to some issue with my solution (if so, could you clarify where in the code runtime information about types is stored)?
yes i was talking about using runtime interface. I copied your code and written a main function just to test it, when i try to compile the program i get an error complaining about storages field having no value. sorry if its something easy to fix but i havent touched tuples in zig at all, so correct me if im wrong, are tuples just structs without field names, indexed with numbers, like arrays? code:
const std = @import("std");
pub fn main(init: std.process.Init) !void{
const dog: Dog = .{.kind = .Dachshund};
const animal_update: AnimalUpdate = .{};
defer animal_update.deinit(init.gpa) catch {};
try animal_update.get(Dog).append(init.gpa, dog);
animal_update.update();
}
const Dog = struct{
kind: DogKind,
pub fn speak(self: Dog) void{
std.log.info("woof woof i am {s}", @tagName(self.kind));
}
};
const DogKind = enum{
Dachshund,
Corgi,
Alaskan,
Bulldog,
};
const Cat = struct{
is_garfield: bool = false,
pub fn speak(self: Cat) void{
if (self.is_garfield){
std.log.info("GIVE ME LASAGNA");
return;
}
std.log.info("meow i am cat");
}
};
const AnimalUpdate = struct {
const available_types = [_]type{Dog, Cat};
const Storage = blk: {
// Convert to [_]type{std.ArrayList(Dog), std.ArrayList(Cat)}
var array_list_types: [available_types.len]type = undefined;
for (&array_list_types, available_types) |*array_list_type, T| {
array_list_type.* = std.ArrayList(T);
}
break :blk @Tuple(&array_list_types);
};
storages: Storage,
pub fn init() AnimalUpdate {
var animal_update: AnimalUpdate = undefined;
inline for (available_types) |T|{
animal_update.get(T).* = .empty;
}
return animal_update;
}
pub fn get(self: *AnimalUpdate, comptime T: type) *std.ArrayList(T){
inline for (available_types, 0..) |animal_type, index| {
if (T == animal_type) {
return &self.storages[index];
}
}
@compileError("Unsupported type");
}
pub fn update(self: *AnimalUpdate) void {
inline for (available_types) |T| {
const animals = self.get(T);
for (animals.items) |animal| {
animal.speak();
}
}
}
pub fn deinit(self: *AnimalUpdate, allocator: std.mem.Allocator) !void {
inline for (available_types) |T| {
const animals = self.get(T);
animals.deinit(allocator);
}
}
};
On line 6:
You are using struct initialization. For that, you have to specify all field. But instead of that, it has a init function that you should use:
const animal_update: AnimalUpdate = .init();
Some other things:
animal_update should be var because the update function wants a mutable pointer. (It is not really needed in this case, but I assume the update function will modify the objects)
std.log.info has a second parameter that needs a list of arguments. If the formatting string had no arguments, pass .{}.