Can zig do builder pattern?

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

I have implemented something like this for my Matrix struct in Zignal

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 });
}