Skip to content
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

Closed
ghost opened this issue Jul 20, 2018 · 82 comments
Closed

comptime interfaces #1268

ghost opened this issue Jul 20, 2018 · 82 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@ghost
Copy link

ghost commented Jul 20, 2018

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

const some_interface = interface {
    fn foo () i32;
    some_int: i32;
}

fn some_func (i: some_interface) {
    i.foo ();
    // ....
}

You can pass any struct to some_func that has a method foo() on it.


To be discussed:

  • a) interfaces can allow to specify member variable as well as functions
  • b) interfaces can be used as a member variable that has a variable that has the inteface
  • c) explicitly implement an interface, sometimes you want to make sure your custom_allocator actually implements the allocator interface, there can easily be added a small syntax to ensure this or a small hack like in golang using an anonymous variable, its not a big deal.

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 👍

@BarabasGitHub
Copy link
Contributor

b) interfaces can be used as a member variable that has a variable that has the interface

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.

c) explicitly implement an interface, sometimes you want to make sure your custom_allocator actually implements the allocator interface, there can easily be added a small syntax to ensure this or a small hack like in golang using an anonymous variable, its not a big deal.

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.

@isaachier
Copy link
Contributor

@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 stdout.stream.print could be reduced to stdout.print.

@ghost
Copy link
Author

ghost commented Jul 20, 2018

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.

it is difficult indeed because you may end up having some struct in place of that interface

const a = interface {}

const s = struct {
a_member : a;
}

some_s : s;
// ...
s.a_member.foo; // you never know if a actually has .foo so you would always have to assert on runtime 

this is not something very important and as said, it should be left out initially and only eventually added.

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.

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

@ghost
Copy link
Author

ghost commented Jul 20, 2018

@isaachier

you could embed a vtable which would just be a struct of function pointers.

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

@isaachier
Copy link
Contributor

isaachier commented Jul 20, 2018

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:

class Animal {
  public:
    virtual ~Animal() = default;

    virtual void eat() = 0;  // Pure virtual/abstract method
};

class Dog : public Animal {
    void eat() { /* some implementation here */ }
};

namespace {

void feedAnimal(Animal& animal)
{
    animal.eat();
}

}  // anonymous namespace

int main()
{
    Dog dog;
    feedAnimal(dog);
    return 0;
}

godbolt.org using clang-trunk emits this output (see more here: https://godbolt.org/g/2WNNBU):

...
(anonymous namespace)::feedAnimal(Animal&): # @(anonymous namespace)::feedAnimal(Animal&)
  push rbp
  mov rbp, rsp
  sub rsp, 16
  mov qword ptr [rbp - 8], rdi
  mov rdi, qword ptr [rbp - 8]
  mov rax, qword ptr [rdi]
  call qword ptr [rax + 16]  ; this line is calling a function pointer in the vtable
  add rsp, 16
  pop rbp
  ret
...

@ghost
Copy link
Author

ghost commented Jul 20, 2018

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:

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.
This again needs more thought once you allow member variables that have the type of an interface but we are not talking about this yet, that is not as important as the proposal itself.


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.

@tgschultz
Copy link
Contributor

tgschultz commented Jul 20, 2018

Just for the sake of argument, you can do something like this with currently existing Zig:

fn ifaceAssert(x: var, comptime I: type) void {
    comptime T = @typeOf(x);

    //uses @typeInfo to compare I's members and defs to
    // T's, throwing a compileError if any part of I is missing
    // from T.
}

const BarInterface = struct {
    data: []u8,

    //for comparison purposes a parameter with type
    // of the interface is considered as "SelfType",
    // which is to say that it matches the target type
    // of the comparison.
    fn bar(self: BarInterface, usize, []const u8) void;
};

fn foo(x: var) void {
    ifaceAssert(x, BarInterface);

    x.bar(10, "ABCDEFGHIJ");
}

@ghost
Copy link
Author

ghost commented Jul 20, 2018

@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.

@tgschultz
Copy link
Contributor

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.

@ghost
Copy link
Author

ghost commented Jul 20, 2018

To me, that's a worthwhile tradeoff

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.

@tgschultz
Copy link
Contributor

tgschultz commented Jul 20, 2018

If something like ifaceAssert were in the std lib, then it wouldn't be dozens of lines on the users end. Compare:

const some_interface = interface {
    fn foo () i32;
    some_int: i32;
}

fn some_func (i: some_interface) {
    i.foo ();
    // ....
}

to

const SomeInterface = struct {
    fn foo (SomeInterface) i32;
    some_int: i32,
};

fn someFunc (i: var) {
    std.meta.ifaceAssert(i, SomeInterface);
    i.foo ();
    // ....
}

@isaachier
Copy link
Contributor

@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.

@isaachier
Copy link
Contributor

isaachier commented Jul 20, 2018

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.

@ghost
Copy link
Author

ghost commented Jul 20, 2018

I am saying this is precisely the problem with using the word interface in this proposal.

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

@ghost
Copy link
Author

ghost commented Jul 21, 2018

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.

If you implement three basic functions you get the rest for free by calling parent functions by suing parentPointer.

This can be done with this proposal as well, but much easier for the reader and writer.
Every function that is implemented in terms of the basic allocator interface (which consists of those three functions) can be turnt into a free function that takes as first parameter a var and asserts that ist an allocator (see #1268 (comment) ) and then takes the usual arguments. It can then call/ get/ use the three basic allocator functions by using the ones provided by the struct that implements the allocator interface.
The only difference at call side is foo(some_struct, other_param1, other_param2) vs currently some_struct.foo(other_param1, other_param2) and there could be syntactic sugar to make both equivalent as its currently the case with struct methods already. (not sure this would work already in this exact case but its not a big deal anyway)


another gotcha for the implementation of this meta programming facility is:
Its important that this (#1268 (comment)) works with struct embedding (#1214) as well. Such that the interface can be satisfied by a struct that is embedded.

This must be rigid and not have weird edge cases but if people insist meta programming can solve this I'm curious to see.

@tiehuis
Copy link
Member

tiehuis commented Jul 21, 2018

One thing to consider with a var and a comptime check in the function that it satisfies the constraints is that documentation is much less obvious just from the function signature which I consider a significant downside to the 'clarity' tenet of Zig.

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. +, -) so these would still need a var anyway.

@ghost
Copy link
Author

ghost commented Jul 21, 2018

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.

One downside of this I can in regards to consistency is that we cannot specify a function as satisfying a primitive operation (e.g. +, -) so these would still need a var anyway.

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.
This is thus only relevant for functions that would like to take primitive builtin types and in those case typeof does suffice IMO. I do not think that is a huge use case for interfaces.

@isaachier
Copy link
Contributor

isaachier commented Jul 22, 2018

@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 feedAnimal illustrates, comptime vs. non-comptime problems are two completely different problems.

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 comptime types leads to ambiguous error messages. That is why C++ starting developing concepts in addition to templates. Templates lead to verbose error messages that could go on for pages without much clarity to the user.

@ghost
Copy link
Author

ghost commented Jul 22, 2018

Comptime vs non comptime are completely different

Yes, that’s why this proposal is strictly about comptime.

not fan of comparisons to cpp

I mean that I can not follow your arguments just because they seem to handwavy.
There is no reason I can see myself or in your arguments that this proposal would yield bad error messages like cpp, quite the opposite, it would strictly improve current error messages as explained in the very beginning.

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 tried my best to give very explicit examples how this proposal will improve code in various aspects and in case there are objections I‘m more than happy to talk about them precisely.

@isaachier
Copy link
Contributor

I meant without this improvement zig doesn't have good error messages for comptime types.

@ghost
Copy link
Author

ghost commented Jul 22, 2018

Ok I missread your comment. Sorry, thanks for the follow up!

@andrewrk andrewrk added this to the 0.4.0 milestone Jul 22, 2018
@andrewrk andrewrk added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Jul 22, 2018
@tgschultz
Copy link
Contributor

tgschultz commented Jul 25, 2018

A good use case for this sort of comptime-interface, which I will refer to as trait to differentiate it from runtime-interfaces, came up in regards to #1291 recently. In this case, the problem is that the runtime-interface pattern (which is a pointer to a struct of function pointers using @fieldParentPtr()) causes the std.mem.Allocator interface to be unusable at comptime. However, if we were to change the idiomatic allocator handling from: fn foo(allocator: *Allocator, ...) to fn foo(allocator: var, ...) and use duck-typing, then we could pass anything that even looked like an allocator to foo, including the current Allocator interface. It is worth noting that this is how @andrewrk implemented allocators for async<>.

If we started using this pattern frequently, then a stricter form of checking might be desirable. My proposed way doesn't look too bad:

////std.mem
const Allocator = struct {
    const Traits = struct {
        //this actually doesn't work currently because T in the return is
       //not handled correctly
        const alloc = fn(self: *Traits, comptime T: type, n: usize) ![]T;
        const realloc = ...
    };
    ...
}
////
fn foo(allocator: var) []Bar {
    std.meta.hasTrait(Allocator.Traits, allocator);
}

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.

@ghost
Copy link
Author

ghost commented Jul 25, 2018

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.

@tgschultz
Copy link
Contributor

tgschultz commented Jul 25, 2018

I did that because we wouldn't want std.meta.hasTrait() to check every field and definition in Allocator, only the ones necessary to satisfy what we care about (namely alloc, realloc, create, destroy, and free). Another way to implement it would be to have std.meta.hasTrait() check only public definitions, but then you can't have fields as traits (unless we add public/private distinction to fields too).

@ghost
Copy link
Author

ghost commented Jul 25, 2018

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)

@isaachier
Copy link
Contributor

@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.

@ghost
Copy link
Author

ghost commented Jul 25, 2018

I argue go style interfaces are possible at compile time (for the parts I care about/ outlined here).

@isaachier
Copy link
Contributor

isaachier commented Jul 25, 2018

Old article but probably still true about Go interfaces:

Languages with methods typically fall into one of two camps: prepare tables for all the method calls statically (as in C++ and Java), or do a method lookup at each call (as in Smalltalk and its many imitators, JavaScript and Python included) and add fancy caching to make that call efficient. Go sits halfway between the two: it has method tables but computes them at run time. I don't know whether Go is the first language to use this technique, but it's certainly not a common one. (I'd be interested to hear about earlier examples; leave a comment below.)

https://research.swtch.com/interfaces

@matu3ba
Copy link
Contributor

matu3ba commented Jan 30, 2022

Traits allow more computationally efficient type checking than comptime duck typing

https://rustc-dev-guide.rust-lang.org/traits/resolution.html#selection-during-translation yes, though once you compute a bit more complex things in logic programming at comptime ie for interface configuration with loops, this inverses the advantage.
Due to technical reasons as of now Rust also does not compute with the exact set of queries upfront (chalk hopefully will fix this).

I think @ArborealAnole proposed a more simple solution, where the interface only describes the basic variants (static, dynamic dispatch and some more complex methods to allow a few more pointer and object checks).

From a third position in for example PLC code the code can be generated from logical derivation of the ladder diagram, although as far as I know, it does not retain the information how to do the reverse step of reconstructing the ladder diagram.

From my perspective, combining comptime and logical derivation would require a convention to construct at comptime a logical program (encoded somehow in AIR) that can be optionally solved with (external) static analysis by combining user-annotations.
However, personally I think that first there must first exist (very) convincing performance numbers + use case analysis to justify the significant complexity and maintenance.

@thislight How is an interface pointer different from a well-known comptime-fn or struct in libstd? Reserving the extra keyword does not look like it has additional advantages, since the user-defined struct (as convention) does the same.

@Calum-J-I
Copy link

Calum-J-I commented May 24, 2022

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 anytype from the language but expose the variable being assigned a type to the type expression.

The result is anytype becomes

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 reader is just a binding to whatever the read function was called with. The only time a variable is declared without a value to consult, at least afaik, is when that value is undefined which of course coerces to all types anyway.

Personally, I think I might even prefer this syntax to anytype, but that's just me.

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 x = @typeOf(x).

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.

@htqx
Copy link

htqx commented Jul 16, 2022

How the interface is currently implemented:

`// interface
const I = struct{
impl: * anyopaque, // direction A
addFn: fn (* anyopaque,u8) u8, // actual interface
pub fn add(self:I, b:u8) u8{ // helper function
return self.addFn(self.impl, b);
}
};
const A = struct {
data:u8 = 0,
fn add(self:anyopaque, other:u8) u8{
var aligned = @alignCast(@Alignof(@this()), self); // align
var this = @ptrCast(
@this(), aligned); // restore type A
this.data += other; // Business code
return this.data;
}
fn getI(self:
@this()) I { // A --> I
return .{
.impl = self, //type erasure A --> *anyopaque
.addFn = A.add, // actual function implementation
};
}
};

//Example
var e = A{};
var d : I = e.getI(); // use interface
print("{}\n", .{d.addFn(d.impl,100)}); // Manually call the function with the A pointer.
//or with helper functions
print("{}\n", .{d.add(100)}); `

I hope in the future zig can write code like this:
`const I = struct{
impl: I,
add : fn (
I,u8) u8,
};
const A = struct {
data:u8 = 0,
fn add(self:* A, other:u8) u8{
self.data += other; // Business code
return self.data;
}
};

var e = A{};
var d:I = e;
print("{}\n", .{d.add(100)});`

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.

@Yay295
Copy link

Yay295 commented Aug 15, 2022

@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.

```zig
<code>
```

@presentfactory
Copy link

presentfactory commented Nov 9, 2022

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 std.trait and std.meta), and then raise compile errors when whatever conditions are not satisfied. All of this is done in a fairly elegant way and without any special type checking syntax to complicate the language like you see in others (C++ and Rust for example).

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:

  • "error messages will suffer" - No, things just will need to emit good messages, easily solved on the compiler side of things or of course by the error messages helper functions emit.
  • "everyone will roll their own incompatible imeplentation" - No, this is what the standard library is for as it will standardize the tools needed for this if its even needed (which often times it is not).
  • "interfaces offer an indespensible tool for structuring code" - No, documentation does if anything and is not something interfaces are going to solve magically.

@matu3ba
Copy link
Contributor

matu3ba commented Nov 10, 2022

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).

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.

  • This is too vague for operations/fixing in phrasing.
  • What OP probably meant, is that the user has no error output of what caused the type instantiation(s) or value initialization(s).
  • For both errors during comptime and runtime

@TwoClocks
Copy link
Sponsor Contributor

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 std. The language has already been complicated. This is just standardizing a pattern already in place. Just like the language did w/ fat-pointers.

I also don't understand the "you can do this yourself" argument either. Everything in std you can do yourself, yet zig comes w/ the stdlib. I'm not sure there is value added by being able to do this yourself. Is one solution going to be materially better than another? don't seem likely.

Having a standard way to do this would improve:

  • Communicate intent precisely
  • Only one obvious way to do things
  • Favor reading code over writing code (via reduced usage of anytype)

Weather it's a new language keyword, or a new builtin , or a conical imp in std, I think the zig team should define the standard way this common pattern is done.

I guess you could argue "passing anytype is this standard way this is done". I think for the trivial case fn do_it(reader:@isA(std.io.Reader)) isn't much better than fn do_it(reader:anytype). At least for a human, but I don't think this is true generally. For an IDE the former is much better.
I'm not sure I'd advocate for removing anytype from the language. If someone really wants to ducktype everything, go for it. But I think there should be a standard way of being more formal on expected types, where the compiler error shows up at the call loc, if not the decl. That seems like compiler domain to me.

@leighmcculloch
Copy link

leighmcculloch commented Dec 17, 2022

I think there should be a standard way of being more formal on expected types, where the compiler error shows up at the call loc, if not the decl.

It would be ideal if the compiler and zls could display a full list of the functions required by an anytype parameter. Then as a developer I could inspect an anytype parameter and see the full definition required. Or if I'm missing two functions the compiler tells me what they all are.

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?

@andrewrk andrewrk modified the milestones: 0.11.0, 0.12.0 Apr 9, 2023
@lhk
Copy link
Sponsor

lhk commented Apr 12, 2023

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.
So now I decided to try and figure out how to properly implement dynamic dispatch. I guess I'll copy and tweak the VTable struct from Allocator.

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.
Even if this proposal is abandoned, it would be great to have some form of official documentation.

People seem to enthusiastically apply zig to all kinds of things, UIs, game engines, etc.
That makes perfect sense to me, the language is awesome :)
But in all these domains there will surely be some list of widgets, or game objects, or entities.

I think even in the carefully carved niche of Zig (better C, not C++) some form of polymorphism is important.
The comment of @TwoClocks really captures the gist of it I think :

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 std. The language has already been complicated. This is just standardizing a pattern already in place. Just like the language did w/ fat-pointers.

I'd like to add another reply to "this will make the language more complicated":
Wouldn't it be possible to introduce interfaces as a very modular/isolated feature?

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.

@Pyrdacor
Copy link

Pyrdacor commented Apr 15, 2023

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.

@owl-from-hogvarts
Copy link

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 std.meta namespace (and mark as unstable or put only in nightly builds if any). I took a quick glance at https://github.com/mov-rax/zig-validate and I liked the implementation. That's because it does actual compile-time polymorphism, introducing zero runtime overhead. Incorporating this into std lib will be valuable addition to the language.

@andrewrk andrewrk modified the milestones: 0.12.0, 0.11.0 May 31, 2023
@owl-from-hogvarts
Copy link

c) explicitly implement an interface, sometimes you want to make sure your custom_allocator actually implements the allocator interface, there can easily be added a small syntax to ensure this or a small hack like in golang using an anonymous variable, its not a big deal.

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 Implements function would simply check assignability of struct to an interface

@andrewrk
Copy link
Member

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.

@andrewrk andrewrk closed this as not planned Won't fix, can't repro, duplicate, stale May 31, 2023
@hugoc7
Copy link

hugoc7 commented Dec 8, 2023

Hi ! I'm just discovering Zig wich seems awesome, well done guys !
But something is bothering me ... in Zig there is no auto-completion for anytype.

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
Copy link
Contributor

xdBronch commented Dec 9, 2023

@hugoc7 firstly, the problem of autocompletion is a function of ZLS, not of zig. second, completion for anytype is actually a thing in zls, its just limited to the current file for now

@hugoc7
Copy link

hugoc7 commented Dec 12, 2023

@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).
Why not skip that interface and just use anytype ? Like that:

fn foo(allocator: anytype); // instead of expecting std.mem.Allocator as a type

@presentfactory
Copy link

@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 anytype for such things with my own allocator system, but this is the justification at least in the stdlib's case.

@mk12
Copy link
Contributor

mk12 commented Dec 13, 2023

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.

@16hournaps
Copy link

16hournaps commented Apr 10, 2024

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

@htqx
Copy link

htqx commented Apr 11, 2024

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.

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.

@16hournaps
Copy link

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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests