What Should We _Remove_ from Zig?

I’m mixed on inferred error sets. I’ve started using Io in combination with Reader/Writers more seriously recently, and they do seem like a mistake there. Too easy to use a try, and then pass a WriteFailed too far up the stack to the point context is lost.

However, they have one important property I would want preserved: They only have the errors actually returned. This means if you stop returning an error from a function, a switch handling the result type 3 functions up might (correctly, good thing) start throwing a compiler error because that error isn’t possible anymore and the handling code needs to get chucked into the bin. On the other side of things, sometimes an API has a specific error set that’s not necessarily returned yet/ever, but is important to the contract. (eg, Reader.fixed never returns ReadFailed.) So you need a way to express both.

Main and test functions aren’t a problem, just use anyerror, or inspect the return type as an error union.

5 Likes

Huh. Looks like it?

I think I wouldn’t flat out remove inferred errors because they’re quite nice for prototyping. Of course anyerror would work to but it’s a bit difference.

I would instead replace them with the ability to annotate that some error isn’t in the return type(or in an error set for that matter). This would solve the problem with bubbling up {Read,Write}Failed and Cancelled across a certain point.

1 Like
  1. Remove if/while syntax for unwrapping error unions. I’m pretty sure I can count on on one hand the number of times I’ve actually used these. Usually it’s in some weird construction that is hard to read and I end up doing it thinking that it makes it easier to read, but in hindsight I’m not sure that it ever actually does.
  2. Remove catch expr. IMO this is almost always a footgun. It makes it extremely easy to accidentally swallow errors when a new error type is added to a function later, and it’s tempting to use only because catch |err| switch (err) { error.Something => expr } is quite wordy.
  3. Replace catch |err| expr syntax with catch { else => |err| expr }. In other words, catch would always be equivalent to catch |err| switch (err) in the status quo, except without needing to name the error capture. In addition to being more concise for 90% of cases, this reduces the potential for naming confusion around nested catches, e.g.
someFunc() catch |err| switch (err) {
    error.Something => {
        anotherFunc() catch |err2| switch (err2) {
            error.SomethingElse => {
                std.log.warn("anotherFunc failed: {}", .{ err }); // Oops!
            },
        };
    },
};

P.S. Note that while #1 would be a breaking change that would likely require manual fixes, #2 and #3 should both be possible to fix automatically by zig fmt:

  1. catch expr => catch { else => expr }
  2. catch |err| switch (err) { ... } => catch { ... } with |err| captures inserted on individual prongs when necessary
  3. catch |err| expr => catch { else => |err| expr } where expr is not a switch
16 Likes

It is possible to assert that a certain error isn’t possible to return from a function which I think can help with Reader/Writer interface stuff:

2 Likes

(we need a (official?) zig cookbook with such recipes)
sorry for offtopic

1 Like

I do agree that the removal of inferred error sets from the language generally would be better, but it is convenient during development, and then make it concrete once you know all which errors are possible.

My ideal would be that they are allowed in debug builds, but I doubt that will ever come to pass. My sole concern with total removal is that it adds one extra annoyance during development, similar to unused variables and var/const headaches. It would feel much nicer to have some of these language features relaxed, whether that be a command-line switch or build-mode.

2 Likes

I love zig error handling as it is, maybe even go as far to say its the best thing about the language. I’d be really disappointed if try or inferred errors were to go away. anyerror is the only thing that one should avoid, but even that can have its rare use.

10 Likes

Can smb explain why this community is obsessed with removing convinience features, or features that specific person doesn’t like/doesn’t use?

I think try keyword is nice, I don’t think we need to rework catch to make it more annoying to use, managed types are nice, infered error sets are nice, append is nice, anytype is also nice. Why does Andrew even care about tabs in comments, why is that issue still open?

Oh but in this very specific case for some very important system this feature would hurt if used, therefore we should remove it for everybody so I, the person who can’t control themselfs, don’t use it out of laziness.

9 Likes

Same idea that I had. Just making things harder for prototyping will just push people further away from even trying to hand-write code and use AI for everything.

try is great I agree, I don’t understand the issues some people have with it, since catch is always an option. but catch isn’t great as it is, all points raised by @bcrist are golden.

1 Like

Because much of the appeal of Zig is achieving a lot with a little, and the kind of programmer it attracts is likely one who is just as happy solving an issue by removing code than adding code.

Edit: “code” perhaps not the best term, features? Functionality? Complexity? I hope you get the spirit.

1 Like

I don’t like the idea of replasing catch with “catch |err| switch (err)”. i guess you can always type catch {else => } but that just adds text thb. Idk, maybe I just don’t use it enough and people switch on errors much more often, in which case I understand and will probably agree with you and him then.

The idea was to replace it with catch { ... }, removing the switch, and making catch EXPR illegal.

2 Likes

I don’t see removing complexity in replasing append with “ensureUnusedCapacity” and “appendAssumeCapacity” will remove complexity. I don’t understand how removing managed types removes complexity. If anything, it is nice just put allocator in there and write your code. You deallocate only the list itself, so having allocation on each append is just annoying.

Yes, the idea is (like the try keyword itself) to make the easiest thing also be the correct/safe thing.

In my experience, any use of catch that doesn’t explicitly list the errors it was intended to handle (using switch (err) {...}) is a future bug waiting to happen. But currently catch expr/catch |err| expr (where expr is not a switch) is easier to write than the more correct catch |err| switch (err) {...}.

1 Like

Yes, the idea is (like the try keyword itself) to make the easiest thing also be the correct/safe thing.

Ok, I will agree with you then. if you wana easy use try, if you wana handle with catch you shouldn’t have to write catch |err| switch (err) {}. Maybe keep catch EXPR, and have catch {}, but otherwise, yes, fair

I’m assuming it’s complexity in the Rich Hickey sense.

Edit: i.e. complecting reserving memory with updating memory or complecting data with what allocator the data is allocated with

2 Likes

I don’t think it removes complexity, but if append is the first thing you reach for you may default into suboptimal code. Consider loading objects line-by-line from a file, using append eventually the array list’s buffer will need to expand and copy it’s content, potentially multiple times. If you are allowed to edit the file format or read-ahead you can find out how many objects are needed ahead of time, allocate once and never copy. This also allocates precisely, less wasted space.

var list: std.ArrayList(MyObject) = .empty;
while (try reader.takeDelimiter('\n')) |line| {
    // any `append` could re-allocate the list; if MyObject makes
    // pointers out of `line` then all of our previous objects
    // may be freed and invalid!
    try list.append(allocator, MyObject.init(line))
}

// changing the file format to give us a count first
const count = try reader.takeInt(u64, .little);
var list: std.ArrayList(MyObject) = try .initCapacity(allocator, count);
while (try reader.takeDelimiter('\n')) |line| {
    // no try, no allocator!
    list.appendAssumeCapacity(MyObject.init(line))
}

And I think that’s a pattern with these recommendations, most here are trying to help point out pitfalls they have fallen into and realized produce worse code. I believe this thread asks in a round about way “how could the language change where if you are new and learning programming, you wouldn’t make mistakes?” Considering you can replicate the current append with those two calls to ensureCapacity & appendAssumeCapacity it would be safe to remove append (and shorten appendAssumeCapacity please)


If I had to pick on any feature it’d be the incrementor/iterator thing

var index: usize = 0
//                      v this thing v
while (index < maximum) : (index += 1) {
}

I just don’t like it, never use it, any time I’ve tried to use it I’ve found it way more confusing than a defer index += 1; as the first line.

Maybe the .? operator too, I don’t like funny symbols and orelse unreachable works the same while showing you unreachable can be replaced with anything.

I think this is the one that most aligns for me so far in the thread. Yes, it’s a convenience for stuff like foo.?.bar, but it “hides” an unreachable in a way that’s not conducive to spotting at a glance. In my code I try and avoid it whenever I can.

edit: I have spotted some usages where it’s needed, but it’s largely only at comptime:

fn callFunction(func: anytype, args: anytype) @typeInfo(@TypeOf(func)).@"fn".return_type.? {

I wonder if there’s a sensible way to allow .? only at comptime when it is verifiably non-null. That’d be nice.
Another annoying one is:

writer.err.?

But I think that touches on the other thing I’d remove from the language, these wrapped hidden errors - they feel frustratingly unsafe and hard to maintain.

2 Likes

I agree with, that there are better ways to write code, e.g. replasing append with ensure capasity and appendAssumeCapacity is safer, preallocating array is better.

This is true, not gona argue against it.

But understand me. I don’t need super optimised project that does stuff in nanoseconds. Idc if my array is reallocated, I don’t want to write (variable orelse unrechable) instead of .?. There is just stuff that a casual personal project doesn’t need to have that a serious project should.

I don’t want the removal of, what I think are nice convinience features that make coding faster and simpler. They may not be the best, but are good enough.

If your goal is to make so new users don’t do smth stupid, or remove common pitfals this is not the way, cause you are gona make language so hostile nobody would wana learn it anymore.

5 Likes