For pathes I wonder whether you would need some more complicated mixture of std.fs.path.join (for the path) and allocPrint (for the filename) to make the code work on windows? (But I haven’t dealt with windows in a while)
I am assuming that this isn’t specifically about filenames with placeholders?
1. Helper on the Stack
I think if I was in that situation my first approach would be to play around with and consider a helper data structure on the stack, like this:
const helper:Helper = .{ .allocator = main.stackAllocator };
const path1 = helper.print("assets/cubyz/blocks/textures/{}.png", .{i});
const path2 = helper.print("assets/cubyz/blocks/textures/{}.png", .{i});
I think that has the benefit that the helper can have arbitrary relatively low effort methods that are tailored to making some specific code look less cluttered.
But if the uses of print are single calls scattered throughout different functions, than declaring that helper may not actually help with reducing clutter. So basically this is only helpful if there are many calls within specific functions.
2. Helper built into some common Data Structure
Another approach may be to try and group the calls that are currently scattered through out many different functions and instead collect them in a helper, for example if you have many different kinds of pathes that get generated, you could create a thing in your main
structure that handles all the different path kinds, I imagine something along the lines of:
const path1 = main.path(.block_texture, i) catch unreachable;
// or
const path1 = try main.path(.block_texture, i);
And for the path
function I would imagine that it would be defined something like this:
const Pathes = struct {
const Kind = enum { block_texture, icon, background, character_portait, ... };
// somewhere has a mapping from Kind to path prefix
// either allocates the pathes every time or maybe caches them in a hashmap
pub fn get(self:*Pathes, allocator:std.mem.Allocator, comptime kind:Kind, arg:anytype) ![]const u8 {
// ...
}
};
const Main = struct {
// other fields and functions
pathes: Pathes,
pub fn path(self:*Main, comptime kind:Pathes.Kind, arg: anytype) ![]const u8 {
return self.pathes.get(self.stackAllocator, kind, arg);
}
}
Depending on what main is and contains, you also could avoid the extra path function and just use main.pathes.get
directly.
I think only when I didn’t find a way, to somehow put a helper data structure (that basically groups the part that is messy into one implementation that can handle all the cases (either because there is only one kind, or by separating between different kinds)), then I would consider adding such a method to the allocator interface.
I think it can be helpful to have one specific part of the application, that handles filenames. Basically I don’t want the knowledge about what filename prefixes are used scattered through the codebase, but instead have one file that maps all the different kinds of prefixes (either statically or at runtime) to their actual values and then only have the shorter kind values scattered through the code base.
If this grouping works, it also allows you to make the decision for what allocator to use etc. at the same time, either for specific groups or all groups, thus eliminating the need to specify these things multiple times, for every call.