Factory function advice

I’ve got an application where a factory function would be useful - i.e. a function which creates and returns an object, where the type of the object is decided at runtime according to parameters passed to the function. All objects returned by the function must share a common interface so that they can be used interchangeably. This is somewhat equivalent to a function which returns a trait object in rust.

Below is what I’ve come up with (it’s a trivial example to illustrate the method). However, I’m not sure if it’s the best way of implementing a factory function - does anyone know a more concise or better way? Are there any potential problems with my approach here?

Sorry for the length of the code - I’m not sure how I could cut it down much without losing the point.

const std = @import("std");

/// Define a factory which creates one of several (in this case two)
/// types of object. All possible objects created here share a common
/// interface, and it is this interface that is returned.
fn factory(alloc: *std.mem.Allocator, direction: Direction, start: u32) *Counter {
    return switch (direction) {
        .up => &Upcounter.init(alloc, start).iface,
        .down => &Downcounter.init(alloc, start).iface,
    };
}

const Direction = enum { up, down };

/// Define a common interface shared by all objects created by the
/// factory. An interface forwards calls to the object that contains
/// it.
const Counter = struct {
    countFn: fn (self: *Counter) u32,
    deinitFn: fn (self: *Counter) void,

    pub fn count(self: *Counter) u32 {
        return self.countFn(self);
    }
    pub fn deinit(self: *Counter) void {
        self.deinitFn(self);
    }
};

/// Define an up-counter which implements the Counter interface.
const Upcounter = struct {
    iface: Counter,
    alloc: *std.mem.Allocator,
    val: u32,

    fn init(alloc: *std.mem.Allocator, start: u32) *Upcounter {
        const ob = alloc.create(Upcounter) catch @panic("FAILED TO CREATE Upcounter");
        ob.alloc = alloc;
        ob.iface = Counter{ .countFn = count, .deinitFn = deinit };
        ob.val = start;
        return ob;
    }

    fn deinit(iface: *Counter) void {
        const self = @fieldParentPtr(Upcounter, "iface", iface);
        self.alloc.destroy(self);
    }

    fn count(iface: *Counter) u32 {
        const self = @fieldParentPtr(Upcounter, "iface", iface);
        const last = self.val;
        self.val += 1;
        return last;
    }
};

/// Define a down-counter which implements the Counter interface.
const Downcounter = struct {
    iface: Counter,
    alloc: *std.mem.Allocator,
    val: u32,

    fn init(alloc: *std.mem.Allocator, start: u32) *Downcounter {
        const ob = alloc.create(Downcounter) catch @panic("FAILED TO CREATE Downcounter");
        ob.alloc = alloc;
        ob.iface = Counter{ .countFn = count, .deinitFn = deinit };
        ob.val = start;
        return ob;
    }

    fn deinit(iface: *Counter) void {
        const self = @fieldParentPtr(Downcounter, "iface", iface);
        self.alloc.destroy(self);
    }

    fn count(iface: *Counter) u32 {
        const self = @fieldParentPtr(Downcounter, "iface", iface);
        const last = self.val;
        self.val -= 1;
        return last;
    }
};

test "factory" {
    const expect = std.testing.expect;

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer {
        const leaked = gpa.deinit();
        if (leaked) std.testing.expect(false) catch @panic("TEST FAIL - MEMORY LEAK");
    }
    const alloc = &gpa.allocator;

    const up1 = factory(alloc, .up, 100);
    defer up1.deinit();

    const down1 = factory(alloc, .down, 200);
    defer down1.deinit();

    const up2 = factory(alloc, .up, 300);
    defer up2.deinit();

    const down2 = factory(alloc, .down, 400);
    defer down2.deinit();

    try expect(up1.count() == 100);
    try expect(down1.count() == 200);
    try expect(up2.count() == 300);
    try expect(down2.count() == 400);

    try expect(up1.count() == 101);
    try expect(down1.count() == 199);
    try expect(up2.count() == 301);
    try expect(down2.count() == 399);
}

I haven’t read thoroughly your code yet, but as a quick first question: have you considered using a tagged union to implement the polymorphism? The trick is to add to the union itself the interface methods, which internally then dispatch correctly depending on the active case.

I did wonder if there was a method of doing this using a tagged union, but couldn’t figure out a pattern to do it which achieved what I wanted. I’ll have another think about it based on your suggestion. The method I used in the above code was based on the interface method described in https://www.nmichaels.org/zig/interfaces.html - I should probably have credited that before but I forgot.

Take a look at this if you haven’t already:

1 Like

Thank. I have watched some of that before, and I’ll give it another go, but I find it very hard to take information in from videos and talks - I am much better with printed information.

I’ve been trying to figure this out, but I’m not quite getting it. Any chance of an example?

Sure!

const std = @import("std");

const Foo = struct {
    a: bool,

    pub fn msg(self: @This()) void {
        std.debug.print("Foo: {}\n", .{self.a});
    }
};

const Bar = struct {
    b: []const u8,

    pub fn msg(self: Bar) void {
        std.debug.print("Bar: {s}\n", .{self.b});
    }
};

const Messenger = union(enum) {
    foo: Foo,
    bar: Bar,

    pub fn msg(self: Messenger) void {
        // this boilerplate won't be necessary
        // once inline switches land in Zig
        switch (self) {
            .foo => |f| f.msg(),
            .bar => |b| b.msg(),
        }
    }
};

pub fn main() void {
    var myvar: Messenger = .{ .foo = .{ .a = false } };
    myvar.msg();

    myvar = Messenger{ .bar = .{ .b = "Hello!" } };
    myvar.msg();
}
1 Like

Thanks for the example, @kristoff - it was enough to point me in the right direction. I’ve written a revised version of my factory example using this approach, below. The main difference from your example was that I needed to use pointers for the self parameters because I want the created objects (structs) to have volatile state which can be modified by their methods.
The init() and deinit() methods don’t really do much here, but I’ll need them in my real program as there will be files to open and close etc.

I think that one possible advantage of this method compared to the “vtable” interfaces method in my previous example is that it creates the objects on the stack so no need for allocation and deallocation, so it is a bit simpler. The down side is that the switch statement is repeated in every forwarding function (see Counter.count() and Counter.deinint() - I tried it without but the program just crashes. Any suggestions on eliminating that repetition would be welcome.

Anyway, here’s my revised example, in case anyone is interested:

const std = @import("std");
const assert = std.debug.assert;

const Direction = enum { up, down };

/// Define the "Counter" factory (the init funtion) which will create
/// the appropriate type of counter object based on its parameters.
/// This union also defines the wrapper methods to forward calls to
/// the appropriate object.
const Counter = union(enum) {
    upcount: Upcounter,
    downcount: Downcounter,

    pub fn init(direction: Direction, start: u32) Counter {
        return switch (direction) {
            .up => Counter{ .upcount = Upcounter.init(start) },
            .down => Counter{ .downcount = Downcounter.init(start) },
        };
    }

    pub fn count(self: *Counter) u32 {
        return switch (self.*) {
            .upcount => |*u| u.count(),
            .downcount => |*d| d.count(),
        };
    }

    pub fn deinit(self: *Counter) void {
        return switch (self.*) {
            .upcount => |*u| u.deinit(),
            .downcount => |*d| d.deinit(),
        };
    }
};

/// Define an up-counter which implements the Counter interface.
const Upcounter = struct {
    val: u32,
    active: bool = false,

    fn init(start: u32) Upcounter {
        return .{ .val = start, .active = true };
    }

    fn count(self: *Upcounter) u32 {
        assert(self.active == true);
        const last = self.val;
        self.val += 1;
        return last;
    }

    fn deinit(self: *Upcounter) void {
        self.active = false;
    }
};

/// Define a down-counter which implements the Counter interface.
const Downcounter = struct {
    val: u32,
    active: bool = false,

    fn init(start: u32) Downcounter {
        return .{ .val = start, .active = true };
    }

    fn count(self: *Downcounter) u32 {
        assert(self.active == true);
        const last = self.val;
        self.val -= 1;
        return last;
    }

    fn deinit(self: *Downcounter) void {
        self.active = false;
    }
};

test "factory" {
    const expect = std.testing.expect;

    var up1 = Counter.init(.up, 100);
    defer up1.deinit();
    var down1 = Counter.init(.down, 200);
    defer down1.deinit();
    var up2 = Counter.init(.up, 300);
    defer up2.deinit();
    var down2 = Counter.init(.down, 400);
    defer down2.deinit();

    try expect(up1.count() == 100);
    try expect(down1.count() == 200);
    try expect(up2.count() == 300);
    try expect(down2.count() == 400);

    try expect(up1.count() == 101);
    try expect(down1.count() == 199);
    try expect(up2.count() == 301);
    try expect(down2.count() == 399);
}

You can’t fully eliminate the switch because it’s the exact place where you bridge from static dispatch to dynamic (based on the tag value). If all the methods have the same name it does seem verbose though, that’s true, and inline switches will allow you to write this:

switch(self){
   inline else => |impl| impl.count();
}

You will have to wait for stage2 for that though :^)

2 Likes

Yes, to be honest, I didn’t think it would work without the switches for that very reason. The inline switch looks good when it comes, but for the moment this will do nicely. Thanks for all your help.
(I’ve also got half way through Alex Naskos’ video. It appears that your suggested solution is similar to the first one he presents, whilst my original was similar to his second one. I’ll try to watch the rest of it at some point.)

1 Like