Compiler-checked runtime interfaces with struct(interface) syntax

Zig std often uses a pattern of building a vtable to represent an interface at runtime.
This is used for things like std.Io for example.
The main problem with building an interface structure by hand is that:
It is a bit verbose to implement a vtable in a structure with correct logic from memory.
Handwritten interface implementations are not checked to adhere to the interface surface.

In Zig there already exist similar syntax to collection that give a contract for user.
The enum and union keywords.

const Tag = enum { a, b, c };

const Tagged = union(Tag) { a: u8, b: f32, c: bool };

const Untagged = union { a: u8, b: f32, c: bool };

Enum is more or less a collection of words representing numbers.
While union is just a collection.
Together they form a tagged union where the union is restricted by the enum.

This can be applied in a similar manner to interfaces.
Interface is a collection of function definitions.
While a struct is a collection.
Together we can restrict the struct to adhere to the interface.

const Reader = interface {
    fn read() void,
    fn readLength(length: usize) !void,
};

const ReaderImpl = struct(Reader) {
    pub fn read() void {...}
    pub fn readLength(length: usize) !void {...}
};

const WithouInterface = struct {...};

Making it match the enum syntax very closely.

Interface can only contain function definitions.
Interfaces is type checked by compiler, and the struct must contain all fn that the interface has or error.
Compiler generates vtable or other dispatch machinery needed for runtime interface use.
Interface fully restrict the public surface of the struct making it have no other public functions or fields then what the interface defines.
The struct non-public surface can be whatever needed to implement the interface’s demands.

The strictness in the struct only exposing interface publicly is that when used and accessed as an interface it would be impossible to reach pub functions outside the interface definition.
This exposes two public surfaces depending on struct is known or only interface is known.

Usage of the interface can happen at runtime and be swapped at runtime just like Allocators, Io and so on in std is.

fn useReader(reader: Reader) void {
    reader.read();
}

var file_reader = ReaderImpl{};

useReader(file_reader);

Related discussion:
Can we have compile-time, zero cost interfaces?

In this scheme, I’m not clear how you get methods: functions with a first parameter of the struct? interface? type.

I don’t think I’ve clearly understood what the problem you’re pointing out with status quo is?

If you hand-code a VTable struct, then when you produce an instance of that struct you must provide all of the corresponding function pointers or get a compile error. In other words, the benefit you describe exists today?

Oh, my bad.
I made the examples a bit sparse.

An interface can only contain method declarations that the implementing type is required to provide.
A struct that implements the interface is then checked against those method signatures.

@This() inside the interface refers to the type implementing that interface.

const std = @import("std");

const Calculation = interface{
    fn add(self: @This(), value: i32) i32,
};

const CalcImpl = struct(Calculation) {
    value: i32 = 0,

    pub fn add(self: @This(), value: i32) i32 {
        return self.value + value;
    }
};

test {
    const calc = CalcImpl{};
    try std.testing.expectEqual(1, calc.add(1));
}

I think my original was a bit poorly worded.

  1. I want to not have to type out boilerplate every time I use interfaces.
    I also do not remember how to boilerplate an interface fully, so I often stumble a bit halfway.
    Or I reach for documentation, copy-paste, and replace things just to get the boilerplate.
    It is a rather common structure and is a bit too verbose for my liking with a vtable.

  2. I want the compiler to understand that the struct is an interface implementation, and be able to make guarantees and optimizations relevant to such a structure.

  3. I want the struct using the interface to be rather strict.
    I do not fully see a point in letting the structure have a larger surface than the interface defines.
    It only leads to usage of the struct and passing it when it is meant to be used through the interface way.
    When the struct is passed as an interface, if the struct had other public methods, those would be unreachable.

  4. I also think it is a bit of a discoverability issue for new users if the default way is to do vtables.

Ahhhhh, I see, thanks for explaining. I think the discoverability issue is probably thought of as a feature rather than a bug: Zig appears to want to keep a certain amount of friction present around dynamic dispatch. I’m not sure if this is a function of me or the projects for which I use Zig for or what, but somehow as a result I mostly don’t use complex interfaces when I program in Zig.