`anyerror` is a bad practice

Hmm, maybe you could argue for replacing anytype with a typing/declaration system where comptime parameters can be implicit if they can be inferred.

One part that annoys me about Zig is when I repeatedly need to write MyTypeCalculatingFunction(@TypeOf(val)) for both the return type expression and the result variable within the function.

One thing I had considered is what if we had comptime variables associated with a function definition something like:

with {
    const T = @TypeOf(val); // this is sort of tautological
    const R = MyTypeCalculatingFunction(T);
} fn func(val:T) R {
    var res: R = undefined;
    for(std.meta.fields(R)) |f| @field(res, f.name) = .empty;
    return res;
}

Or to avoid tautology have an infer keyword:

with {
    const T = infer type;
    const R = MyTypeCalculatingFunction(T);
} fn func(val:T) R {
    var res: R = undefined;
    for(std.meta.fields(R)) |f| @field(res, f.name) = .empty;
    return res;
}

with would basically add a syntactic way to have comptime variables associated with a function, so that you can avoid repeating expressions.

Or without with:

fn func(val: infer T) MyTypeCalculatingFunction(T) {
    const R = MyTypeCalculatingFunction(T);
    var res: R = undefined;
    for(std.meta.fields(R)) |f| @field(res, f.name) = .empty;
    return res;
}

Having infer type variables could be an alternative to anytype, in this example it would be basically equivalent to this code:

fn func(val:anytype) MyTypeCalculatingFunction(@TypeOf(val)) {
    const R = MyTypeCalculatingFunction(@TypeOf(val));
    var res: R = undefined;
    for(std.meta.fields(R)) |f| @field(res, f.name) = .empty;
    return res;
}

I think some kind of infer would probably be nicer syntax, but it also may require more complexity in the compiler, not quite sure.

Regarding one way of doing things, I don’t think forcing the user to explicitly type the type every time is better (if that is what you mean), you already have the type of the thing when you write const my_value:u32 = 5 having to type it again when calling doSomething(u32, my_value) isn’t the same as being able to just call doSomething(my_value).

So said another way there is one way to call a function with explicit parameter types and one way to call a function that has implicit parameter type.

I think some kinds of implicit are good.
If everything was explicit you would have to type the type of the tuple value given to print/format functions and I think that would be horrible, also at that point the Zig language would feel more like writing an intermediary format.

But in the case of generic arguments that can accept different types there isn’t a compelling precise way to denote what is expected, all the ways I have seen to express a contract for generic arguments have been math-ish looking expressions that are defined in a parallel type-expression language which is simple to read for simple cases and almost in-decipherable for complex cases, with Zig we already can write comptime code to make calculations based on types and that seems better to me than having a separate dsl notation for expressing type constraints.

You then could put that code in a function and give it a name, but then you are back to learning a bunch of vocabulary for types classified/named in different ways.

I am not completely against being able to specify type constraining functions, but it has its own downsides (more complexity in the compiler, more vocabulary, potentially disallowing types that would have worked but are rejected because of rigid type checking logic, instead of embracing duck typing). And the issues that proposed similar things are closed, so it seems unlikely.

anytype is simple and you can write type checks where they are really necessary and use ducktyping anywhere else, if you use good names for your anytype parameter then that already explains what to pass in 90% of cases, for the rest 8% you can use documentation comments and pop open the detailed lsp-info for the function which shows you those documentation comments, for the last 2%, you can just pass something and let the compiler errors guide you, which is more likely correct and precise than any manually defined constraint-expression, which either would have to capture the type with excruciating detail (so people would resent having to create those descriptions) or would be subtly wrong for example by disallowing more types then necessary / over-generalizing.

Also technically you already can do something like concepts in Zig, which is similar to constraints but just using existing Zig code (and the benefit is that it doesn’t require learning another language, or set of names for concepts, or even combinators, or even thinking in functional combinator logic instead of just zig code), for example this just uses a block expression to define the result type and does some type checking alongside:

pub fn add(a: anytype, b: anytype) R: {
    const T = @TypeOf(a, b); // types need to be compatible
    switch (@typeInfo(T)) {
        // allow numbers
        .int, .float, .comptime_int, .comptime_float => break :R T,
        else => @compileError("not supported"),
    }
} {
    return a + b;
}

pub fn main() !void {
    const a: u32 = 7;

    const res = add(a, 35);

    std.debug.print("res: {}\n", .{res});
}

const std = @import("std");

Which shows up like this if I ask the lsp for the function info:

In combination with what I wrote in the previous section, I think it would be more like adding another lego brick into the picture that creates “Zig code itself is enough to describe type constraints / concepts”.

I think instead of trying to import the solutions from other languages (which usually is “invent a new language which can do most of what the actual language already can do but differently and likely slower”) we should embrace the strength of Zig, meta-programming? write Zig that runs at compile time, build-system? write Zig that runs at build-time…

I think it would be better to have a solution that is just a slight tweak on how things work and then have the solution fall out of that as a lucky find.

Basically having orthogonal features combine in ways that creates many possibilities, instead of creating too many overly specific solutions that don’t actually combine in a way that creates multiple benefits at the same time.

6 Likes