Why marking function arguments of type type with comptime?

Let’s look at an example from the docs, generic/polymorphic max function:

fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}

We know that this function can only be “called” at compilation time
and this “calling” means that the compiler will generate a code for a specific T.
But isn’t a type of an argument T (which is type) alone enough for the compiler to “understand” what a programmer wants to say?
After all, types are values only when compiling and since we see such a type we know that function is comptime only function, which will be mono-morphized when being “invoked”.

I can guess that in this case comptime keyword is kinda
compiler helper - it lets it know about comptime stuff in advance,
before it will see T : type, i.e. without lookahead.

If so, I can imaging some other places for comptime keyword:

comptime fn max(T: type, a: T, b: T) T {
fn comptime max(T: type, a: T, b: T) T {
fn max(T: comptime type, a: T, b: T) T {

So, why compiler can not do without comptime before T: type (or in some other place)?

3 Likes

So first of all, these wouldn’t be a good idea:

comptime fn max(T: type, a: T, b: T) T {
fn comptime max(T: type, a: T, b: T) T {

This to me would imply that all arguments, including a and b must be evaluated at compile time, while in reality we only want to evaluate T at compile time.

Secondly it seems that zig can work fine without the keyword, in fact with some trickery we can work around the keyword. Consider the following functions:

fn fun1(comptime Type: type, T: Type, x: T) void {
fn fun2(comptime Type: type, comptime T: type, x: T) void {
// Called like this:
fun1(type, i32, 0);
fun2(type, i32, 0);

The second function needs the explicit comptime, while the first one, despite technically doing exactly the same things, does not need the comptime in front of the second parameter.

I agree that in the function signature comptime T: type the keyword comptime is redundant. I can only guess that requiring it regardless is done for syntactic uniformity with the cases when it is required. For example, when specifying comptime known integers.

4 Likes

I would interpret comptime fn ... as a permission to use type as arguments’ type and, probably, there must be at least one such argument.

Would it be correct to state that comptime keyword is used for two (or maybe more) completely different things:

  • actual calculations during compilation
  • automatic code generation (polymorphic functions, generic data structures)

?

Yes, it is used for different things.

  1. comptime blocks just run the code in compile-time.
  2. comptime expressions evaluate the expressions in compile-time.
  3. comptime function arguments perform partial evaluation of the function on each call by using the compile-time defined argument.
2 Likes

Is it possible that comptime is here not to help the compiler (which does not need it, as you noriced) but to help the human reading the code?

Well, for me type type by itself (either in args or in return type) is quite enough to make a conclusion that this is comptime function, otherwise I wouldn’t ask.

1 Like

To @dee0xeed’s point, you can make comptime fields by only specifying that type is the type of a field without the keyword comptime.

Exactly. Something like this would look odd as hell:

fn hello(T: type, comptime value: anytype) T {
    // ...
}

I am afraid, I do not see no relation between “partial evaluation” and what the compiler is doing in case of parametrically polymorphic functions or generic data structures, i.e:

fn max(comptime T: type, a: T, b: T) T { ...
// or
fn List(comptime T: type) type { ...

In these cases (when “calling” such functions) the compiler does not “evaluate” nothing in mathematical sense, it generates a code for specific T. Please correct me, If I do not understand this appropriately.

The Zig contribution of types as first-class values in comptime means that you can also view the process as a partial evaluation.

1 Like

The fact that types are first-class citizens in comptime only means that comptime in comptime T: type is not needed.

You are right about that.
Since type is comptime only value, the compiler can derive that it’s a comptime argument.
But I don’t need to skip the comptime keyword and it’s easier for me to read the code when it’s there.

Ok

const print = @import("std").debug.print;

fn incr(i: u32) u32 {
    return i + 1;
}

pub fn main() void {
    var k: u32 = 0;
    comptime var j = incr(9);
    print("j = {}\n", .{j});
    k = incr(j);
    print("k = {}\n", .{k});
}

No questions. I wanted my j to be computed at compile time, I got it.

But “generics” are absolutely different story. They are not about computation (doing something with numbers or strings), they are about code generation. For me it looks like same keyword (comptime) is used for very different purposes.

A bit of airy dreams: :slight_smile:

generic fn name(T: <arg-type>) <return-val-type> {...}

This generic in front of fn would mean “We are going to use type for <arg-tyoe> and/or for return-val-type”.

If you have more than one arguments, at least one generic and at least one not generic, you need a mechanism to mark the generic arguments.
e.g. generic<T> fn name(T: G) G {...}
That is accomplished in zig using the comptime keyword in front of the argument:
fn name(comptime T: G) G {...}

Again - if an argument is of type type, it must be known at compile time.
If there are no such arguments => compilation error.
But consider this only as my fantasies.

Yes, this is the reason. I got it.
An example from language reference makes it clear:

fn firstNPrimes(comptime n: usize) [n]i32 {
2 Likes