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: @trait for defining types based on user-defined conditions #8008

Closed
mjoerussell opened this issue Feb 13, 2021 · 2 comments
Closed
Milestone

Comments

@mjoerussell
Copy link

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".

@SpexGuy
Copy link
Contributor

SpexGuy commented Feb 13, 2021

Hi, thanks for this proposal! It's well thought out, detailed, and contains realistic examples. Unfortunately, we've considered this idea before, in #1669 and #6615. The decision on those issues holds here as well. This feature would add significant complexity to the language, for something that is already achievable with comptime code. It's not clear that this approach is better, because nothing keeps the trait checks consistent with the code that uses the types. Ultimately we think the ergonomic improvements from language support for this feature are not worth the complexity it introduces.

@SpexGuy SpexGuy closed this as completed Feb 13, 2021
@mjoerussell
Copy link
Author

Thanks for the response! Totally understand that position, guess I should've searched harder before writing this up.

@andrewrk andrewrk added this to the 0.8.0 milestone Jun 4, 2021
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