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?