Comment by qudat

Comment by qudat 20 hours ago

7 replies

I'm excited to see where this goes. I recently did some io_uring work in zig and it was a pain to get right.

Although, it does seem like dependency injection is becoming a popular trend in zig, first with Allocator and now with Io. I wonder if a dependency injection framework within the std could reduce the amount of boilerplate all of our functions will now require. Every struct or bare fn now needs (2) fields/parameters by default.

messe 20 hours ago

> Every struct or bare fn now needs (2) fields/parameters by default.

Storing interfaces a field in structs is becoming a bit of an an anti-pattern in Zig. There are still use cases for it, but you should think twice about it being your go-to strategy. There's been a recent shift in the standard library toward "unmanaged" containers, which don't store a copy of the Allocator interface, and instead Allocators are passed to any member function that allocates.

Previously, one would write:

    var list: std.ArrayList(u32) = .init(allocator);
    defer list.deinit();
    for (0..count) |i| {
        try list.append(i);
    }
Now, it's:

    var list: std.ArrayList(u32) = .empty;
    defer list.deinit(allocator);
    for (0..count) |i| {
        try list.append(allocator, i);
    }
Or better yet:

    var list: std.ArrayList(u32) = .empty;
    defer list.deinit(allocator);
    try list.ensureUnusedCapacity(allocator, count); // Allocate up front
    for (0..count) |i| {
        list.appendAssumeCapacity(i); // No try or allocator necessary here
    }
  • turtletontine 15 hours ago

    I’m not sure I see how each example improves on the previous (though granted, I don’t really know Zig).

    What happens if you call append() with two different allocators? Or if you deinit() with a different allocator than the one that actually handled the memory?

    • messe 15 hours ago

      Storing an Allocator alongside the container is an additional 16-bytes. This isn't much, but starts adding up when you start storing other objects that keep allocators inside of those containers. This can improve cache locality.

      It also helps devirtualization, as the most common case is threading a single allocator through your application (with the occasion Arena allocator wrapping it for grouped allocations). When the Allocator interface is stored in the container, it's harder for the optimizer to prove it hasn't changed.

      > What happens if you call append() with two different allocators? Or if you deinit() with a different allocator than the one that actually handled the memory?

      It's undefined behaviour, but I've never seen it be an issue in practice. Expanding on what I mentioned above, it's typical for only a single allocator to be used for long live objects throughout the entire program. Arena allocators are used for grouped allocations, and tend to have a well defined scope, so it's obvious where deallocation occurs. FixedBufferAllocator also tends to be used in the same limited scope.

scuff3d 17 hours ago

I think a good compromise between a DI framework and having to pass everything individually would be some kind of Context object. It could be created to hold an Allocator, IO implementation, and maybe a Diagnostics struct since Zig doesn't like attaching additional information to errors. Then the whole Context struct or parts of it could be passed around as needed.

Mond_ 19 hours ago

Yes, and it's good that way.

Please, anything but a dependency injection framework. All parameters and dependencies should be explicit.

SvenL 20 hours ago

I think and hope that they don’t do that. As far as I remember their mantra was „no magic, you can see everything which is happening“. They wanted to be a simple and obvious language.

  • qudat 19 hours ago

    That's fair, but the same argument can be made for Go's verbose error handling. In that case we could argue that `try` is magical, although I don't think anyone would want to take that away.