Why doesn't Zig support anytype result type?

I hope the following code compiles, but it is not.

const std = @import("std");

pub fn main() !void {
    var it = iterator();
    while (it.next()) |n| std.debug.print("{}\n", .{n});
    
}

fn iterator() anytype {
    return struct {
        n: usize = 0,

        pub fn next(it: *@This()) ?usize {
            if (it.n >= 5) return null;
            defer it.n += 1;
            return it.n;
        } 
    }{};
}

Two problems:

  1. You are trying to reference iterator as though it were not a function, when it is.
  2. iterator should return type instead of anytype

The reason why it works this way is because the @TypeOf() result of any struct{} is type.
You can even verify this with a quick comptime block:

comptime {
	std.debug.assert(@TypeOf(struct{}) == type);
}

EDIT: I see now that you’re trying to instantiate the struct in the function before you return it.
This could be solved in a few simple ways:

  1. Just pre-declare the struct as a constant and instantiate it normally
  2. Separate the struct declaration into a different Iterator() function that returns a type, and change the return type of the iterator() function to Iterator()
  3. Don’t instantiate the struct in the function call, but instantiate it directly after calling the function like so: var iterator: Iterator() = .{};
4 Likes

I also think that it’s annoying sometimes that you can’t do that. And sometimes it’s really annoying to work around it because it can destroy code locality.

I assume that the function returns a constant because you simplified your example to make it easier for us. Anyway, this should work:

const std = @import("std");

pub fn main() !void {
    var it = iterator();
    while (it.next()) |n| std.debug.print("{}\n", .{n});
}

fn Iterator() type {
    return struct {
        n: usize = 0,

        pub fn next(it: *@This()) ?usize {
            if (it.n >= 5) return null;
            defer it.n += 1;
            return it.n;
        } 
    };
}

fn iterator() Iterator() {
    return .{};
}

This can of course be extended to this:

const std = @import("std");

pub fn main() !void {
    var it = iterator(@as(i32, 3));
    while (it.next()) |n| std.debug.print("{}\n", .{n});
}

fn Iterator(T: type) type {
    return struct {
        n: T = 0,

        pub fn next(it: *@This()) ?T {
            if (it.n >= 5) return null;
            defer it.n += 1;
            return it.n;
        } 
    };
}

fn iterator(start: anytype) Iterator(@TypeOf(start)) {
    return .{.n = start};
}
1 Like

If you want your code to work just replace anytype with type.

anytype can be used only with functions parameters, to derive the type from the call site type.

You might want to derive a return type, and there are two different ways to do it.

  • Derive it from the return statement value type. This was proposed in the past and was rejected. The reason was mostly the bad experience from D programming language that have a similar concept.
  • Derive it from the use of the return value in the call site. This is a open issue that I’ ll love to have.
3 Likes

The whole point here is to avoid leaking the result type out of the iterator function. Yes, it is not a necessary feature. I just wonder if this is possible from tech view. If it is possible, is the reason why it is not supported is compilation speed related?

If you look closer at OP’s code, you see

return struct {
    //...
}{};

In the last line here a value of the type gets immediately created.

Kinda hard to miss, but valid.

Oh, thank you. Yes, I missed that.

Why do you want to hide the type?

Even if it wasn’t accessible directly, somebody could just use @TypeOf(it) to get the type, so I am unsure what you want to achieve, is the intent to have less exposed declarations or something else?

2 Likes

Sometimes when I am just trying things I do this

const std = @import("std");

pub fn main() !void {
    var it = iterator();
    while (it.next()) |n| std.debug.print("{}\n", .{n});
}

fn iterator() struct {
    n: usize = 0,

    pub fn next(it: *@This()) ?usize {
        if (it.n >= 5) return null;
        defer it.n += 1;
        return it.n;
    }
} {
    return .{};
}

Also don’t do this this is unreadable

5 Likes

My wording is inaccurate. @KilianHanich mentions ā€œlocalityā€, which is a better wording.

1 Like

Allowing this would lead to what D calls Voldemort types: types which one can use, but can’t ā€œspeak its nameā€.

var iter: ??? = iterator();

D considers this a good thing, Zig does not.

There’s nothing to put in the ???, you couldn’t give it a name if you wanted to. This goes against Zig’s values, which favor legibility: it should generally be possible to annotate the type of something, even if that gets weird (e.g. having an if statement mirrored on the LHS and RHS).

Also, an anytype return type would allow for completely duck typed Zig, everything could have the signature fn (anytype) anytype and just use tuple call syntax. I didn’t immediately spot the issue where anytype returns were decided against, but I remember these considerations being a factor in that decision.

I very occasionally am writing some reflection-heavy operation, and end up duplicating a big chunk of the function logic in a block which just provide the return type of the function. It’s a bit awkward, but it’s rare, and it’s ok if weird stuff looks weird, I’d say it’s good actually.

If it means I never have to open up a file and discover that every function just says anytype for its return value, that’s a fair trade. Bad enough when all the error types are just left inferred, and that’s common; I wouldn’t want to see it spread to the return types themselves.

9 Likes

If you really want to hide the type (you probably don’t, but for the sake of understanding) that is what opaque is for.

1 Like

As many people have mentioned in this particular example you can fix it by replacing with ā€œtypeā€œ

However I’ll share something that can solve similar problems that have happened to me, many times I have functions that don’t know the type of the return value immediately.
In those cases a solution I found was to make a function just to calculate the type.

so instead of
functionOfUnknownReturnType(x: anytype) anytype {}
which zig doesn’t allowbb

You can have
functionOfUnknownReturnType(x: anytype) findType(x) {}

then a function
findType(x: anytype) type {}
inside the findType function you can calculate the type of the value the previous function would return.

It’s a bit tedious to write, and them might be some code duplication but when you come back to it later you at least have some information as to what the return type will be.

2 Likes

Finally I wrote it as


fn iterator() struct {
    n: usize = 0,

    pub fn next(it: *@This()) ?usize {
        if (it.n >= 5) return null;
        defer it.n += 1;
        return it.n;
    } 
} {
    return .{};
}

I still don’t really understand why. Of all the solutions here, this was one of the ones that was listed as being unreadable, and frankly I agree. I read the linked article about D and all the comments here, and I don’t really get the argument against just returning type from your function, or, if you for some weird reason really do want to hide the type, declaring the struct at the same level as the function, and if you want to expose functions that wrap your struct, you can mark them as pub and make sure those are the only functions in the file, treating it like a module. Again though I ask, why? The argument D gave about having to see the RandomNumberGenerator type all over doesn’t really apply in Zig, because you could name your function whatever you want, and you only need to write it on the right-hand side. Sure, it’s in the namespace, but does that matter?

1 Like
@enumFromInt(integer: anytype) anytype

Isn’t this Voldemort then? That is sometimes how I feel it!
Somehow i tend to agree that it should be possible to return a anytype because it is possible for built-ins. Not fair :slight_smile:

2 Likes

It does if you want the language to remain consistent, which of course is a decision for the language designer. The strong tendency I see is for Zig to be kept as consistent and simple as possible, and omit features that aren’t necessary and would add inconsistency, even if they might be convenient in some cases. If anything, I see that features are sometimes removed to make it simpler and more consistent.

Being able to refer to the result location type in any way, within the body of the function, and by reference in the result statement: I’ve wanted that a few times.

It’s kind of the anti-Voldemort though: you can’t use it at all if you can’t say what the type is. Doesn’t have to be an explicit annotation, Zig’s result types are nicer than that, but it’s passing certainty down rather than propagating uncertainty up. It would mean the caller can ā€˜duck type’ the result, not just the parameters.

The effect would be… not trivial though, since where does the result location type come from? Well the result type obviously. Except now we’re trying to work it in the other direction.

I’m doing fine without this, frankly. Sometimes I’ve wanted the result location itself, mainly out of paranoia that a struct is going to get copied off the stack instead of put straight into the result location as I would prefer. But I started doing the TigerBeetle thing of creating an undefined on the stack and constructing it by pointer, because that’s guaranteed to work, so I haven’t thought about it in awhile.

2 Likes