#Return anytype for comptime-only functions

1 messages · Page 1 of 1 (latest)

edgy trench
#

So, I believe zig can't return anytype & have that type inferred - which is understandable for readability, and constructing the desired type at comptime is generally easy

However, i'm doing some pretty heavy comptime manipulation of anonymous struct data for some auto serialization library - so my types are annotated as follows:

const Foo = struct {
    const serializer_config = .{
        .ignore = .{"foo", "baz"}
    }
    foo: i32,
    bar: i32,
    baz: i32,
}

Then my auto-serializer function walks the type tree, reads the config attached to the type, and serializes appropriately - so in this case it reads ignore and only serializes bar

So, typically I have many functions that operate on this config type, e.g.:

fn getFieldIgnores(comptime config: anytype) ??? {
    if (@hasField(@TypeOf(config), "ignore")) {
        return config.ignore;
    }
    else {
        return .{};
    }
}

What is the return type of this function?

To compute the return type manually means effectively duplicating each function (and I have many functions that operate on this config anytype):

fn GetFieldIgnores(comptime config: anytype) type {
    if (@hasField(@TypeOf(config), "ignore")) {
        return @TypeOf(config.ignore);
    }
    else {
        return struct{};
    }
}
fn getFieldIgnores(comptime config: anytype) GetFieldIgnores(config) {
    if (@hasField(@TypeOf(config), "ignore")) {
        return config.ignore;
    }
    else {
        return .{};
    }
}

This gets a bit silly, though.

Is there any way around the anytype return restriction, which isn't an insane hack likely to not work between compiler versions?

Thanks 🙂

untold drift
#

For comptime-only functions, you could do something like this:

inline fn getFieldIgnores(comptime config: anytype) getFieldIgnoresInner(config).T {
    const res = getFieldIgnoresInner(config);
    const ptr: *align(1) const res.T = @ptrCast(res.ptr);
    return ptr.*;
}

fn getFieldIgnoresInner(comptime config: anytype) struct { T: type, ptr: *const anyopaque } {
    const ret = comptime blk: {
        if (@hasField(@TypeOf(config), "ignore")) {
            break :blk config.ignore;
        } else {
            break :blk .{};
        }
    };
    return .{ .T = @TypeOf(ret), .ptr = &ret };
}

This works by having the Inner variant return a type-erased pointer alongside the type at the pointer's target. Of course, there's a bit of constant boilerplate here (the getFieldIgnores wrapper and the block in the Inner function), so this would only make sense if the function was fairly large.

#

Another, similar, approach:

inline fn getFieldIgnores(comptime config: anytype) getFieldIgnoresInner(config, true) {
    return getFieldIgnoresInner(config, false);
}

fn getFieldIgnoresInner(comptime config: anytype, comptime ret_type: bool) if (ret_type) type else getFieldIgnoresInner(config, true) {
    const ret = comptime blk: {
        if (@hasField(@TypeOf(config), "ignore")) {
            break :blk config.ignore;
        } else {
            break :blk .{};
        }
    };
    return if (ret_type) @TypeOf(ret) else ret;
}
#

Here we use a comptime parameter to decide if we're computing the type or value of the result. Again, avoids duplicating the actual function logic, but comes at the cost of some constant boilerplate

#

I can't give more specialized advice without more details as to what these functions are - to me this smells of a slightly questionable design for this config thing, but I can't be sure