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

Proposal: Adding interfaces to the language #20819

Closed
Darkfllame opened this issue Jul 27, 2024 · 3 comments
Closed

Proposal: Adding interfaces to the language #20819

Darkfllame opened this issue Jul 27, 2024 · 3 comments

Comments

@Darkfllame
Copy link

I guess it already has been discussed and maybe rejected, if not just tell me, but they are in practically every languages. Interfaces/traits (whatever you call them, warning: will may or may make some develop ptsd <:0) would be nice in zig and make it the unbeatable systems programming language (at least for me), the current method of making interfaces in zig is, well, the good old "vtable" one, but it's a bit tedious to make and use. A basic implementation could be with static interfaces: a single interface gets regenerated for every types implementing it, this is nice, easy and it works well, proven to be really reliable, but it has some flaw, like you can make an array of those without specifying a type (btw rust's trait are static by default). But the big goal is: dynamic interfaces (ooh scary). They got a fixed size for all implemented types: they have a vtable and a pointer to the actual interface data (just like std.mem.Allocator), those can be harder to debug (maybe). But what everyone cares about is "how do would I use them in zig!". For this I'd like to introduce you the syntax I though for them:

 const MyInterface = interface {
    foo: u32,
    // default declarations
    // this would go nicely with #20242 btw (at least a function literal syntax)
    bar: fn () u32 = struct {
        pub fn inner() u32 {
            // Maybe a bit too long, idk...
            return @Implemented().foo;
        }
    }.inner,
};

They are declared just as structs, except that the fields declare public, mandatory declarations for the implemented type. They can be implemented on structs, opaques, enums and unions (every types which can have declarations). interfaces can have default values (which result in a default behavior if the declaration is not specified) and they can be pretty much anything. For dynamic interfaces, the interface type is dynamic by default. If referenced in anyway other than implementation, a default code gets generated for it (if there's default functions). To declare a static interface though, you'll need to use the new && operator (could maybe change if you'd prefer to):

const MyStruct_raw = struct {
    pub const foo = 5;
};

// This could partially be implemented via comptime, i know,
// but you cannot really set declarations with it, you can
// still check though.
// here, `MyStruct_raw` is in fact not the same as `MyStruct`
pub const MyStruct = MyInterface && MyStruct_raw;
pub const MyStruct2 = MyInterface && struct {
    pub const foo = 10;
};


pub fn main() void {
    std.debug.print("MyStruct.bar() = {d}\n", .{MyStruct.bar()});   // MyStruct.bar() = 5
    std.debug.print("MyStruct2.bar() = {d}\n", .{MyStruct2.bar()}); // MyStruct2.bar() = 10
}

Also static interfaces can be coerced to dynamic interface like this:

var baz = MyStruct{};
const bazi: MyInterface = &baz;

Which clearly indicates that it stores the pointer to baz (which makes more sense for zig). However dynamic interfaces cannot be coerced back to static ones except via @ptrCast or maybe a new builtin function for this special case (which also would make more sense for zig).

As I said before, dynamic interfaces are literally the equivalent of a struct containing all the declarations as pointer + the pointer to the interface data and thus, getting should do as so (being able to access data pointer, declaration pointers is pretty straight-forward).

@rohlem
Copy link
Contributor

rohlem commented Jul 27, 2024

I guess it already has been discussed and maybe rejected

You can use the GitHub issue search for "interface label:proposal": https://github.com/ziglang/zig/issues?page=1&q=interface+label%3Aproposal .
There are a lot of unrelated search results, but from what I could find these seem relevant: #130, #500, #1268, #3620, #9272, #10184 .
(Not to say they're all good examples to follow when creating a GitHub issue, but they are certainly attempts at something similar.)

The issue template section for "Language Proposal" links to a statement that the maintainers do not welcome language proposals at this time.
Given this, if you decide to go against this notice and write a proposal on the issue tracker anyway, I would suggest you first use other channels, for example one of the community spaces, to discuss and refine your idea with feedback from other developers.
Ultimately, it will be considered spam, so it needs to be good enough to convince people to read it regardless.

As for my personal feedback now that I've read the current version:

IMO your proposal doesn't specify the deficiency in the language well enough that it tries to solve.
As it stands, it's not clear what its goal is, and therefore it's difficult judge whether it would succeed at it.
What do you dislike about status-quo? @fieldParentPtr? Writing a struct with function pointer fields?

Interfaces/traits [...] would be nice in zig [. ... T]he current method [i]s a bit tedious to make and use[.]

One of Zig's fundamental goals is to be explicit and value reading code over writing it. A feature request aligned with these goals ("the Zen") is easier to justify than one only oriented on a hunch, without direct comparison to status-quo.
Can your idea model std.mem.Allocator as it is today? Is your new formulation of std.mem.Allocator a clear improvement?
Are there drawbacks? Zig Zen: "Avoid local maximums," so if the new mechanism is less powerful you might have to switch back to manual VTables later. If this switch is a lot of work, the new language might lead to more work for developers overall.

Simplicity is also a design goal of Zig, and an easy way for simplicity is to keep a small size overall.
This proposal introduces a lot of new syntax and concepts at once:

  • syntactic interface { ... } scopes,
    • which construct a new type class of "interfaces",
      • which will also have special type coercion / interaction rules,
  • an @Implemented() builtin,
  • a && operator,
  • maybe more that I've missed.

Every one of those would need to be justified:

  • Are these new concepts valuable anywhere else in the language?
  • Can't we do the same with something simpler, or extend the semantics of something the language already has?

@Darkfllame
Copy link
Author

  • Are these new concepts valuable anywhere else in the language ?

The basic idea behind this proposal is to have an easier/more flexible way to do interfaces, for example, allocators implementation should look like:

// in some std/mem/allocator.zig
pub const Allocator = interface {
    // on dynamic interfaces, all declarations are *const T (T being the type of the decl)
    rawalloc: fn (self: *@Implemented(), len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8,
    rawresize: fn (self: *@Implemented(), buf: []u8, buf_align: u8, new_len: usize, ret_addr: usize) bool,
    rawfree: fn (self: *@Implemented(), buf: []u8, buf_align: u8, ret_addr: usize) void,

    // just like with structs, you can have methods on interfaces, having a aspecific
    // syntax for method calling could really be useful here tbh
    pub fn create(self: Allocator, comptime T: type) Error!*T {
        if (@sizeOf(T) == 0) return @as(*T, @ptrFromInt(math.maxInt(usize)));
        const ptr: *T = @ptrCast(try self.allocBytesWithAlignment(@alignOf(T), @sizeOf(T), @returnAddress()));
        return ptr;
    }

   // . . .
};

And every Allocator implementations could simply do Allocator && struct { . . . } and have no .allocator() function whatsoever, when giving an allocator, just do &gpa or such and it will cast it automatically to a dynamic Allocator.

Also if you implement an interface twice, it changes absolutely nothing, and Interface&&Struct == Interface&&Interface&&Struct, also if the orignal type already implements everything of the interface, it does not change at all, right side would need to have priority over left side too.

Another use could be in places where we need some generic implementations (well that's the primary use of interfaces), so that being std.mem.Allocator, std.Thread, serialization, cross-platform library (for internal API, makes them more contributor-friendly) and overall, well, everywhere you need a specific API.

  • Can't we do the same with something simpler, or extend the semantics of something the language already has?

Well, it could work with comptime and @hasDecl (at least for the checks), but there would be no defaults, if comptime could set declarations on comptime-created type (with @Type()): std.builtin.Type.Declaration would need to have a type, is_pub, is_var in addition of the name, we could really just have interfaces be dumb structs, with an .implement comptime function, and we could work around, but without it, I feel that the language can be a bit limiting sometimes. Also it would actually say where is the error, instead of just pointer the @compileError call.

I think having a clearer way of doing interfaces would be great in any points. Right now, a lot of languages have interfaces or things similar to them: C++ (virtual functions), obj-c (protocols), c#, java, go, rust (traits) and (i guess) many others. Having a standard way of doing interfaces could be great.

While writing this, i realized how weird it could end up, and become a footgun, like, what if you give a default function with comptime logic depending on the implemented type ? compile error, the interface becomes unusable alone, apparently, not a lot of people complain except new-comers, guess i'm too young to the language ¯_(ツ)_/¯ (i've been using it for like 10 month or so). Anyway good day, might have been useless to waste 30 mins of my life writing a useless proposal. Even though comptime-set declarations could be really interesting :^

@Darkfllame Darkfllame closed this as not planned Won't fix, can't repro, duplicate, stale Jul 27, 2024
@zaddok
Copy link

zaddok commented Feb 15, 2025

Comptime interfaces have the potential to be an innovation that no other system level language supports. (Although go comes close, with its generics system that does comptime duplication of the code for each required type)

Given zig's (awesome) love for allocators, there is a significant potential to improve the way allocators are handled with comptime interfaces as well. If comptime interfaces dont arrive before 1.0 then it'll probably be too late to add them later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants