Default implementations of otherwise abstract interfaces

in languages like java/c# where i can explicitly define abstract interfaces, IDEs supporting the languages will often generated “default implementations” of the interfaces when the current editor source code declares that it inherits/implements this interface… very helpful when dealing with interfaces that could have a large number of methods with complex signatures…

putting aside the IDE and turning to zig, it seems i could achieve this effect by have an “abstract interface” defined as a concrete module with “stub implementations” of its methods… for sure i can blend these into my own module with usingnamespace

the problem, of course, is that i want to locally override only some of these methods; but like the IDE-based solutions, i’m ok if the entire “default interface” is sitting in front of me within the editor…

note – i am NOT interested in any sort of runtime dispatch… conceptually, it’s as if i “copied at build-time” the text from my interface into my current implementing module…

short of an IDE quite literally transforming my source code within the editor, is there a more “standalone” idiom for achieving this in zig???

1 Like

I can see a way you can do this but I wouldn’t call it… cannonical? Either way, what you can do is create a mixin type that provides self: anytype function signatures (that stops the issue of self-dependency loops) that a type can subscribe to.

The mixin type looks for an impl version of a function. If it cannot find one, it provides a default implementation instead.

const std = @import("std");

// mixin for incrementing a counter

const Subscribe = struct {
    pub fn incr(self: anytype) usize {
        if (!@hasDecl(@TypeOf(self.*), "incrImpl")) {
            self.counter += 1;
            return self.counter;
        } else {
            return self.incrImpl();
        }
    }
    pub fn decr(self: anytype) usize {
        if (!@hasDecl(@TypeOf(self.*), "decrImpl")) {
            self.counter -= 1;
            return self.counter;
        } else {
            return self.decrImpl();
        }
    }
};

const Foo = struct {
    // exposes public interface to Foo
    pub usingnamespace Subscribe;

    counter: usize = 0,    

    // non-public backend function
    fn incrImpl(self: *Foo) usize {
        self.counter += 2;
        return self.counter;
    }
    // no decr implementation
};

pub fn main() !void {
    var foo: Foo = .{};
    std.log.info("{}", .{ foo.incr() });   
    std.log.info("{}", .{ foo.decr() });   
}

Now for anything you want to specialize, you just write a somethingImpl function and it will be picked up by the mixin type. I’m not handling the issue of “you don’t have a counter either” here.

To keep the implementation functions non-public, it would have to be declared in the same file.

One drawback here is that you have to ensure function names are correct because it will silently dispatch to the subscribed version instead. Not sure what else we could do about that (probably something involving tuples… I’m sure lol) but that’s just something to be aware of.

3 Likes

wow… there is definitely some potential here!!!

i’m ok with the “impl” names being public; i’m already managing that with naming conventions that suggest “leave me alone”…

my use case – an abstract interface plus a set of benign, no-op defaults – is really just a special case of your Subscribe pattern…

another interesting boundary case is when NONE of the functions are implemented… this could be either “work in progress” or in fact an “error of intent”…

my use-case would probably have additional checks to validate that some container actually implements some interface – where the latter is described as named function types, to be matched against the purported implementor…

having a Subscribe (as available as part of the interface) in the current namespace would pass the validation… the only “weaker” link in this chain is ensuring the “impl” methods are not incorrectly named…

the one aspect of my ORIGINAL issue that is still unsolved is the need to actually generate textual stubs for what now has become the “impl” functions… clearly this is something an IDE already does for certain languages…

my hack solution relies an additional comptime assert that the current container does in fact implement all of the functions declared in some interface… (as mentioned above, use of Subscribe could pass the test)…

in my case, the “interface provider” ALSO has an default implementation which i can (gasp!!!) cut-and-paste into my own source file… as long as i programmatically validate the interface, i’m somewhat protected from getting out of sync… just rationalizing, but these abstract interfaces are in fact relatively stable and long-lived; otherwise you’re heading into what used to be termed the “fragile base class problem”…

again, a very helpful suggestion on your part…

2 Likes

would this work?

pub fn Base(comptime Class: type) type {
  return struct {
    pub fn add(this: Class, x: u32) u32 {
    if(comptime @hasField(Class, "add_override")) {
        return this.add_override(x);
    }
    return x + 1;
}

const Deriv = struct {
    const B = Base(@This());
    usingnamespace B;

   pub fn add_override(x: u32) u32 {
     return x+2;
   }
};
1 Like

very nice… that would certainly solve my problem AND has the nice feature that anyone who inherits Base has to explicitly state their intent to override…

in reality, this is a comptime delegation pattern with that employs a strict naming convention…

pretty much. I use something similar for a well-optimized POC iterator lib , but I create this long chain of types:

Slice(slice).map(x).filter(y).map(z).reduce(a)

where each of those is a type function and it returns a type:

ReduceIter(MapIter(FilterIter(MapIter(Slice([]u32), ReturnType(z))),ReturnType(x)),ReturnType(a))

for exmaple. These get big. I like the old behavior of usingnamespace from what I read about it, but its power was dimished in refinements of it (like no longer pulls in fields and you have to explicity reference the struct that you are pulling in to access the inheritered decls. But it still works for a number of things.

i don’t use that exact pattern I originally commented with, so im actually not even sure it works.

Andrew’s and this is basically the same thing, but his has the correct syntax, so use his.