kj4tmp
October 9, 2025, 5:16am
1
I’m writing some bindings for a rust library that uses the builder pattern a lot, can this be replicated in zig?
For example, here is some rust code:
publisher
.put(buf)
.encoding(Encoding::TEXT_PLAIN)
.attachment(attachment.clone())
.await
.unwrap();
yes, you just return self param for the builder type from each function, it can be through a pointer or copied, the former requires a variable.
2 Likes
I generally prefer returning copies, because zig treats temporary values as const, but if you are ok with the ‘var’ declaration before, you can use pointers.
var builder = Builder{}; // this is necessary to get around the const temporary.
const obj = builder.methods().stuff().unwrap(); // allowed to work with the non-const pointer now.
1 Like
adria
October 9, 2025, 9:47am
4
I have implemented something like this for my Matrix struct in Zignal
//! Dynamic matrix with runtime dimensions
//!
//! ## Chainable Operations
//!
//! Matrix operations can be chained together for expressive linear algebra:
//! ```zig
//! const result = try matrix.transpose().inverse().scale(2.0).eval();
//! ```
//!
//! Each operation executes immediately and returns a new Matrix. Errors are
//! stored internally and checked when you call `.eval()` at the end of the chain.
//!
//! ## Memory Management
//!
//! **Important**: When chaining multiple operations, each operation creates a new
//! matrix. For optimal memory usage, use an ArenaAllocator:
//!
//! ```zig
//! var arena = std.heap.ArenaAllocator.init(allocator);
//! defer arena.deinit();
This file has been truncated. show original
The main challenge was that some methods allocate abs might error out. So I just track the errors internally and only return them when we unwrap the computation. I’m open to better alternatives.
that’s roughly the same approach I used , internally the builder is a union(enum) {valid: internalBuilder, invalid: anyerror}
, and then unwrap returns the error.
I haven’t found a model I like better yet.
Instead of returning an error union, you might need to keep the errors monadically.
For example:
const std = @import("std");
const Entry = struct {
x: usize,
};
const BuildError = union(enum) {
invalid: []const u8,
};
const BuildResult = union(enum) {
right: std.array_list.Aligned(Entry, null),
left: std.array_list.Aligned(BuildError, null),
alloc_error,
};
const Builder = struct {
allocator: std.mem.Allocator,
entries: std.array_list.Aligned(Entry, null),
errors: std.array_list.Aligned(BuildError, null),
has_alloc_error: bool,
pub fn init(allocator: std.mem.Allocator) Builder {
return Builder{
.allocator = allocator,
.entries = .empty,
.errors = .empty,
.has_alloc_error = false,
};
}
pub fn addEntry(self: *Builder, entry: Entry) *Builder {
if (entry.x > 100) { return self.addAsInvalid(entry); }
self.entries.append(self.allocator, entry) catch |err| switch (err) {
error.OutOfMemory => { self.has_alloc_error = true; },
};
return self;
}
fn addAsInvalid(self: *Builder, entry: Entry) *Builder {
const mgs = std.fmt.allocPrint(self.allocator, "invalid entry: {}", .{ entry })
catch |err| switch (err) {
error.OutOfMemory => {
self.has_alloc_error = true;
return self;
},
};
self.errors.append(self.allocator, BuildError{ .invalid = mgs })
catch |err| switch (err) {
error.OutOfMemory => { self.has_alloc_error = true; }
};
return self;
}
pub fn build(self: Builder) BuildResult {
if (self.has_alloc_error) {
return .alloc_error;
}
if (self.errors.items.len > 0) {
return BuildResult{ .left = self.errors };
}
return BuildResult{ .right = self.entries };
}
};
pub fn main() !void {
const allocator = std.heap.page_allocator;
var b = Builder.init(allocator);
const r1 = b.addEntry(Entry{ .x = 42 }).build();
const r2 = b.addEntry(Entry{ .x = 108 }).build();
std.debug.print("r1: {}\n", .{ r1 });
std.debug.print("r2: {}\n", .{ r2 });
}