How to implement a one-line Builder pattern in Zig when structs default to const?

const std = @import("std");
const expect = std.testing.expect;

const ListWrapper = struct {
    list: std.ArrayList(u8),
    allocator: std.mem.Allocator,
    pub fn init(allocator: std.mem.Allocator) ListWrapper {
        return ListWrapper{ .list = std.ArrayList(u8).init(allocator), .allocator = allocator };
    }

    pub fn append(self: *ListWrapper, item: u8) ListWrapper {
        self.list.append(item) catch unreachable;
        return self;
    }

    pub fn print(self: *ListWrapper) void {
        const slice = self.list.toOwnedSlice();
        std.debug.print("{}\n", .{slice});
        self.allocator.free(slice);
    }
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    ListWrapper.init(allocator).append('H').append('E').append('L').append('L').append('O').print();
}

when run, this gives

scratch.zig:28:32: error: expected type '*scratch.ListWrapper', found '*const scratch.ListWrapper'
    ListWrapper.init(allocator).append('H').append('E').append('L').append('L').append('O').print();
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~

Which I think means that .append is being passed a *const ListWrapper, since that’s the default for any variable created in Zig.

I can get around this by splitting the declaration and operations into separate lines:

var lw = ListWrapper.init(allocator);
lw.append('H').append('E').append('L').append('L').append('O').print();

but this feels overly verbose. Is there any way to make .init return a var, or to mark the result of an intermediate operation as var - something like @asVar(ListWrapper.init(allocator)).append('H').[...]?

a value not assigned to a variable is temporary, and taking a pointer to a temporary will always be const
this lets zig do more optimisations

a solution would be

var lw = ListWrapper.init(allocator);
lw.append('H').append('E').append('L').append('L').append('O').print();

some notes

  • ArrayList has appendSlice function, to append multiple elements at a time
  • toOwnedSlice can return an error
  • not sure why print takes ownership of the ArrayList slice, you can access the items with .items, it makes no sense for print to free the slice, anyone would be surprised by that behaviour
  • instead you should have a deinit function to clean up the resources instead of doing it in print
  • ArrayList stores the allocator, if you need to store it for some reason use ArrayListUnmanaged, same api as ArrayList except you pass an allocator to functions that need it

this isnt a builder pattern this is just function chaining

a builder pattern is this

var builder = Builder.init();
builder.stuff();
builder.moreStuff();
const stuff = builder.build();

Builders often do allow function chaining, but they are different is my point

if you have any questions ask :3

5 Likes

This is an error discarding misuse if you want your program to crash if append fails you can use panic, otherwise you should handle the error or return it.

1 Like

Ack’d, thank you - this was just a small demo program to illustrate the question rather than production code, so it was deliberately unsafe for brevity, but I appreciate the note!

Got it, ok, thank you. That’s going to require some changes to my usual style - can’t create an object and then immediately call a (mutating) function on it in the same line - but that’s a small price to pay for the optimizations that Zig provides. Thanks for clarifying!