-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
comptime interfaces #1268
Comments
I don't get this. An interface isn't a concrete thing I'd say, so it doesn't make sense to have it as a member.
Maybe, although if the purpose it to make sure I'd say write a test that tests this. It might be good for documentation purposes though, but it can also add clutter. |
@BarabasGitHub you could embed a vtable which would just be a struct of function pointers. That is essentially what we have in Zig now. Then, with Plan 9 struct embedding, at least |
it is difficult indeed because you may end up having some struct in place of that interface
this is not something very important and as said, it should be left out initially and only eventually added.
yes, as said its a non issue, there are many ways to solve is and its not hard, pick your poison and just use one in the end |
you would not need a vtable cause all functions, even the embedded ones would be known at compile time unless im terribly mistaken this proposal is all about really not magical really straight forward compile time checking not any wizardry/ pointers involved |
Again, there is a fine line between type constraints and interfaces. Interfaces are runtime-specific. An interface allows me to load some blob into memory and know how to call a method because I know how to access the vtable. The compiler emits the code to make this function call without knowing the underlying type of the object. Imagine the following C++ code:
godbolt.org using clang-trunk emits this output (see more here: https://godbolt.org/g/2WNNBU):
|
this is a different example and does not apply to this proposal in your case the issue is that, feedAnimal does not know which method it has to call because it does not know which type its passed. Is it a dog, cat .... its just any animal. But with this proposal you know exactly what struct you pass and know all functions on that struct so there is no issue. can you provide the same example with the proposed interfaces and create a case where its not crystal clear what function to call at compile time? I can not. |
Just for the sake of argument, you can do something like this with currently existing Zig:
|
@tgschultz thanks for the concise example which is on point The question is, why isn't it done this way in the std? Maybe because its extremely tedious to type (and actually read as well) for something that should be strongly encouraged rather than made harder and thus discouraged? For me this just shows how easy it would be to implement in the compiler and what a very small addition can bring as a huge benefit to all user facing code. |
Well we didn't have @typeinfo until relatively recently, for one. But now that we do we've been working on std.meta and maybe something like this should be a part of it. That'd alleviate a lot of the typing hassle while still providing most of the benefit provided by handling the concept in the language itself. To me, that's a worthwhile tradeoff, others may legitimately disagree. |
a tradeoff between typing and then reading multiple times lots of boilerplate VS having one more keyword that is extremely unobtrusive? I get that many just want good old c but good old c did not have any of this meta magic either and I prefer a clear syntax instead of dozen lines of meta programming unless I want to play games and feel creative which may be the case from time to time. There is its own reward in understanding some meta meta code after staring at it long enough. One keyword more is certainly closer to c than typeof, typeinfo etc. etc. chained together in some artistic way. |
If something like
to
|
@monouser7dig I am saying this is precisely the problem with using the word interface in this proposal. Also, I'm not sure why the use of comptime type variables doesn't solve most issues for compile-time type constraints. |
OK thought this over and realize what you are asking for is along the lines of C++ concepts. Look at that and see if similar idea could be used in Zig. Edit: here's the link: https://en.cppreference.com/w/cpp/language/constraints. |
quote: Interfaces / Traits (the terminology is left to the reader / implementer) Not a huge fan of comparisons to cpp, I mean what @tgschultz outlined pretty precisely so don't think there is value in adding ambiguity. @tgschultz I very much like your last example and would very much like to see this |
In the beginning I mentioned the current Allocator struct as a bad example without yet providing a 100% coverage of what it achieves, namely default functions.
This can be done with this proposal as well, but much easier for the reader and writer. another gotcha for the implementation of this meta programming facility is: This must be rigid and not have weird edge cases but if people insist meta programming can solve this I'm curious to see. |
One thing to consider with a Now you could of course check any assertions made in the function and document those, or just do it manually, but I feel like this is adding a lot of non-obvious knowledge to the language just for the sake of being smaller. Adding a new feature doesn't necessarily make the language more complex and I feel having these static assertions specified as part of the function signature could be quite beneficial in this regard. One downside of this I can in regards to consistency is that we cannot specify a function as satisfying a primitive operation (e.g. |
Yes documentation is one downside I also thought about. If you have interfaces using some keyword you could klick through some html docs and get to that interface, quite a huge benefit.
I do not think this is a huge issue because code that relies on + - etc. can not be really generic anyway because you can not implement them yourself. |
@monouser7dig I am sorry I had missed the title. But you have to choose interface or type constraints/concepts, not both. As the example I showed with I don't know what you mean by "not a fan of comparisons to cpp." Whether or not you are a fan of them, C++ had literally the exact same problem with templates. From what I understood, the main issue here is that using |
Yes, that’s why this proposal is strictly about comptime.
I mean that I can not follow your arguments just because they seem to handwavy. To say again: I think at this pint here that comparisons to other languages like cpp only help if they are actually applicable and not just „something I saw somewhere“. |
I meant without this improvement zig doesn't have good error messages for comptime types. |
Ok I missread your comment. Sorry, thanks for the follow up! |
A good use case for this sort of comptime-interface, which I will refer to as If we started using this pattern frequently, then a stricter form of checking might be desirable. My proposed way doesn't look too bad:
But I think @tiehuis is right that if this sort of thing is desirable and having language semantics around it would express intent more clearly then we should do that. |
I don’t like how you have to explicitly implement that trait struct. Having ducktyping like in Go and like proposed in the beginning with an option to have explicitly checked compliance to an interface is the way to go IMO. |
I did that because we wouldn't want |
So that would be another reason to have this as a language features besides readability and documentation generation? As an aside: I like traits just as well as interfaces, I don’t care about the naming at all (maybe I should but ... just call it ziggies 😆 whatever) |
@monouser7dig I think the main issue here is you want to have Go-style interfaces, but you are using templates/comptime types to accomplish that goal. Go-style interfaces will probably not be possible in Zig due to the overhead that would incur. |
I argue go style interfaces are possible at compile time (for the parts I care about/ outlined here). |
Old article but probably still true about Go interfaces:
|
My 2 cents. The main argument against implementing comptime interfaces is that they can allready be done via comptime asserts. The primary arguments in favour is clearer type signatures for bother readers and (more importantly) tools trying to understand the code. Naturally people are suggesting some special syntax to call the type asserts in the signature. But this is a huge breakage of Zig's "no hidden control flow" and "one obvious way to do things" mantra as it's an alternate way to call a function. Ideally we could just call functions in the type signature. One possible solution to this is to remove The result is pub fn read(reader: anytype, action: fn(line: []u8): void) void {
...
}
pub fn read(reader: @typeOf(reader), action: fn(line: []u8): void) void {
...
}
// this is type checked as @typeOf(std.io.Reader) == @typeOf(std.io.Reader) which passes.
read(std.io.getReader(), ...); This might seem recursive but conceptually Personally, I think I might even prefer this syntax to We can then write something along the lines of fn Is(interface: type, x: @typeOf(x)) @typeOf(x) {
if (!std.meta.traits.is_a(interface, @typeOf(x)) {
@comptimeError("{} is not a {}", @typeOf(x).name, interface.name);
}
return @typeOf(x);
}
pub fn read(reader: Is(Reader, reader), action: fn(line: []u8): void): void {
...
} Of course, this has some serious implications and I'm sure someone can give examples of it being problematic. In particular idk if this works with type coercion since that allows the value's type and the variable type to be different. Perhaps a built-in to refer to the value would be necessary. What appeals to me is that it doesn't introduce any new syntax, infact it could even get rid of a keyword. Edit it's occured to me if this was the case type inference is possibly just a short hand for const x = 5;
const x: comptime_int = 5;
const x: @typeOf(x) = 5; are all equivalent. Therefore there's already a bit of a precedent for this syntax. Perhaps even an argument for the same shorthand elsewhere. |
How the interface is currently implemented: `// interface //Example I hope in the future zig can write code like this: var e = A{}; The point is to transform A->I. The compiler should do the checking automatically, it's the compiler's responsibility. Instead of letting the user add glue code. It is the user's responsibility to have the code written in a way that everyone agrees with. Declarative. The interface should keep the zig open style, so it has no hidden members, making it a vtable. the most important things. |
@hryx To format a block of code on GitHub you need to use three backticks instead of one, and they should be on their own lines. You can also provide a language name to get syntax highlighting.
|
This is a terrible idea in my opinion and goes against one of Zig's core principles: being a simple language. As others have said like @Calum-J-I's more in-depth post above, Zig's first class typing and compile time programming model already provide a solution to many type-based ailments. If you really want, you can write functions to check for specific properties of types if you wish (as can be seen in All of that type checking functionality is not even needed in most cases though as Zig as it stands right now is duck typed. "Interfaces" are implicitly defined by the code itself, much like how languages like Python are written, or C++ before C++20's concept insanity, usually called something like the "type requirements" of a function. These requirements can be listed in a comment or standardized in other documentation, requiring nothing on the language side to change, and many things in Zig are designed this way already. Interfaces are merely a redundant way to express this information encoded in the codebase itself, and while they can help for a bit of planning about future functionality that may not yet be called in the code, they essentially do not add anything of value while merely complicating the syntax and making it that much harder to write code (whereas right now in Zig you don't need to do anything special to implement an interface). I for one can say I would stop using Zig if this feature was implemented, I do not think the language should be striving to simply implement every flashy thing in other languages as this will only make it as complex as these other languages in the end (and these languages are what myself and others came to Zig to get away from). Instead it'd be better to focus on what is possible with the type system right now and change the way you program to suit Zig, not the other way around because Zig is not the same as these other languages. Also to address the points the OP post brings up saying why this is a "bad" idea:
|
To me the use case for making fine-grained size/performance tradeoffs with less code to write (at cost of higher compiletime) sounds reasonable. Why I am not sold yet: The compilation increase of comptime vs the gains are not evaluated and approximated yet and baking stuff into the type system or suggesting to users in libstd, which does not scale/its unclear how it scales, sounds like a bad idea. So far there is https://github.com/mov-rax/zig-validate with ~1k LOC (including tests) and https://github.com/alexnask/interface.zig ~500 LOC (including tests).
|
As @matu3ba points out, there are numerous libraries that try to solve this problem. He linked to two of them, I'm aware of 3 others (some old and code-rotted). To me, this is an argument for the language to define this. I don't think the "don't complicate the language" argument holds weight here. It's already being done all over the place, including the I also don't understand the "you can do this yourself" argument either. Everything in Having a standard way to do this would improve:
Weather it's a new language keyword, or a new builtin , or a conical imp in I guess you could argue "passing |
It would be ideal if the compiler and zls could display a full list of the functions required by an If there's not going to be interfaces, the compiler needs to fill the gap I think, otherwise we have to read the code of a function, and read the code of it's calls, and so on, to understand what the type should be. If the compiler can do this, it's basically an autogenerated interface, at which point do we need hand crafted interfaces? |
My application is a small toy project only worked on by myself, I'm using a tagged union and dispatch with a switch. But that doesn't scale well. My experience was: it's hard to find this snippet of code. I'm not entirely sure whether it's the right way to go. And there's many dead end git discussions that are confusing. People seem to enthusiastically apply zig to all kinds of things, UIs, game engines, etc. I think even in the carefully carved niche of Zig (better C, not C++) some form of polymorphism is important.
I'd like to add another reply to "this will make the language more complicated": Until you want dynamic dispatch, you just don't care. But once you do there's a clear standardised way to do it, and some degree of compiler support. |
I would really like comptime interfaces in zig. It is quite complicated to work with function parameters of objects which share methods or properties. Even more so if they are generic. Assertions are not the same as you need to add the calls which you can easily forget and you have to duplicate this in worst case. Moreover you can't see from the function signature what the function accepts then. I think structural typing as in Go might be preferable. |
As I can see, the good first step towards solving the issue would be to peek the best available implementation of comptime interfaces and place it into |
This can be addressed by simple comptime function. Something like on call site: const MyReader = Implements(IReader, struct {pub fn read() void}) The implementation of |
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. |
Hi ! I'm just discovering Zig wich seems awesome, well done guys ! fn foo(param: anytype) {
//...
}
foo(//At this point there is no autocompletion); But if we have no interface syntaxic sugar in Zig, how else could we have autocompletion for anytype ? (without adding a payload at runtime of course) I'm saying this because I think autocompletion is really important for the developper, and I see that you already use a lot of syntaxic sugar in Zig (defer, try, ...) and people seem to enjoy it, so why not for interface ? PS: When I say "interface syntaxic sugar" I have this in mind : const Allocator = interface {
fn create(self: Allocator, comptime T: type) Error!*T;
fn destroy(self: Allocator, ptr: anytype) void;
}
const Page_allocator = struct implements Allocator {
fn create(self: Allocator, comptime T: type) Error!*T {
//...
}
fn destroy(self: Allocator, ptr: anytype) void {
//...
}
}
fn foo(allocator: Allocator) { //Replace anytype by an interface
//...
}
foo(Page_allocator//Now we have autocompletion !!!); |
@xdBronch thanks for your answer, I'm gonna find out about ZLS. By the way, I don't understand why std lib memory allocators implements std.mem.Allocator interface (with vtable). fn foo(allocator: anytype); // instead of expecting std.mem.Allocator as a type |
@hugoc7 That is done to reduce the amount of code generated, every instantiation of a generic function will duplicate code in the binary whereas a runtime polymorphic interface will not. Of course this has a performance cost and personally in my own engine I use |
I'd also note that the standard library uses anytype for readers/writers but not allocators because you might need to read/write single bytes at a time in a hot loop, and you really don't want to pay for an indirect call every time. But you should never be calling allocators that frequently, you should allocate beforehand in bigger chunks instead. If your allocation might have to request memory from the OS (which is usually a possibility unless you're using a fixed buffer allocator, or reusing a warmed up arena), then the overhead of the indirect call is insignificant compared to that. |
Completely misunderstood and very needed proposal, confused why zig has async road-map but this was closed. Zig's many good design choices vastly outweight absence of comptime interfaces/traits (whatever you want to call them) so we will all continue using it. But as people pointed out anytype has very complicated autocompletion, no explicit constraints, hard to define interaface for multiple implementations, allocator in stdlib is a great example of why we should have this. Anytype creates "wild west" that reminds me of C pre-processor (obviously not nearly as bad, but still is not very explicit). I like the thing that zig maintainers like to repeat about code being mostly read by other people. In that regard seeing "uart: anytype" is very obscure compared to "uart: CmsisUartInterface". As Andrew pointed out this proposal is very inconcrete, but it clearly expresses the need for some feature to fix the problem. Ghost is MVP for defending this. The only argument for this not to be implemented is that core team writes thousands of lines without this so it is probably not that bad. Maybe we will see something done in #17198 |
Appreciate your cautious approach. Now I propose that zig can refer to the implementation of go interface. Because it looks very similar to anytype (I mean for normal interfaces, not as complicated as generic constraints). If an object implements all methods defined by the interface, it implements the interface. This idea is beautiful and simple, and this can be determined through static analysis. Why do we need interfaces? The main purpose is to let customers know what they need to provide. This kind of communication is necessary. It is for customers, not for us to construct a complex type system. I like simple things, simple concepts. |
i was writing an stm32 hal library and came to a point where i want to support multiple series like u5x and f4x. So i wanted some kind of "promise" to higher level libraries so that driver layer could be written without any specific implementation in mind AND most importantly since i know everythig in compile time I wanted to evade having vtables on MCU where it is actually a downgrade from C where one would just define macros for example. So i managed to fix it in a non-trivial way by not using this approach at all. Then again they (Andrew and co.) write gpu and compiler code so I guess if it was very needed ot would be in the language already. |
Proposal on comptime Interfaces / Traits (the terminology is left to the reader / implementer)
motivation
Zig currently has very bad code for expressing some sort of shared properties.
Look at mem.Allocator and heap.xyz for the implemenations if you have not already. (there are other places where this smells of course as well)
Notice the use of @fieldParentPtr and function pointers which make the code hard to read and unnecessarily inefficient by adding pointer indirections.
current state of the art
Currently Zig has compile time ducktyping.
However, one can not easily restrict input to a certain class of inputs. This yields bad compile error messages and very bad readability as explained in the following:
An example: a func that takes some struct with method foo() on it and calls foo(). If you pass in a struct that does not have foo() you get an error, possible deep down the hierarchy which bubbles up out of some library. This error should be brought up at the immediate calling position where you passed your struct.
Even more importantly its bad for documentation because its not clear to the reader what kind of
var
that func takes. It is much better to express that it takes a struct that has a fucntion foo() by using interfaces.explanation
A missconception is that interfaces have to happen at runtime and or involve some notion of vtables.
As proposed, interfaces are just compile time checks on a type.
These improve performance because methods are just like regular methods on structs, not fucntion pointers that require an additional lookup.
syntax for interfaces should clearly be
You can pass any struct to some_func that has a method foo() on it.
To be discussed:
I think all those should be possible but i'd actually recommended to do a first implementation without those three because they can be added later without issues.
This proposal should go along with #1214 making inheritance obsolete by using clean composition.
Also see #1170 (comment) for extremly positive effects of this proposal.
There have been proposals for hacking interfaces using metaprogramming which would have bad consequences because error messages will suffer, everyone will roll their own incompatible imeplentation and as shown above interfaces offer an indespensible tool for structuring code.
If you just want to write c, you can ignore this proposal but your code will be harder to read for most people, harder to debug, refactor(see 2nd link) and possible inefficient.
thanks for your attention 👍
The text was updated successfully, but these errors were encountered: