Closer! First, good on you for sticking it out. Thinking about design, even for simple projects, will help you grow as a developer in this language.
Thereās one very simple change I would recommendā¦ in your main function, you create your Cowsay
object on this line:
line 27: var cow = Cowsay{ .w = stdout.any() };
Right there is where you should pass the allocator in. Preferably, it should look something like so:
// make your allocator in main.zig
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
// pass the allocator to Cowsay
var cow = Cowsay{ .w = stdout.any(), .allocator = gpa.allocator() };
Okay, so why am I making a big deal out of this? Think about our options here:
- Itās part of the module, but itās public
- Itās part of the module, and it is not public
- Itās handed to the module from outside
Letās go through our options and think about the costs.
For option 1, Think about it like a security model. Each user should have the minimum amount of privileges required. If you make it public, that means that other modules that import it could use that allocator too (why not, itās public, after all). You have to answer the question āwho owns this allocator?ā and that becomes more important as you explore different allocator options.
On option 2, you are controlling the allocator from the implementation. For simple projects, this is hard to debate and is kind of pedantic. Once you get to anything more complex, it becomes a problem. What if I wanted to pass a logging allocator to the implementation to see what what allocations are actually made? I canāt because that decision was made for me.
For option 3, you get what you needed in one package - the user chooses and itās clear who owns the allocatorā¦ in this case, main
owns the allocator and itās main
ās job to call deinit
to cleanup the resources. Itās clear, simple, and allows the user to have options.
Should you ever make choices about an allocator? Sure, it happens in the standard library actually in the json
module. They use an arena allocator depending on the circumstance because theyāll use that to clear out the memory when theyāre done. That said, itās still wrapping an allocator that the user provides. You can see that hereā¦
pub fn parseFromTokenSource(
comptime T: type,
allocator: Allocator,
scanner_or_reader: anytype,
options: ParseOptions,
) ParseError(@TypeOf(scanner_or_reader.*))!Parsed(T) {
var parsed = Parsed(T){
.arena = try allocator.create(ArenaAllocator),
.value = undefined,
};
errdefer allocator.destroy(parsed.arena);
parsed.arena.* = ArenaAllocator.init(allocator);
errdefer parsed.arena.deinit();
parsed.value = try parseFromTokenSourceLeaky(T, parsed.arena.allocator(), scanner_or_reader, options);
return parsed;
}
Iāll also add that it makes it explicit that āthis thing allocatesā. If you donāt do that, it allocates in the background silently. Again, for simple projects, you wonāt lose sleep over it. For anything more advanced, that kind of thing can really matter.
The point being, allocation is a touchy subject (to put it mildly). If you can, itās best to let the user decide what kind of allocation strategy they want to use (aka, they can hand different allocators to your Cowsay
object). Thatās part of why we program in Zig - we care about things like allocation