Discussion: A potential solution to the anytype problem

you need to what you can do in zig first before thinking about what it can’t do.

i’m basically repeating my point but, the same can be done with the existing zig features like this

fn satisfiesAnimal(comptime T: type) bool {
    var ret = @hasField(T, "age");
    ret = ret and @hasField(T, "height") and @FieldType(T, "height") == u8;
    ret = ret and @hasDecl(T, "eat") and @TypeOf(T.eat) == fn (T) void;

    return ret;
}

that new sugar syntax is not worth it.
also a builtin like @Self already exists and that’s @This

With |T| syntax maybe also this would be possible:

fn IEater(T: type) type {
    if (!@hasDecl(T, "eat") or @TypeOf(T.eat) != fn (T) void)
        @compileError(@typeName(T) ++ " lacks `eat` method.");
    return T;
}

fn feed(animal: IEater(|T|)) void {
    animal.eat();
}

This is a really awesome idea. It composes so cleanly with Zig’s existing features. Basically you’re just putting the type-checking code (IEater) into the type signature, which serves as better documentation than the current model, where it hides inside the function body. More generally you can chuck in any Boolean expression as the type:

fn feed(animal: Eater(|T|) and Creature(T)) void {
    animal.eat();
}

This is basically interfaces/traits, except it’s just ordinary comptime function execution! No need for a special trait concept.

All we’d need is compiler support for using these “predicates” where a type is expected.

The above syntax abuses Boolean “truthiness” which Zig doesn’t support elsewhere. So a syntax such as the following might make more sense:

fn feed(animal: |T| where Eater and Creature) void {
    animal.eat();
}

Same semantics (just comptime function evaluation), but this time the functions aren’t forced to return T. This probably simplifies the implementation a bit, because the type of T can be deduced more easily.

This is basically @devBoi76’s proposal, except using Andrew’s |T| syntax.

Edit: I’ve looked through some of the older discussion threads about anytype and it looks like designs almost identical to the ones above have been proposed multiple times over the years. It’s probably not worth having another round of discussion.

It’s less pretty but this is possible today with an anytype parameter and calling things like IEater(@TypeOf(param)); in the body of the function.

1 Like

Yeah I’m aware. But this thread is about being able to document type constraints using syntax that appears in a type signature. That’s usually what people mean when they say they wish Zig had some kind of “trait” feature.

Compiler already gives you good errors even without the requires blocks here. The main problem people have with anytype is the tooling and documentation aspect for user which this does not solve.

Something like requires would be very interesting to be able to test comptime functions however.

1 Like

Whoops, I mean to use @This, not @Self. I was writing that snippet without an IDE and I misremembered the name. Its been edited now.

This wouldn’t work because Eater/Creature must return a type and you can’t treat them as boolean, or expect that Eater(|T|) and Creature(T) can resolve into a type.

On a side note it would be cool if this worked already with anytype (but it doesn’t).

(post deleted by author)

Yeah I suppose if we don’t want to special-case the behaviour of and then it’s not the ideal syntax. Did you see the second syntax I added to my post? You may find that more palatable:
fn feed(animal: |T| where Eater and Creature) void {...}

(post deleted by author)

The point of my example was in not inventing new syntax for something that doesn’t really need it. If Eater(anytype) worked this would be already possible (but it isn’t).

This thread is certainly slowing down, and I’m seeing more and more responses along the lines of:

Actually, this is already possible in zig, you just need to put comptime checks in the function body.

Yeah, I know, this was one of the very first things that I established.

This is a sign that this thread is getting too long for people to read and understand the whole thing (or even the initial post, for that matter). This is to say that I don’t expect to see many new opinions from here on out. That said, I will continue to monitor and respond to any ideas that I find to be truly novel and in the spirit of this imaginary feature.

Many thanks to those who contributed their brainpower to this discussion, I learned a lot!

I’m currently writing a module that should make it easier to do more complex assertions on types, and get useful compile errors out of them. I’ll eventually provide a constraint similar to this feature, although, no syntactic sugar of course.

I also want to point out that, testing for equality between types is far more restrictive than needed. If we want an .eat() method, does the self parameter needs to be a pointer or a value, does it matter? Naive assertions often rule out perfectly valid types, that a bare anytype would’ve accepted.

People here are focusing way too much on compiler error messages. That’s the least of the problems. If an LSP could let us know what functions are available, we wouldn’t even run into these errors.
I believe that any effort to change anytype is only worth it if it improves tooling.
Think about how an LSP works, and you’ll immediately see that the only way for an LSP to provide autocomplete for a generic parameter is if the function signature directly exposes a declarative list of functions and fields for that type. Arbitrary code cannot exist anywhere between the function signature and this list of fields and functions. There’s very little wiggle room outside of what @Tallis-Larsen has proposed.
I was cooking an idea that used regular structs:

const AnimalConcept = struct{
  age: u8,
  pub fn eat(self: *@This()) void{
    _ = self;
    return undefined;
  }
};

fn foo(animal: anytype(AnimalConcept)) void{...}

This doesn’t introduce any new keyword, nor a new kind of type.

3 Likes

Thanks for recognizing this! I appreciate that you actually understand the use-case I’m targeting. As for your idea, I’m actually quite a fan of the fact that it requites no new keywords. However, the intent is rather ambiguous, and it would be easy to think that you can add a function body to eat(), when it should only actually include the signature. Also, this implementation would allow for the use of AnimalConcept as a regular type, when such usage should not be allowed. Other than that, I really like the anytype(AnimalConcept) syntax, and if you can find a way to visually enforce that eat() should not be implemented, then I would definetly say it’s better than my idea; I don’t think doing that in an intuitive way without adding a keyword will be very easy though.