I’ve noticed that a common pattern in my usage of std.Io is collecting results into a std.ArrayList and wrapping functions that return error unions for std.Io.Group. std.Io.Select is nice, but doesn’t quite fit my use case.
To handle these issues more nicely, I’ve written a little wrapper around std.Io.Group that collects the output of a bunch of functions at once. You can use it like this, for example:
fn readABunchOfFiles(gpa: std.mem.Allocator, io: std.Io, files: []const []const u8) !void {
var c: Collect(std.Io.Dir.ReadFileAllocError![]u8) = .init;
defer c.deinit(gpa, io);
defer for (c.results.items) |result| gpa.free(result);
for (files) |name| {
try c.async(gpa, io, std.Io.Dir.readFileAlloc, .{ .cwd(), io, name, gpa, .unlimited });
}
for (try c.awaitResults(io)) |result| {
std.debug.print("{s}", .{result});
}
}
To make error handling simpler, Collect uses “error or data”, meaning awaitResults either gives an error or all results. This matches a lot of real-world tasks reasonably well, but to release resources when applicable, the remaining results can be accessed with results.items. When this isn’t the case, std.Io.Select is probably the better choice anyway.
Results are unordered because I was too lazy to make them ordered, and I would have to handle errors in a more complex way, something like using a bit set to indicate failed tasks.