#An other failed pattern for interface implementation.

1 messages · Page 1 of 1 (latest)

crisp raven
#

Hi,

Snippet:

const std = @import("std");

fn DummyInterface(comptime Impl: type) type {
    return struct {
        impl: *const Impl,  // Implements the functionality
        const Self = @This();

        pub fn init(impl: *const Impl) Self { return .{ .impl = impl }; }
        pub fn get_num(self: *const Self) u32 { return self.impl.get_num(); } // Delegation
    };
}

const Impl5 = struct {
    const Self = @This();
    num: u32 = 5,

    pub fn init() Self { return .{}; }
    pub fn get_num(self: *const Self) u32 { return self.num; }
};

const Dummy5 = DummyInterface(Impl5);  // Aims to implement the "Interface"

pub fn main() !void {
    const check = struct {
        fn check(dummy_if: *const DummyInterface(type)) void {
            const num = dummy_if.get_num();
            std.debug.print("get_num(): {}\n", .{num});
        }
    }.check;
    const impl = Impl5.init();
    const dummy = Dummy5.init(&impl);
    check(&dummy);
}

I don't know the terminologies for the upper constructs, but I'd like to achieve something similar to abstract class implementation in C++ (or Interface in Java). So I create a generic struct (DummyInterface) which delegates functionality for an implementing struct (Impl5).
One of the problems that I get this error:

src/main.zig:33:11: error: expected type '*const main.DummyInterface(type)', found '*const main.DummyInterface(main.Impl5)'
    check(&dummy);
          ^~~~~~

How could I achieve that fn check(...) could accept any "sub-types" of DummyInterface?

fierce tangle
#

This is going wrong because DummyInterface(type) is giving type itself as the type. Since type is a type, things go weird.

i.e. impl: *const Impl ends up being a const pointer to a type. Which is... weird.

DummyInterface(Impl5) fixes that for you.

FWIW though Zig's type system is very different and does not map to abstracts or interfaces. Every DummyInterface(...) you make with a unique type will itself be an entirely unique type, you wouldn't be able to hand different ones around as if they're the same thing.

Check out how Writer or Allocator are implemented in the stdlib, they are the Zig way of creating "interfaces", but are roll-your-own, in that they store their own vtables.

crisp raven
#

I have briefly checked this article and seems to be very excellent, thanks!

crisp raven
#

Hi @fierce tangle ,
I have found something related to the article. Maybe I am just dumb and cannot understand the necessity of this magic: return ptr_info.@"pointer".child.writeAll(self, data);
Here I did a small test:

const Printer = struct {
    const Self = @This();

    impl: *anyopaque,
    printImpl: *const fn(impl: *anyopaque) void,

    pub fn init(impl: anytype) Self {
        const T = @TypeOf(impl);
        const implInfo = @typeInfo(T);

        const gen = struct {
            pub fn print(rawImplPtr: *anyopaque) void {
                const implPtr: T = @ptrCast(@alignCast(rawImplPtr));
                implInfo.@"pointer".child.print(implPtr);   // Is this really needed?
                implPtr.print();    // Doesn't this work this way?
            }
        };

        return .{.impl = impl, .printImpl = gen.print,};
    }

    pub fn print(self: *Self) void {
        self.printImpl(self.impl);
    }
};



const Print5 = struct {
    const Self = @This();
    num: u8 = 5,

    pub fn print(self: *Self) void {
        std.debug.print("Num: {}.\n", .{self.num});
    }
};

pub fn main() !void {
    var p5: Print5 = .{};
    var p = Printer.init(&p5);
    p.print();
}

Output:

$ zig run src/main.zig
Num: 5.
Num: 5.

So both implInfo.@"pointer".child.print(implPtr); and implPtr.print(); reaches Print5.print(). Than while the implInfo magic is needed?

fierce tangle
#

I'm not at a zig capable machine right now, but it may well just be that the article is using old Zig. I can check later today though.

Tbh I don't usually take that final "Prettier" step. Sometimes the simplicity is easier, and it's not what the stdlib does

#

Like I'm happy to do file.writer().writeAll() rather than allowing file.writeAll()

#

It's also a sort of code design thing, shown by Allocator where the actual functions you implement are different to those used. e.g. Allocator requires you to write alloc, free etc. but Allocator itself provides the create and destroy stuff.

It's more of a philosophical thing with Zig where polymorphism isn't really the goal I don't think, it's more that this is a way to improve the reusability of code by composing low level and higher level concepts.

crisp raven
#

I am just curious water these 2 lines are doing the same:

implInfo.@"pointer".child.print(implPtr);
implPtr.print();
fierce tangle
#

They should be yes, and will compile down to the same code.

Since print is a function that takes a pointer and calls it "self" you can either call it directly from the type (in your case above Print5.print(ptr)) or Zig (like other languages) let's you call it with a ptr.print().

implInfo.@"pointer".child == Print5 above. For your case.

Does that make sense?

#

(I personally find it weird that child is lowercase. If it's a type then surely coding standards would have it as Child)

crisp raven
#

Thanks! Do you know the author of that interface article? Maybe if he has time for it, he could review the related part.

fierce tangle
#

based on their alias it would seem to be @versed quest

versed quest
#

Ya, dunno why I did that :/

brazen mica
#

Pardon for tooting my own horn, but I made a library that handles boilerplate when defining interfaces or when implementing it: https://nvlled.github.io/zig-intf/#root.vt
It allows you to use the Alloctor interface pattern, but without using self: *anyopaque.