How to extend a type in Zig

Hello :slight_smile:
I need to create N types that share a lot of functionality. Say they have 12 member functions each, 2 of which are specific to each type (in function signature and implementation), and the other 10 are exactly the same for all types (in function signature and implementation).

I wanted to abstract out the common behavior so I wouldn’t have to rewrite the same functions over and over again.
What I did was

fn MyLargeType() type {

   return struct {
       fn one_of_many() void {...}
       fn another_one_of_many() void {...}
      // ... many more functions
   }
}

fn SmallType1() {
     return struct {
           usingnamespace MyLargeType;
           fn specificFunction1() {...}

           // plus a couple other functions specific to this type
     }
}

// same for SmallType2... SmallTypeN

Obviously this won’t work anymore since usingnamspace has been removed from the language,
In Zig 0.15 hwo may I achieve this sort of behaviour? I’m open to unusual solutions.

Only thing that comes to mind (besides code generation, which I don’t really want to do unless it’s absolutely the only way) is having a MyLargeType field in all of my types and forwarding its methods. t/This saves me some LoC for the implementation itself, but I still have to repeatedly write declarations and the forwarding part myself by hand.
For many dozens of methods, and N types, that’s a whole lot of manual work.

Please help me, thank you !

Some will argue that instead of attempting to reinvent OOP inheritance in Zig, you should model your app differently through composition.

5 Likes

that is the recommended solution. Is there any reason you need to write wrappers around the base type’s functions instead of just doing small_type.base.one_of_many()?

1 Like

It was an intentional decision to remove the mixin part from the language without any serious alternative as it’s not the prefered way to write zig code.
I myself got hit by this hard as there were only a few files in my projects that didn’t use this feature.

I believe there are like a 3 or 4 things you can possibly do:

  1. What I myself ended up doing is something like the following:
    const test1 = struct 
    {
        comptime { @import("mixin").implements(test1, .{Interface}); }
        pub const func2 = Interface(test1).func2;
        pub const func3 = Interface(test1).func3;
        pub const func1 = Interface(test1).func1;
    
        value: i32
    };
    
    fn Interface(comptime record: type) type
    {
        return struct
        {
            pub fn func1(r: record) i32 {return r.value;}
            pub fn func2() i32 {return 2;}
            pub fn func3(r: *record, value: i32) i32 {r.value += value; return r.value;}
        };
    }
    
    The mixin.implements import does some checking to make sure it points to the correct implementation. You do end having to duplicate a lot of extra lines in your code that the usingnamespace used to do for you. But atleast the behaviour you get is the same as with usingnamespace.
  2. Abandon what you are doing and start over in a new way.
  3. Basically the same as option 1. but without the strict checking.

Some people prefer to do the following or prefer a more C like way of doing things:

  1. Using composition/Exposing implementation details by adding a member to your structs like proposed by @pachde and @vulpesx
  2. C style object orientated programming using runtime shenanigans(I wouldn’t use this but there are people who are fan of this).

use composition instead

2 Likes

I’ve found that it’s often more manageable to simply stick a switch() into each of the “polymorphic” functions. Modern editors handling code folding quite well. Massive switch statements really don’t cause practical issues.

Conceptually, you’d only have one parameterized type:

fn MyType(comptime options: struct {
    param1: bool = false,
    param2: bool = false,
    param3: usize = 0,
    // ...
}) type {
    return struct {
        // ...
    };
};

Individual methods within the returned struct would then simply react to the parameters given (possibly using switch). When they don’t, then the compiler will automatically collapse them to the same function.

1 Like

What I do in C (and also would do in Zig) is nested structs (e.g. what’s commonly called composition), e.g.:

// all resource objects have a slot:
typedef struct {
    uint32_t id;
    uint32_t uninit_count;
    sg_resource_state state;
} _sg_slot_t;

// all common properties of a buffer resource:
typedef struct {
    // ...
} _sg_buffer_common_t;

// the 3D API specific parts of a buffer resource (here: D3D11):
typedef struct {
    // ...
} _sg_d3d11_buffer_t;

// and finally the actual 'composed' buffer for when the D3D11 backend is active:
typedef struct {
    sg_slot_t slot;
    sg_buffer_common_t cmn;
    sg_d3d11_buffer_t d3d11;
} _sg_buffer_t;

…this is similar to inheritance (Slot => BufferBase => D3D11Buffer => Buffer), but more flexible since you can ‘compose’ the different parts more freely (also google for ‘composition over inheritance’).

How can I achieve what I want (avoiding code repetition) with composition in this case? Can you give an example of what you mean?

Like this basically:

…e.g. instead of

    bla.one_of_many();

…it would be:

    bla.cmn.one_of_many();

(it’s essentially the same thing you propose, just minus the usingnamespace magic)

5 Likes

This of course only works when the functions being called don’t use anything from SmallType1.
Things will get hairy when you do need to call something from the parent. You either need to pass in the parent as parameter to the function making it possible to pass in the wrong thing or you’ll have to do some very dirty C style/linux kernel object orientated programming.

1 Like

True, but that would be similar to a parent class trying to access items of a subclass. The more common scenario of the subclass accessing the parent class works (via self.cmn).

you can use @fieldParentPtr

3 Likes

should be avoided as it is illegal behaviour to use that when its not in the specified field of the specified type

1 Like

While this may seem tedious, you only need to do it once, and you can copy/paste into all the other SmallTypes.