-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Description
I know this is pretty out there, but I was thinking about the interface/inheritance question that's been going around in the community, and it occurred to me: when we say we want something like interfaces, it's kind of a specific version of the more general requirement "I don't care about the exact type of this argument, as long as it satisfies X condition". This is basically what std.meta.trait
is all about, giving you convenience functions for patterns like
fn addAnyNumber(a: anytype, b: @TypeOf(a)) !@TypeOf(a) {
if (std.meta.trait.isNumber(@TypeOf(a)) {
return a + b;
else {
return error.NotANumber;
}
}
My thought is, what if the "trait" concept was expanded to the type system. Colloquially any function with the signature fn(type) bool
could be known as a trait, and types could be defined by the traits that they satisfy using the new builtin @trait
. So let's say you wanted a function that could take any indexable type whose child type is numeric, you could use traits to define it like so:
const std = @import("std");
const isNumber = std.meta.trait.isNumber;
const isIndexable = std.meta.trait.isIndexable;
// Sloppy name, I know
fn canIndexAndGetNumber(comptime T: type) bool {
if (!isIndexable(T)) return false;
const Child = std.meta.Child(T);
return isNumber(Child);
}
const ValidType: type = @trait(.{canIndexAndGetNumber});
fn useSpecificType(a: ValidType) std.meta.Child(@TypeOf(a)) { ... }
Traits could even be composed, so the above example could be rewritten as
// ....
fn isChildNumber(comptime T: type) bool {
return isNumber(std.meta.Child(T));
}
const ValidType = @trait(.{ isIndexable, isChildNumber });
fn useSpecificType(a: ValidType) std.meta.Child(@TypeOf(a)) { ... }
Functionally, this would be equivalent to specifying a
to be anytype
and comparing it against isIndexable
and isChildNumber
at comptime before executing the function. Something that could be done today with something a little contrived like:
// ....
fn useSpecificType(a: anytype) blk: {
const A = @TypeOf(a);
if (isIndexable(A) and isChildNumber(A) {
break :blk std.meta.Child(A);
} else @compileError("a is not indexable or it does not contain numbers.");
} { ... }
I believe that in addition to the general benefit of giving users an elegant, Zig-like way of defining generic types, this could also provide a solution to the interface "issue" that doesn't require a large change to Zig semantics. Imagine you had a function that expected its parameter to have a field called "len" and a declaration named "init". You could define this using traits like this
fn hasLen(comptime T: type) bool {
return @hasField(T, "len");
}
fn hasInit(comptime T: type) bool {
return @hasDecl(T, "init");
}
fn useSpecificType(a: @trait(.{ hasLen, hasInit })) void { ... }
With @trait
users could create and use type conditions/specifiers in arbitrarily complex fashions while not having to complicate the main logic of their functions. In my opinion it's also very understandable if you're trying to use the code, since it's easy to reason about the contents of an @trait
tuple; "Ok, here's the trait being used, it has three conditions (which I know are simply fn(type) bool
's), let's see what the conditions are and then I'll know exactly what qualities this type has".