Type Function and anytype

Zig has type function which can be used to implement generic types when it takes a type parameter.

For example

pub fn Generic(T: type) type {
}

This is clear to me and nothing new here. The type passed can be seen as instantiating an actual type, and it would probably end up being the type of a member field. Most of the languages I have used has same idea.

What is confusing to me now is when the parameter to the type function is anytype which means a value can be passed to it instead of type. ie

pub fn Generic(T: anytype) type {
    _ = T;
}

pub fn main() !void {
    const Generic42 = Generic(42);
    std.destd.debug.print("{any}", .{Generic42});
}

Now this is the part that I have not wrapped my head around. Why would one want to pass in a value this way (similar to how a type can be passed).

When Generic accepts type I can reason about this. Mainly that this is providing a way to fill in the concrete type ie we have a generic data structure.

But I cannot reason about when Generic accepts a value this way. What patterns/problem is this useful for?

There is a topic in Docs, maybe will be helpful somehow.

Just did but I don’t think it does. The doc page explains what anytype is. My specific question is around using anytype in the context of type functions and what pattern does that encode, and how to reason about it and how that is different from using type in the context of type functions

I’ve never seen anyone use anytype for functions that return a type.
In your example if the type function expects a number it would be better to choose comptime_int or comptime_float as the type instead.

anytype is mainly useful for functions that require a specific interface that cannot be encoded in the type system (like for example functions that need a Reader/Writer interface, or math functions that take any number type).

/opt/zig-0.14/lib$ grep -rI anytype | grep "type {"
std/fmt.zig:pub fn Formatter(comptime format_fn: anytype) type {
std/io/counting_reader.zig:pub fn CountingReader(comptime ReaderType: anytype) type {
... <more>

Ah, yes for functions it makes sense, so that the user can pass functions with different error sets, or function pointers.

But that CountingReader could just use type instead of anytype.

1 Like

The part that is further confusing to me is how it seems if a value is passed into a type function expecting anytype, and that value is not set to be part of the struct returned, somehow that value passed at compile time is still accessible at runtime.

For example

pub fn Generic(T: anytype) type {
    return struct {
        fn fortyTwo(self: @This()) void {
            _ = &self;
            std.debug.print("{}", .{T});
        }
    };

}

pub fn main() !void {
    const Generic42 = Generic(42);
    const value: Generic42 = .{};
    value.fortyTwo();
}

Running this would print 42 even though the 42 was passed into the type function and was not added to the state of the struct returned.

If it were atype that was passed and being used to create a generic structure, the type would be clearly part of the struct returned. For example

pub fn Generic(T: type) type {
    return struct {
        value: T,
        fn fortyTwo(self: @This()) void {
            std.debug.print("{}", .{self.value});
        }
    };

}

pub fn main() !void {
    const GenericU8 = Generic(u8);
    const value: GenericU8 = .{.value = 42};
    value.fortyTwo();
}

But in the case of passing in a value via anytype at compile time, it seems I could still access that value at runtime even though it was not stored in the state of the type returned. More like a closure of some sort.

It is things like this that makes me not sure if I fully understand how, why and when using a anytype with type function is useful.

Yeah, it works kind of like a closure, except there is no runtime state, because the compiler will insert your value at compile time. The function basically gets compiled into this:

        fn fortyTwo(_: @This()) void {
            std.debug.print("{}", .{42});
        }

It is things like this that makes me not sure if I fully understand how, why and when using a anytype with type function is useful.

Well as seen by the use-cases of the standard library, it can be useful for function pointers.
Other than that I don’t see much use.

Using numbers as generic type parameters is quite useful for arrays, but you should use comptime_int instead of anytype for these:

fn GenericArray(T: type, len: comptime_int) type {
    return struct {
        arr: [len]T,
        pub fn someFunction(...
    };
}

I mostly share your confusion.

Doing generic data types via ‘fn (T: type, ...) type {’ was one of Zig features I was able to understand very quickly. But anytype is another kind of beast.

It seems anytype is not a type at all (like u32, type(!!!) etc). It looks more like an instruction to the compiler - “expect argument of any type and do monomorphization for each actual type encountered in a program”.

I can do generic polymorphic addition by two ways.

with anytype, like this:

const std = @import("std");

fn add1(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
    return a + b;
}

pub fn main() void {

    const x: f32 = 1;
    const y: f32 = 2;
    std.debug.print("{}\n", .{add1(x,y)});

    const j: u32 = 1;
    const k: u32 = 2;
    std.debug.print("{}\n", .{add1(j,k)});
}

We have two monomorphized versions:

zig-lang/anytype$ nm 1 | grep add1
000000000103c0d0 t 1.add1__anon_3427
000000000103c2b0 t 1.add1__anon_3429

with type type:

onst std = @import("std");

fn add2(T: type, a: T, b: T) T {
    return a + b;
}

pub fn main() void {

    const x: f32 = 1;
    const y: f32 = 2;
    std.debug.print("{}\n", .{add2(@TypeOf(x),x,y)});

    const j: u32 = 1;
    const k: u32 = 2;
    std.debug.print("{}\n", .{add2(@TypeOf(j),j,k)});
}

Again, we have two versions of the function:

zig-lang/anytype$ nm 2 | grep add2
000000000103c0d0 t 2.add2__anon_3427
000000000103c2b0 t 2.add2__anon_3429

What is the difference?
In the second case we indicate arguments’ type explicitly, in the first one it is done automatically, I think so. Correct me, if am wrong.

As to type type and anytype mix - I think they are orthogonal to each other.
Just because anytype is not a name of a type.

anytype is for “parametric polymorphism”, type type is for generic data structures, a sort of that.

Here come “interfaces” :slight_smile:

which, alongside with allocators’ interface are not very easy to grasp.

While anytype can be useful, it mostly relies on you to add your own comptime assertions, I think there’s a proposal up to add an infer keyword that would resolve the use of anytype / @TypeOf(x) issues.

fn add(x: infer T, y: T) T { return x + y; }

Was the proposed idea, of course there’s a lot of details to workout because args in zig can get very complex with inlined types

fn push(x: std.ArrayList(infer T), item: T) void;

I think was a possible example of usage, and I’m sure more comptime wizardry would complicate the matter.

Honestly I try to avoid doing anytype generics if possible since they have a confusing aftertaste that typically requires code spelunking to understand what they’re doing.

Sometimes I’ve found the clearer way to build is to use comptime functions that do take clearer types, and produce a function, or type as an output.

2 Likes