Interfaces have been requested before many times, but for some reason the idea has been rejected?
My proposal doesn’t involve any new type of polymorphism to be added to the language, it doesn’t make it more complex, it would actually make it simpler. It would simply provide better type-safety (and I believe it can be had at zero cost).
The way I’ve seen interfaces (sort of) implemented is with tagged unions (in ziglings there is an example of that), but imho it’s an ugly and hackish workaround which also has a runtime cost, since unions are as big as their biggest member, and it requires some (maybe comptime) form of introspection to have some type safety. It also clashes with one of zig’s tenets (“focus on debugging your application rather than debugging your programming language knowledge”). Interfaces instead are a simple and well known concept and I think they can be had at zero runtime cost.
Example (working in 0.14.1):
const print = @import("std").debug.print;
const a = struct {
a: u8 = 9,
b: u8 = 4,
c: u8 = 2,
pub fn printSelf(self: @This()) void {
print("{s}: a = {}, b = {}, c = {}\n", .{@typeName(@TypeOf(self)), self.a, self.b, self.c});
}
} {};
const b = struct {
b: u8 = 9,
a: u8 = 4,
pub fn printSelf(self: @This()) void {
print("{s}: a = {}, b = {}\n", .{@typeName(@TypeOf(self)), self.a, self.b});
}
} {};
pub fn main() !void {
p(a);
p(b);
}
// no type-safety at all, but it works
fn p(s: anytype) void {
s.printSelf();
}
With interfaces, it would be:
const print = @import("std").debug.print;
// no data should be contained in interfaces, only field declarations with
// their type, otherwise it can't be zero-cost
// function prototypes don't need the parameter name, only the type
const ifc = interface {
a: u8,
b: u8,
pub fn printSelf(type) void,
}
// note the symmetry with the tagged union syntax
// the compiler will make sure that all fields declared in the interfaces will
// also be declared here, with the same type
const a = struct(ifc) {
a: u8 = 9,
b: u8 = 4,
c: u8 = 2,
pub fn printSelf(self: @This()) void {
print("{s}: a = {}, b = {}, c = {}\n", .{@typeName(@TypeOf(self)), self.a, self.b, self.c});
}
} {};
const b = struct(ifc) {
b: u8 = 9,
a: u8 = 4,
pub fn printSelf(self: @This()) void {
print("{s}: a = {}, b = {}\n", .{@typeName(@TypeOf(self)), self.a, self.b});
}
} {};
pub fn main() !void {
p(a);
p(b);
}
// the compiler here will check that only fields declared in the interface are
// used
fn p(s: ifc) void {
s.printSelf();
}
So interfaces will be used by the compiler, that must:
- validate structs types that use the interface, making sure that they implement all fields declared in it, with the exactly the same types (signatures for functions)
- validate all functions that take an interface as parameter, making sure that only fields declared in the interface are used in it
Why zero-cost? Because after the compiler has done the above, information about interfaces doesn’t need to end up in the binary or affect runtime in any way. After removing the interface stuff, the above script becomes the same as the first one (where interfaces weren’t used), but you gain type safety in the process:
const print = @import("std").debug.print;
// interface is gone
// again a normal struct
const a = struct {
a: u8 = 9,
b: u8 = 4,
c: u8 = 2,
pub fn printSelf(self: @This()) void {
print("{s}: a = {}, b = {}, c = {}\n", .{@typeName(@TypeOf(self)), self.a, self.b, self.c});
}
} {};
const b = struct {
b: u8 = 9,
a: u8 = 4,
pub fn printSelf(self: @This()) void {
print("{s}: a = {}, b = {}\n", .{@typeName(@TypeOf(self)), self.a, self.b});
}
} {};
pub fn main() !void {
p(a);
p(b);
}
// again anytype, but the work has been done already, this function is much
// safer than it was
fn p(s: anytype) void {
s.printSelf();
}
I’m probably oversimplifying, but you surely got the idea? Interfaces would mean:
- better type safety, much less need for
anytype - much less need for introspection, and relative boilerplate code
- overall simplification of the code, using a well-known pattern
- potentially reduce the need for generics?
Maybe there could be even be runtime benefits. Honestly I don’t see what keeps zig from having interfaces.