Hello
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.
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()?
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:
What I myself ended up doing is something like the following:
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.
Abandon what you are doing and start over in a new way.
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:
Using composition/Exposing implementation details by adding a member to your structs like proposed by @pachde and @vulpesx
C style object orientated programming using runtime shenanigans(I wouldnât use this but there are people who are fan of this).
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:
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.
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â).
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.
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).