As a bonus, this means that when you're reading code you can easily spot the function calls that have a possibility of failing.
As a bonus, this means that when you're reading code you can easily spot the function calls that have a possibility of failing.
> 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? 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
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.https://github.com/ziglang/zig/blob/master/std/debug/failing...