Difference between anytype and type

I notice in some case I can use type and anytype interchangeably. For example

fn List(comptime value: anytype) type {
    return struct {
        values: []const value,
    };
}

vs

fn List(comptime value: type) type {
    return struct {
        values: []const value,
    };
}

compiles and seems to do the same thing.

While in other cases, I cannot use the two interchangeable. For example in the return type.

This does not work, ie anytype in the return position, while type works.

fn List(comptime value: anytype) type {
    return struct {
        values: []const value,
    };
}

What are the differences between these two language elements?

anytype can be any type, including the type type. anytype is only allowed in function parameters, as it’s not a real type, but a placeholder.

2 Likes

From Function-Parameter-Type-Inference:

Function parameters can be declared with anytype in place of the type. In this case the parameter types will be inferred when the function is called.

Example:

fn increment(value: anytype) @TypeOf(value) {
    return value + 1;
}

anytype is a mechanism to infer the type from the call value.

Not sure I see how this prevents it from also appearing in the return position?

The return type can also be inferred based on how the return type is used too right?

They explicitly decided agaisn’t type inference in the return type. I don’t remember the issue, but Andrew said it simplified the compiler and lead to more readable code.

1 Like

Zig grammar prevent this. While the return can be TypeExpr the ParamType is:

ParamType
    <- KEYWORD_anytype
     / TypeExpr

anytype is a keyword that can appear only as function parameter type.

1 Like

Oh my! All of these sounds arbitrary to me :expressionless: but I guess it is the price you pay for jumping into a language still influx :slight_smile:

#230 and #447 (var was anytype)

3 Likes

999 times out of 1000, not being able to specify what a function returns is a failure of the abstraction.

What are you trying to do that you need the return type inferred?

2 Likes

Nothing in particular. When learning a new language I usually just try things out and see what works or does not, as I try to get a better understanding of how to use the language

Dropping a link to a doc related to this subject: Generic Programming and anytype

2 Likes

And with this, I discovered the doc section! Thanks!

2 Likes

You can write Type Functions to calculate return types, if there’s some kind of relation with the input type.

For example, in the parser combinator library mecha, there are two Type Functions ParserResult and ReturnType which calculate return types for some functions based on the type passed into a function through anytype.

ReturnType takes a type as a parameter, and depending on whether it’s a pointer to a function or a function itself, uses typeinfo to find the return type of the parse function.

Since all parsers wrap their results in a struct created through the Type Function Result, they store the type of the result value as a member of the struct, which ParserResult can directly access.

So when the exact relationship between the return type of a function and the input type is known, you can write a type level function to calculate the return type in place of actual return type deduction. It’s certainly not a replacement for complete return type deduction, but it can allow you to write functions you would otherwise have to rely on type erasure for (meaning returning *anyopaque then casting it back to its original type).

3 Likes

So after some more time with Zig, I think I have a better understanding of anytype. As @dimdin said, it is mainly a “…mechanism to infer the type from the call value.”

This makes sense now. It is more like a more powerful Any type in TypeScript where the type can be inferred at compile time.

Now it is type I need to wrap my head around. Back to my original question:

This works:

fn List(comptime value: anytype) type {
    return struct {
        values: []const value,
    };
}

so also does

fn List(comptime value: type) type {
    return struct {
        values: []const value,
    };
}

I am looking now for more clarification of type versus clarification about anytype.

How is the version with comptime value: type different from comptime value: anytype?

Are there instances where type is what would be needed over anytype? if so what are such instances?

@LucasSantos91 mentioned that " anytype can be any type, including the type type ." but I am not sure what “the type type means” because from what I have seen, type is like the super type, and the root of all types? And not something that can be used directly

ie when instantiating the List it can be done by passing another type. ie List(u8) or List(MyStruct) etc and not a type of type.

Also does type also not infer the type from the call value? If so how does its infer different from infer based on anytype?

I’m still relatively new here, so with the caveat that I may get corrected:

I think you’re misunderstanding what happens when you use your List function.

When instantiating the List it can be done by passing another type. ie List(u8) or List(MyStruct) etc and not a type of type .

u8 or MyStruct are both values with type type. type is the type of type values.

A slice of bytes like "hello" has type []u8. []u8 has type type.
A 32 bit float like 7.45 has type f32. f32 has type type.

Leaving the argument type as anytype means the function accepts any type.
Not just any type.

A function accepting anytype would take any value, like "hello" or 7.45, but it could also take the type of those values - []u8 or f32 - since types themselves are also valid anytype values.

A function accepting type would only accept the types, and not the values.

Your version of List accepting anytype only works as long as you use it correctly and pass it a type. If you try to pass it something that’s not a type value, the compiler will yell at you:

const any_list = List(0); //type of '0' is 'comptime_int'
const any_list_values = any_list{ .values = &[_]u8{ 1, 2 } };
std.debug.print("Any: {any}\n", .{any_list_values});

Yields:

error: expected type 'type', found 'comptime_int'
        values: []const value,
5 Likes

I think the best way to build up a good understanding of these things is to build very simple examples / functions and add a lot of @compileLog calls for different parts to look at the different types and values.

I think doing this carefully you eventually learn how to find out the specific types of things and how to gain insight when you are unsure, instead of having to simulate everything in your head and hope your mental model matches reality, instead you can use @compileLog to let the compiler tell you whether your idea of what the values are was wrong or not.

First of all anytype isn’t actually a type it is more like a hole/placeholder that can be filled by any type.

The language reference describes it as a declaration that can be used for function parameters:

The Zig grammar describes it as a keyword while types are handled as type expressions:

ParamType
    <- KEYWORD_anytype
     / TypeExpr

So with an example like this:

const std = @import("std");

pub fn main() !void {
    const A: type = type;
    const B: A = u32;
    const c: B = 15;

    @compileLog(A);
    @compileLog(B);
    @compileLog(c);
}

You get output:

temp15.zig:8:5: error: found compile log statement
    @compileLog(A);
    ^~~~~~~~~~~~~~

Compile Log Output:
@as(type, type)
@as(type, u32)
@as(u32, 15)

So you can see that type is its own type, that is why I can declare A as type and assign type to it, which is the same as const A = type;.

Now B is just const B: type = u32;, which is equal to const B = u32;
And c is const c: u32 = 15;.

A and B are types, the difference is that when you have a parameter of type type that parameter can be set to an arbitrary type.

With B however it is one specific type that will always be that specific type and the parameter can only be set to a value of that specific type.

(in a sense it is the same with A just that with type the value is an arbitrary type)

Types don’t really exist at runtime, types disappear after compilation because they get compiled into the program (you may still have some type-names, or enums, or organization patterns that remind you of types, but they don’t really exist anymore.

So a type can’t be used as a value at runtime, however types do exist at comptime and thus they can be used as values at comptime.

With this you are defining a function that takes a parameter called value and this parameter is a value and is expected to be a type.

fn List(comptime value: anytype) type {
    return struct {
        values: []const value,
    };
}

With this you are defining a function that takes a parameter called value and this parameter is a value and it can be any possible value that can exist at comptime and its type will be inferred.

But then because this line values: []const value, uses value as a type, it basically means that using anything that isn’t a type, for example "hello" will give you a compile error.

temp15.zig:5:25: error: expected type 'type', found '*const [5:0]u8'
        values: []const value,
                        ^~~~~

If you try to use anytype as if it were a type you get a compile error like this:

temp15.zig:24:21: error: expected expression, found 'anytype'
    const D: type = anytype;
                    ^~~~~~~
1 Like

Zig has some types which can only exist in comptime. One example is comptime_int, which is the default type of a literal numeric value. So these are equivalent:

const int1: comptime_int = 1;
const int2 = 1;

Which means you can’t do this in a function:

var int3 = 1;
// must be, for example
var int4: usize = 1;

Another comptime-only type is type. The type of comptime_int? Is type. It isn’t a superclass, there’s no type lattice in Zig, so it’s not “the type of all types”, it is the type of types.

Any expression which looks like these:

fn aFunc(v: T) T { ... }
var a_var: T = val;
const a_var: T = val;

Everything which goes in the place occupied by T is a type, of type type. Values of type type are legal elsewhere, but these are the positions where only such values are legal.

With one exception: anytype in this position only:

fn aFunc(v: anytype) T {...}

anytype is different. It’s a parameter-only keyword which can be used in place of a type, but is not a type itself. The expression @TypeOf(anytype) doesn’t compile, because it’s not a type, it’s a keyword, which is only valid in the type position of a parameter.

What the keyword does, is allow users of a function with an anytype parameter to pass an instance of any type (including type!) to that function. If the type of that instance is valid for the code which uses it, then the function compiles (specializes) a version for the specific instance type passed in. If it doesn’t work, then compilation fails. We call this compile-time duck-typing, because it’s meaningfully similar to the runtime duck-typing used in dynamic languages.

Note that it almost never makes sense to pass a type as an anytype parameter. Types are different enough from other sorts of values that you’ll almost always know that a comptime parameter needs to be a type, in which case, use type as the type. You’ll need to understand Zig very thoroughly and be working on specific and unusual kinds of code before you’ll find an exception to this rule of thumb.

I hope that helps. Be wary of analogies to a language like Typescript, the type system is very different from Zig’s and the comparison is mostly misleading.

3 Likes