What is the one features you would want in Zig from another language?

Disclaimer, this is not an attempt at asking for a feature, I’m just curious about the ONE feature, that people are missing in Zig ? why would you want it ? How would you see it fit in the language ? And I’m also curious to know if there is already some ways to achieve the desired behavior.

I for example would really want to have the combo union(error) + match, from Rust. The way I see it, this is a very nice and cozy way of handling errors, while also providing good opportunity for good error messages, and it’s also in my opinion a nice way to use other’s code, instead of looking through countless pages of documentation (when it exist), you can use someone’s code intuitively, and let the compiler walk you through what the API wants. I know this can be somewhat achieved through union(enum) but it’s a bit cumbersome to use, and it doesn’t work well with the rest of the language. At the very least I would really like to have it in debug mode, when trying to build some code.

4 Likes

From language:
comptime interfaces like rust traits.

From stdlib:
go style I/O where you program something that looks and feels blocking but the thread continues doing something else until I/O finishes. No async/await, no callback, same code as blocking mode.

7 Likes

Hm yeah comptime interface would be so nice to have too, honestly they were second on my list, they are already kind of achievable, but having true native support for them would be such a cool feature to have, especially in Zig.

1 Like

I know you said to only name one, but I got 3 things that I miss.

  1. Stability - kind of obvious, hitting compiler/std bugs really slows me down sometimes.
    This will of course just come with time.
  2. Compilation Speed - I know Zig is already pretty fast, but coming from Java, where compilation is trivial and optimization is performed at runtime for hotspots only, I would really love to have faster compile times.
    Again this will be solved eventually, once Incremental Compilation and/or the x86 backend+linker are ready.
  3. C-style for loops
    I like how expressive the C-style for loop was. Zig’s solutions on the other hand feel a bit clunky.
    Range based for loops are great, but they only support usize which leads to plenty of casts.
    The while loop syntax still feels kind of weird, and the loop variable bleeds into the surrounding scope, which is pretty annoying.
    But I understand why things are the way they are, and I don’t really have a magic solution either. Just using C-style for loops would not really fit the language in my opinion.
10 Likes

I agree, with what you brought, like you said this can only come with patience, also for the for loop, I personally prefer the while loop, because also coming from C, I think while loop are harder to mess up, and are more explicit. Especially considering, that the “meaning” of a for loop, in most programming languages, tends to be much more like a for each, than a for (size_t i = 0; i < N; i++); but yeah expressing, negative ranges is a bit of a pain.
At least the while loop as the : while (i < N) : (i += 1) {}; which is convenient. It’s also funny that you brought Java into the conversation, because recently I’ve said in the post “Should the std be batteries included” that I though it should, and I answered that thinking about Java’s API, I don’t like a lot of things about Java but credit where credit is due, they have a very complete and useful API, especially Lock/Lock Free version of DTS.

1 Like

My top four has already been said:

  • pattern matching,
  • comptime interfaces (i’m doing a package for that),
  • stability,
  • incremental compilation (this will be so amazing),

The next one is not THE feature I want zig to have, I think it could be nice

The infer keyword instead of anytype. It’s not from another language (as far as I know), it’s from this proposal. It would work like that:

fn genericFunction(arg: infer T) Return {
    // Now you can use both `arg` and `T`
    ...
}

This is a very basic usage, but maybe you could also do stuff like that:

fn anotherGenericFunction(array: [infer len]Type) ...;
fn anotherGenericFunctionAgain(pointer: *infer T) ...;
// and so on...

So yeah. Better generics.

4 Likes

Yeah I prefer the “infer” as compared to “anytype” or ‘_’ I think it’s more explicit especially considering that it’s also used to discard values, i think using a more explicit keyword would be better, great suggestion.

Is the union(error) + match not equivalent to something like:

const ok = fallible(a, b) catch |err| switch (err) {
    error.A => {},
    error.B => {},
};

Asking out of curiosity mostly.

1 Like

Okay, so i wrote mine up and realized that what i want is doable with copious usage of functions, but I thought i would place it here anyway, since i wrote it up.

Mine’s less of a feature and perhaps a syntax change, as the feature is supported, i just dislike the way it looks…

I’d like to be able to yield/return/express values from more complicated match statements without using labeled breaks. Similar to how in rust if the final Item is an expression, that expression is yielded to the outer scope. Turn this:

const val = out: {
    switch (input) | x | {
        1..0xFF => { 
             // Some transformation
            break :out result 
        }
       _ => //Other Stuff
    }
}

To something I think is less littered with odd syntax.

const val = switch (input) | x | {
    1..0xFF => {
        // Some Transformation
        result
    }
   _ => //Other Stuff
}

I think this in particular is annoying to me because simple switches (i.e ones that don’t define blocks) do express their value. I know that there are simple workarounds (functions etc.) so I understand if no one else sees value in it.

1 Like

Actually, did you know you can label switch prongs instead of creating a top-level block for breaking from the switch statement? In particular, your first snippet could be rewritten into something like:

const val = switch (input) {
    1..0xff => |x| blk: {
        break :blk x;
    },
    else => 0,
};

Also, note that switch doesn’t use capture |x| syntax.

6 Likes
  1. Error payloads
  2. Comptime interface
4 Likes

Yes you can but in doing so you loose the ability for errors to trigger errdefer, and most importantly, you can’t really attach values to your errors, the idea would be more something like :

fn foo(value : i32) error{notInRange}!i32 {
  if (value >= 0 and value <= 42)
    return (value - 1337);
  else
    return notInRange("foo expected values in [0-42] not {d}\n",.{value});
}

pub fn main() !void {
  const value :i32 = 43;
  const result = match(foo(value)) {
    .Ok => |v|,
    .Err => |e| std.debug.print("{error}",{e});
  }
}

The idea would be something like this, of course this is a very silly example but the point would be that when you get a stack trace, each stack frame can expose a more descriptive issue with your inputs, this can guide you quickly into identifying the root of our issues, of course the way zig does tracing is already pretty good and useful on it’s own, but I think being able to have values and message, in the stack trace itself would be pretty neat, especially when some functions output depends on some other functions inputs. Being able to quickly see which one is crapping the value with nice error messages would be good.

Let me ask you this then, where do you store the attached string to your error variant? On the heap? If so, how do you allocate?

3 Likes

In practice, I’m not sure one Idea that I wanted to explore, was to use the global error enum and comptime, to allow the compiler to build at compile time in static memory a sort of enumMap of all the values attached to each error path. Such that when an error occur, the compiler use the specific branch enum value to go fetch the associated fmt string with it. I’m not sure how feasible it is, but there must be a way to achieve this without dynamic memory allocation.

1 Like

Sure but then how can you provide meaningful errors to the user if the error depends on the input data? I don’t think you can achieve this without dynamic memory allocation or at least not without possibly unrolling every possible value which is not feasible.

3 Likes

Sorry my explanation is not very good what I mean is :

fn foo(value : u32) error{a,b,c}!u32 {
  return switch(value) {
          0..10 => value + 10;
         11..100 => error.a("value {d} not in range [11..100]\n",.{value}); // branch a1
        101..200 => value + 11;
        201..300 => error.b("value {d} not in range [201..300]\n",.{value}); // branch b1
        301..400 => value + 11;
        401..500 => error.c("value {d} not in range [401.500]\n",.{value}); // branch c1
   }
}

fn bar(value : u32) error{d,e,f}!u32 {
  return switch(value) {
          0..10 => value + 10;
         11..100 => error.d("value {d} not in range [11..100]\n",.{value}); // branch d1
        101..200 => value + 11;
        201..300 => error.e("value {d} not in range [201..300]\n",.{value}); // branch e1
        301..400 => value + 11;
        401..500 => error.f("value {d} not in range [401.500]\n",.{value}); // branch f1
   }
}

pub fn main() !void {
   _ = match(foo(9)) {
          .Ok => |v|,
          .Err => |e| std.debug.print("{error}",.{e});
     }
     _ = match(bar(12)) {
          .Ok => |v|,
          .Err => |e| std.debug.print("{error}",.{e});
     }
}

So basically in this example, the compiler would look at the code, and see all the branches that can trow an error, and use each error exit point, to build in conjunction with the global error enum set, a comptime HashMap, in static memory, of each branch with it’s corresponding fmt or at least room for the value to be copied, and at runtime the error value + branch id or idk something to identify where exactly the error is coming from, is used to fetch the fmt string plus the values associated with it, and then it’s just a normal std.debug.print, maybe each exit path get some room to write the value or something honestly I’m not sure if it’s feasible at all without using dynamic memory allocation, but given how good the compiler is I’m sure there is some way to at least emulate some of what I’ve mentioned. Or maybe we can enable this feature only through a specific allocator, such that it’s not part of the language by default but you can opt in with the correct allocator.

1 Like

Permitting dynamic allocs globally requires hidden allocations which is against Zig’s ethos of no hidden allocations. On the other hand, having a fixed-sized buffer would be very difficult to maintain and then you would still end up having to copy the messages out to some globally allocated buffer to avoid clobbering the messages in the buffer; again, hidden allocs.

We do actually have a pattern for this btw which would accomplish everything you wanted and without presupposing heap-allocated error payloads in place of simple error enum. In zld for instance, this pattern is common kubkon/zld/src/MachO/Object.zig#L559:

            macho_file.base.fatal("{}: unexpected symbol stab type 0x{x} as the first entry", .{
                self.fmtPath(),
                open.n_type,
            });
            return error.ParseFailed;

This pattern lets the developer handle any auxiliary data while keeping the error enums cheap. In this case, we dynamically alloc the message in the global state but we do so explicitly. Is that what you are looking for?

4 Likes

If it does require dynamic memory allocation, than sure I don’t want Zig to do this behind my back, and if so I will live just fine with the current state, or maybe as I get more proficient in Zig, maybe I’ll find a way to achieve some form of error enum with payload. What you proposed seems like a good alternative in the meantime. But I wonder if this couldn’t be part of a special allocator. Because if you think about it what I’m asking for is already kind of part of in the language in some sense. When you get a stack trace, you get some formatted string of what went wrong where. How big would be the jump of just adding one value from the runtime inside that trace ?

1 Like

Ah yes, I’m mixing syntaxes.

So stack trace printing is actually based on unwinding the stack from the current function frame. Your Zig program will mmap its own binary, read debug info sections and/or EH frame info (or compact unwind on macOS) and use that in combination with base pointer register to actually walk up the stack printing the source lines in reverse. Any error while walking the stack triggers panic during panic and aborts as far as I remember. What if there was an error in the custom error handling you just described that lives in the compiler’s internals and not on the user side? When dealing with errors explicitly how I suggested, you keep stack trace for fatal and effectively unhandled errors/panics which are decoupled from handling custom error payloads. While being more verbose and explicit, I actually rather enjoy having complete control over the error payloads rather than increasing the complexity of the standard library and the compiler.

4 Likes