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.
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 think this falls under style and use-case. In my case, I wanted to keep the calling site error handling to a very zig idiomatic:
const value = try Builder.init(allocator).add(thing).finish();
But I eventually settled for two allocators because I had some use-cases where the building was happening in an arena, but the result needed to last longer than the arena, and I didn’t want to copy after the finish.
const value = try Builder.init(arena.allocator()) // use arena for intermediates.
.add(thing_one)
.add(thing_two)
.finish(long_lived_allocator); // use the parent allocator for the final (owned) result.
errdefer value.deinit(long_lived_allocator);
I really wanted the try so that the rest of the defer statements above would just “magically” happen and the builder was easy to use. Keeping track of the adds that succeeded when the already known failure was going to erase them anyway didn’t seem like it added much value.
I did think that “buffering” the errors so that all errors could be reported at once would be valuable, but I couldn’t find a way to do it with the above call site / usage restrictions. []anyerror doesn’t work with try/catch/if the way I wanted. I’m not aware of a way to attach all the buffered errors as extra data onto a single error. As a result, I decided the first error was good enough for 80% of the real use cases and optimized from there.
I agree that option structs are the better approach if your use-case supports them. Why write code when a good struct with some sane defaults (and fields lacking defaults to force assignment) gets you most of the way.
I think the builder pattern and monads are two different things.
A builder is just a way to store some information about how you want to initialize some object before actually initializing it, right? So you have a builder object that you mutate till you have all the data to finely build the actual object you want.
A monad has to have a function (often called bind) with the signature fn (Monad(A), fn (A) Monat(B)) Monad(B) (Where Monad(A) is a type constructor so something of the form fn Monad(comptime t : type) type) that is a pure function with no side-effects, builders do not have this function and are not necessarily type constructors.
But I think you can implement builders with the state monad.