Polymorphism with Zig 0.10.0

I found the following article on polymorphism in Zig:
https://revivalizer.xyz/post/the-missing-zig-polymorphism-reference/
However, it doesn’t work with Zig 0.10.0 - various errors about things needing to be comptime. Is anyone aware of an up to date example of Zig polymorphism?

1 Like

https://www.nmichaels.org/zig/interfaces.html

quick and dirty example of subtyping

const std = @import("std");

const Interface = struct {
    one: *const fn(b: *Base) void,
    two: *const fn(b: *Base) void,
};

// abstract class
const Base = struct {
    x: i32 = 5,
    methods: Interface, //  VMT
};

const ClassA = struct {
    base: Base,
    a: i32,

    fn init(a: i32) ClassA {
        return ClassA {
            .base = Base {
                .methods = .{
                    .one = &one,
                    .two = &two,
                },
            },
            .a = a,
        };
    }

    fn one(base: *Base) void {
        var self = @fieldParentPtr(ClassA, "base", base);
        std.log.info("A {}", .{base.x + self.a});
    }

    fn two(base: *Base) void {
        var self = @fieldParentPtr(ClassA, "base", base);
        std.log.info("A {}", .{base.x - self.a});
    }
};

const ClassB = struct {
    base: Base,
    b: i32,

    fn init(b: i32) ClassB {
        return ClassB {
            .base = Base {
                .methods = .{
                    .one = &one,
                    .two = &two,
                },
            },
            .b = b,
        };
    }

    fn one(base: *Base) void {
        var self = @fieldParentPtr(ClassB, "base", base);
        std.log.info("B {}", .{base.x + self.b});
    }

    fn two(base: *Base) void {
        var self = @fieldParentPtr(ClassB, "base", base);
        std.log.info("B {}", .{base.x - self.b});
    }

};

pub fn main() void {
    var a = ClassA.init(7);
    var b = ClassB.init(8);
    a.base.methods.one(&a.base);
    a.base.methods.two(&a.base);
    b.base.methods.one(&b.base);
    b.base.methods.two(&b.base);
}

don’t take it too seriously, though :slight_smile:

Thanks, @dee0xeed. However, neither Nathan Michaels example or the later one from Zig NEWS (by KilianVouunckx) seem to work with Zig 0.10.0 either. I guess something has changed with the new compiler, but I’m not yet enough up to speed with Zig to really figure out what the problem is.

Also, I’m not sure if or how your example really addresses what I’m trying to do. I want to be able to choose between several “classes” at runtime, assign an “object” of the chosen “class” to a variable, and then call “methods” on it which dispatch to the appropriate class. Taking your example, I’d want something like this to work:

pub fn main() void {
    var i: usize = 0;
    while (i < 2) {
        var e = if (i == 0) ClassA.init(7) else ClassB.init(8);
        e.base.methods.one(&e.base);
        i += 1;
    }
}

but of course it does not.

Any other ideas?

Thanks

pub fn main() void {
    var i: usize = 0;
    while (i < 2) {
        var base: *Base = undefined;
        if (i == 0) {
            var x = ClassA.init(7);
            base = &x.base;
        } else { 
            var x = ClassB.init(8);
            base = &x.base;
        }
        base.methods.one(base);
        base.methods.two(base);
        i += 1;
    }
}

Works as expected.

Another variant

const std = @import("std");

const Interface = struct {

    oneImpl: *const fn(b: *Interface) void,
    twoImpl: *const fn(b: *Interface) void,

    fn one(i: *Interface) void {
        i.oneImpl(i);
    }

    fn two(i: *Interface) void {
        i.twoImpl(i);
    }
};

const ClassA = struct {
    i: Interface,
    a: i32,

    fn init(a: i32) ClassA {
        return ClassA {
            .i = Interface {
                .oneImpl = &one,
                .twoImpl = &two,
            },
            .a = a,
        };
    }

    fn one(i: *Interface) void {
        var self = @fieldParentPtr(ClassA, "i", i);
        std.log.info("A1 {}", .{self.a});
    }

    fn two(i: *Interface) void {
        var self = @fieldParentPtr(ClassA, "i", i);
        std.log.info("A2 {}", .{self.a});
    }
};

const ClassB = struct {
    i: Interface,
    b: i32,

    fn init(b: i32) ClassB {
        return ClassB {
            .i = Interface {
                .oneImpl = &one,
                .twoImpl = &two,
            },
            .b = b,
        };
    }

    fn one(i: *Interface) void {
        var self = @fieldParentPtr(ClassB, "i", i);
        std.log.info("B1 {}", .{self.b});
    }

    fn two(i: *Interface) void {
        var self = @fieldParentPtr(ClassB, "i", i);
        std.log.info("B2 {}", .{self.b});
    }

};

pub fn main() void {
    var k: usize = 0;
    while (k < 2) {
        var i: *Interface = undefined;
        if (k == 0) {
            var o = ClassA.init(7);
            i = &o.i;
        } else { 
            var o = ClassB.init(8);
            i = &o.i;
        }
        i.one();
        i.two();
        k += 1;
    }
}

Note: var i = &ClassA.init(7).i; not compiled.

I posted this example once on Reddit, related to dynamic dispatch. I think it’s the simplest form of polymorphism that can be highly performant but not very extensible. If you have total control of the code base and don’t need or expect users to add implementations of the interface, a tagged union can be an easy solution.

const std = @import("std");

const Cat = struct {
    fn meow(_: Cat) []const u8 {
        return "meow!";
    }
};

const Dog = struct {
    fn bark(_: Dog) []const u8 {
        return "woof!";
    }
};

const Animal = union(enum) {
    cat: Cat,
    dog: Dog,

    fn speak(animal: Animal) []const u8 {
        return switch (animal) {
            .cat => |c| c.meow(),
            .dog => |d| d.bark(),
        };
    }
};

fn poly(a: Animal) void {
    std.debug.print("{s}\n", .{a.speak()});
}

pub fn main() void {
    const cat = Animal{ .cat = .{} };
    const dog = Animal{ .dog = .{} };
    poly(cat);
    poly(dog);
}

I guess it’s worth looking at standart library (Allocator, Random) as KilianVounckx recommends in the beginning of his article.

Thanks, both. I’ll work through all of your suggestions when I get a chance.

Though using @fieldParentPtr for implementing dynamic dispatch is considered outdated, just in case, beware of this “footgun”:

const std = @import("std");

const Interface = struct {
    methodImpl: *const fn(b: *Interface) void,
    fn method(i: *Interface) void {
        i.methodImpl(i);
    }
};

const ClassA = struct {
    i: Interface,
    a: i32,
 
    fn init(a: i32) ClassA {
        return ClassA {
            .i = Interface {
                .methodImpl = &method,
            },
            .a = a,
        };
    }

    fn method(i: *Interface) void {
        var self = @fieldParentPtr(ClassA, "i", i);
        std.log.info("self.a = {}", .{self.a});
    }
};

pub fn main() void {
    var o = ClassA.init(7);
    var i = &o.i;
    i.method();

    var ii = o.i; // or ii = ClassA.init(7).i;
    // oops!
    // we made a *copy* of the interface
    // `ii` is not *inside* an object and 
    // @fieldParentPtr() will return a pointer to wrong location
    ii.method();
}

Ouput

info: self.a = 7
info: self.a = 2291344 // oops

Citation from this text:

Zig implements dynamic dispatch via a pattern where a struct of function pointers is nested in an outer struct containing closed-over data. The functions can access this data using @fieldParentPtr . It’s really easy to accidentally move the inner struct to a different location, in which case @fieldParentPtr will point at some random location

A couple of days ago, this was posted to Hacker News:
Easy Interfaces with Zig 0.10.0
It looks to be a simplified method of doing interfaces using tagged unions, by using “inline else”. Whilst it looks a little strange, I think it’s probably the best option for my current requirements. As Loris (and both of you) pointed out, you don’t often really need dynamic dispatch, so this will probably do me for now.

Here’s a complete program using Loris’ examples (with a couple of his typos corrected!):

const std = @import("std");

const Cat = struct {
    anger_level: usize,

    pub fn talk(self: Cat) void {
        std.debug.print("Cat: meow! (anger lvl {})\n", .{self.anger_level});
    }
};

const Dog = struct {
    name: []const u8,

    pub fn talk(self: Dog) void {
        std.debug.print("{s} the dog: bark!\n", .{self.name});
    }
};

const Animal = union(enum) {
    cat: Cat,
    dog: Dog,

    pub fn talk(self: Animal) void {
        switch (self) {
            inline else => |case| case.talk(),
        }
    }
};

pub fn main() void {
    var i: usize = 0;
    while (i < 2) {
        var e = if (i == 0)
            Animal{ .cat = .{ .anger_level = 3 } }
        else
            Animal{ .dog = .{ .name = "Bob" } };
        e.talk();
        i += 1;
    }
}
1 Like

yeah, switch with a single magic inline else branch is puzzling

If one variant has small data and another has lot of data there will be inefficient space usage. I added buff: [1024] u8 to the Dog and printed sizes of Cat, Dog and Animal:

sizeof(Cat) = 8
sizeof(Dog) = 1040
sizeof(Animal) = 1048

When that’s the case you can create a union of pointers:

const Animal = union (enum) {
   cat: *Cat,
   dog: *Dog,
   // ...
};
1 Like

Oh that’s funny, I hadn’t seen this comment and came up with pretty much the same exact example as @dude_the_builder! Although, yes, my point was to showcase inline else.

1 Like

It just expands into the explicit form that @dude_the_builder used in his example.

Yep, I’ve read your nice post on zig.news, thanks!

@kristoff , I guess it’s another controversy for the history books. lol

1 Like