Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Yeah, I specifically added the "in Rust" here because I know Zig really cares about this problem. I haven't actually used contemporary Zig enough to be able to say anything about its approach, though.

> Zig's standard library functions that require allocation take the allocator as a parameter, and return oom errors back up to the caller to do with as they please.

You could do something like this in Rust. Here's a function that accepts a vector and pushes something onto it:

  fn foo(v: &mut Vec<i32>, item: i32) {
      v.push_back(item);
  }
(Keeping it non-generic to simplify.)

Does the data structure hold the allocator, or the function? What if I created a list with one allocator, but called push_back with another? It'd have to be in the structure, right? Given that, here's what this feature would look like, roughly, if Vec<T> supported it:

  fn foo<A: Allocator>(v: &mut Vec<i32, A>, item: i32) -> Result<(), Box<Error>> {
      v.push_back(item)?;
  }
This is significantly more boilerplate. The type signature is more than twice as long! You'd also have to adjust the caller:

  // before
  foo(&mut v, 5);

  // after
  foo(&mut v, 5)?;

Of course, it'd be possible to maybe reduce this with langauge features. What does this look like in Zig?


Here's an excerpt from std.ArrayList[1]:

    pub fn ArrayList(comptime T: type) type {
        return struct {
            const Self = @This();

            raw_items: []T,
            len: usize,
            allocator: *Allocator,

            // ...

            pub fn append(self: *Self, item: T) !void {
                const new_item_ptr = try self.addOne();
                new_item_ptr.* = item;
            }
            
            pub fn addOne(self: *Self) !*T {
                const new_length = self.len + 1;
                try self.ensureCapacity(new_length);
                return self.addOneAssumeCapacity();
            }
            
            // ...
        };
    }


    test "std.ArrayList.basic" {
        var bytes: [1024]u8 = undefined;
        const allocator = &std.heap.FixedBufferAllocator.init(bytes[0..]).allocator;

        var list = ArrayList(i32).init(allocator);

        try list.append(1);
        try list.append(2);
        try list.append(3);

        assert(list.len == 3);
    }

In summary, functions that can fail have an inferred error set, or an explicit one[2]. At the callsite of functions that can fail will either be `if`, `while`, `catch`, or `try`, in order to deal with the error case.

[1]: https://github.com/ziglang/zig/blob/e3d8cae35a5d194386eacd9a...

[2]: https://ziglang.org/documentation/master/#Error-Set-Type


Neat! So yeah, reducing Result to ! and inferring errors makes this way less boiler-plate-y. One of Rust's core design decisions is that we should never do type inference for function definitions, so we can't do this.

And yeah, if you hold a pointer to the allocator instead of parameterizing by it, that helps too. With that in mind,

  fn append(&mut self, item: i32) -> Result<(), Box<Error>> {
      v.push_back(item)?;
  }
In today's Rust, which is sort of what already happens; a given instance can't change the allocator, but refers to the global one. I don't have a great handle on the tradeoffs from keeping a pointer vs parameterization.

Anyway, yeah, if we allowed for inferring the error type, that would make this almost the same as Zig here. Stealing your syntax:

  fn append(&mut self, item: i32) -> !() {
      v.push_back(item)?;
  }
A four character diff from current Rust. Very cool! Thanks for the elaboration. That's quite nice.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: