Reading up on interfaces in Zig, and I cannot but get the vibe I got from the JavaScript community back in the early 2000s towards Object Oriented Programming.
JavaScript supports prototypical inheritance, but most of the mainstream programming language supports class based inheritance and for JavaScript to “fit in”, people started coming up with different “patterns” from module pattern to the decorator pattern to emulate some of these features native to languages that have inbuilt support for classical OOP.
All of these felt really hacky to me back then.
Since then, JavaScript has added a veneer of classical OOP on top of its prototypical nature making some of the popular “design patterns” back then to be obsolete.
Now reading up on interfaces in Zig. Currently reading Zig interfaces with a couple more queued up, I can’t help but get the same feeling. These are just hacks, patterns to fill the gap of a missing feature in the labguage.
Feature like having interfaces is clearly, a useful feature, hence why people are coming up with way to emulate it.
My question now is, does Zig plan to ever add such features like interfaces to the language, making it a native and first class citizen? or the ethos of Zig is to be as barebones as possible and leave things like interfaces as something to be emulated?
I don’t have a clear answer but this comments from @mlugg in this issue seems to explain a bit why it isn’t in the language yet, and might not be at all. replace anytype issue #17198
From what I have read in a lot of different issues on their github, is that the maintainers don’t think it’s absolutely necessary to have syntax support for it because technically the “hacky” way can get you there, and the friction necessary from this “hacky” way is more of a feature than a bug, of course I don’t know much and can’t speak about it a lot, but at least that seems to be the general consensus.
I’m not saying there won’t be interfaces of any kind, ever, but there are no current plans for adding them, and this proposal is incomplete. It does not propose anything specific.
I suggest if you want to use zig, you accept the fact that it may never gain an interface-like feature, and also trust the core team to add it in a satisfactory manner if it does get added at all.
When you learn Zig and you have an OOP background, the lack of native interfaces seems like a big limitation and the way interfaces are implemented “by hand” will seem hacky. But, as is demonstrated by the Allocator, Reader, and Writer interface implementations, they work and are quite robust. They even allow for optimizations afforded by knowledge of the internals of the problem being solved, which you can’t get with a geeral purpose language-level interface implementation.
Look at C for example. It doesn’t have interfaces either, but you will find many solid patterns developed over the years that many libraries employ with success. I think Zig, in this case as with many others, takes a page from the C way of doing things versus the OOP way.
Also, I believe it was Richard Feldman (Roc language creator) that mentioned the fact that most of the time in OOP languages you “program to an interface” thinking that some day tons of people will use your interface and extend it in infinite ways. The reality is that most of the time it’s just you alone that ever uses your interface and in that case, you can get by with a closed interface-like API such as Zig’s tagged unions just fine.
It took Go 12 years to add generics to the language, before it had awkward code generation things instead.
I found Go to be dreadful with the amount of repetitive re-implementation and awkwardness without generics, but since it got generics the language has become a lot more interesting to me. However Go and Zig have very different goals and focus.
One part about Zig that I love is that it doesn’t just pick some solution that satisfies 90% of people and puts it into the language and tries to do things more from a first principles perspective, instead of just inventing fancy abstractions.
I think choosing something like Go or JavaScript, which both are a lot more abstracted from actual hardware, is fine and has its benefits, but these languages make tradeoffs for the user, that I no longer want to just accept and live with. Zig tries to give more choice to the user, by avoiding to fuse all kinds of tradeoffs right into the language, with no possibility for the user to have any way of providing their own implementation or avoid using it.
For people who don’t particularly care how interfaces are implemented and just want to use them, that doesn’t matter and is a waste of time, but for people who do care about that, it is what makes Zig interesting.
I like interfaces, I also think it would be good if there was wide compatibility between different libraries so those can work together easier. Maybe there is a place for a specific interface implementation for Zig, but I think mostly it isn’t the right time for Zig to think about that, there are other things that have to be figured out how they should work and my guess is that once those are figured out and implemented, they will form a landscape that allows those involved with the language design to develop a vision for whether and how interfaces could really fit into the language. Without Zig loosing its way of doing things and just becoming more of an imitation of go. If interfaces can’t be added in a satisfactory way, then I think it is better to leave them out.
I don’t like inheritance and I don’t like classes or OOP, at least with prototypes JavaScript had its own identity, but then it just buckled to peer pressure and now it seems to go down the c++ route of trying to add every possible feature to the language, which just makes it a way to giant language with too many different ways to do the same thing, it lacks clarity, elegance and vision.
Personally I would find it more interesting if JavaScript had gone down the route of Self and tried to make something that actually uses the prototypes to their full potential, but sadly we will never see this, because too many mainstream programmers are stuck with kitchen sink language addiction, instead of thinking in new ways and adapting to the language and how it was designed to be used — the languages just get hijacked (by business people who don’t care about language design) and get features added until everyone can do everything with them in mediocre ways.
I do think that interfaces are useful, but there are also a lot of different ways to implement them and use them and a lot of cases where you could design your code differently, which might not actually need interfaces, could even benefit from not using interfaces. For example if it makes you implement things in a data oriented design way, basically fitting the hardware and making good use of caches.
I like that Zig follows its core values, instead of muddling its clarity by just adding features.
Personally I think Zig might be better off staying a fairly minimalist language.
I think one way that could be interesting is if Zig tried to create/support a way for having modules, that are written in custom languages which integrate with Zig (are importable, compile the same way Zig compiles, share values).
To some extent that should already be possible with the buildsystem, but I imagine there would be ways to improve interoperation.
Racket has a #lang <language> at the top of every module, it allows you to write your own implementation for these and then you can use these modules written in different languages together (and share values with contracts (runtime type checks) between them (typed/racket also has static types)), you can have a part that is some datalog implementation use that in a normal racket program, write books with #lang pollen etc.
The problem with racket, is that you are sitting on a tower of macros and the lowest level is racket, which is a gc language. Starting from a more minimalist non-gc language and building something similar, I think/hope could get us closer towards the idea of what racket calls Language Oriented Programming, without the complexity of big macro systems or unavoidable gc.
So to say it another way, I would be fine with Zig to stay minimal and then use the buildsystem to combine Zig with other languages, some of which might be like GitHub - marler8997/zigscript, I don’t think one language needs to have all the things.
I think what makes Zig great is that it is becoming a great foundation that many languages can be built upon and with that, we could actually get to a place where ideas are both expressed in a language that fits the problem and compiles down to something that performs well.
While a discussion about interfaces is legitimate, it’s important to keep in mind that those “hacks” is how interfaces actually work in all languages under the hood.
The comparison with JS inheritance might make sense from a usage perspective but past that it’s fairly inappropriate because inheritance is in on itself an abstraction very far removed from the basic operations that the machine exposes to you, while pointer math (@fieldParentPtr), vtables, tagged unions, etc, are much closer to the fundamental operations that the machine can perform.
There’s a big mentality shift required to go from a high level programming language to Zig. My recommendation is to keep questioning design ideas but make sure to read (and program) as much as you can because a lot of “conventional” programming wisdom doesn’t apply once you decide to concern yourself not only with the linguistics of how to express ideas, but also with their mechanical implementation.
For me (as a C programmer) everything is simple.
What is an interface (or trait or set of virtual methods in “usual” OOP) after all?
It’s just some number of function pointers and that’s all.
What does it mean “something implements this or that interface”?
It means that those pointers point to some functions which perform actions implied by an interface.
I would say in C/Zig we are not “emulating” interfaces, we are constructing them.
It’s other way around - languages like Go try to give a programmer more syntactically/semantically consistent/convenient way to do what can be done in C, hiding under the hood all that function pointers machinery.
At a fundamental level it little matters whether a language has interface/implements keywords or not. If it has - ok, I will use them, if it hasn’t - no problem, I will construct interface-like stuff manually. And in Zig we can do this in 5 different ways for the moment, it’s a bit more freedom in design decisions, imo.
I do think there’s a missing piece of the language here. But it’s better for it to be missing than to fill it with the wrong thing.
If you want to get a handle on why Zig doesn’t have direct language support for interfaces, I suggest reading the issues on the subject. Some are closed, some are still open, every time I go spelunking I find new ones. I don’t want to color your perceptions by linking to any one issue, if you start with obvious search terms and follow the breadcrumbs, there is much to learn.
A solution has to be compatible with Zig’s philosophy. The barrier here is that Zig doesn’t do magic: it’s necessary that authors and readers of code be able to understand what the program is, and does, on a literal level. This is the main criterion which existing proposals have failed at meeting.
When you drill down into how some interface proposal would be implemented, so far it ends up being summarized as (one example) “syntax sugar for a fat pointer”, ‘fat pointer’ here is one pointer to data and another pointer to a vtable, which is a struct containing pointers to functions. Is this something we need sugared? Not really. That would give a leg up to beginners, but it obscures what’s actually going on. If you want to make a fat-pointer interface, you can do that now, and it isn’t the only option for interface-y stuff.
There would need to be a reason for the language to support this as a primitive. Example: the language has tagged unions, which is something which can be constructed manually with sufficient effort. But userspace can’t implement coercion from a tagged union to its enum type, or the way that cooperates with switch statments, and those are valuable properties which we’re happy to have. So the language supports this as primitive syntax.
I see real pain points with the status-quo solution to runtime polymorphism, but so far, no one has a proposal to alleviate those which really fits the language. If and when we get one, it will be written by someone who is familiar with the existing proposals, knows why they’ve either been rejected or not implemented, and deeply understands the various mechanisms which are currently used to provide this feature in Zig’s status quo. Any such proposal has to be readily comprehensible as a mechanism, meaning that it reveals, rather than conceals, the actual object code generated by use of that feature. And it will have to justify why language support is needed to implement it.
I believe this is possible, but it isn’t easy. As @kristoff points out, the available ‘raw’ mechanisms for runtime polymorphism are hand-rolled implementations of the choices other programming languages make for you. Zig is all about hand-rolled code, so I encourage you to treat this as an opportunity to deeply understand those mechanisms by implementing them yourself.
Just have to chime in and say how strongly I agree with this sentiment.
On this topic of interfaces, there are basic tools that would often suffice. I’ve seen many instances where a direct function pointer would be the most elegant solution.
That said, I’m not hardcore against interfaces but as has been mentioned, I hope Zig stays course and continues to prioritize strong fundamental types with good defaults. If interfaces fit in with that mentality, I can see a place for them.
The above code will fail to compile if in the cal ltree. For some, the pain is real! “No! All dogs know how to bark!” For others, every dog breed bark is unique. Both approaches are ez.
const Creatures = struct { cats:[Cat], dogs:[Dog] }
pub fn vocalize(creatures: *const Creatures) void {
//iterate across cats, cat.meow, dogs, dog.bark ...
//or MultiArrayList a more generic creature.
}
I think the big thing is that interfaces represent a deferral of thought. “I don’t know all the things, but I know this thing, so Interface now and worry about the rest after the next milestone is delivered.” “How can I know what I don’t know?” ahh…
It has some merits - definite pros, namely I don’t talk to you, you don’t talk to me, here is this thing, take it or leave it. However…it has cons (I dunno you, you dunno me), and for me. those negatives often outweigh the positives.
It’s like reading a book about someone else’s dreams sometimes, crawling into someone else’s head esp when someone has been drilled it’s all an interface everywhere.
For me, a system really needs to know all the things. zig to me, offers a straight line from a->z->a…just as C does, but without ALL the overhead C requires.
Interfaces do hide things that need to be known, and in some cases, that can be catastrophic as those hidden things can be super problematic (“Hello, mr user loaded object, why, of course, you may have unauthorized access”).
But not having interfaces does mean thinking more about the need, now as we can’t…defer…thinking to later. (well comptime, and some reflection, means it’s a more elegant focus vs past langs, and who says we ever have to think ).
Thinking more now can be harder…but to me, that straight line from a->z->a…the clarity is worth the time.
I am coming from an OOP / interface background and always hated it. Interfaces the most (the Delphi and later C# type of interfaces). I never used interfaces during my entire life
Rust was too restrictive for me but I really love the cleanness of Zig as it is now.
The simpler the better in my opinion.