More syntactically cleaner way to check if a something is an error from an error union

Completely agree.

It’s also worth mentioning that if you’re worried about inlining, that’s also a trivial problem to solve:

inline fn isSemVer(...)

I’d also like to point out a design philosophy that is lurking in the background here that @chung-leong has addressed:

The conversion here is verbose/explicit on purpose.

Now, that said, I have made posts about the overuse of try as that can be a way to ignore errors entirely (just search for Zig Code Smells on this forum and you’ll find the topic I’m referring to).

As far as I’ve seen, the answers given above (such as the answer by @dimdin) are about as elegant as I expect Zig to afford as a matter of design choice.

2 Likes

And (going back full circle here) with an if expression, you can make that function a one liner:

return if (std.SemanticVersion.parse(v)) |_| true else |_| false;
3 Likes

I gotta admit, I find that one substantially less clear.

if is pretty idiosyncratic in Zig. You can use an ordinary boolean value, or you can unwrap and capture an optional, or you can split a return value between the expected and the error union.

That’s powerful but it creates some syntactic overload. The only trace of evidence that you’re responding to errors, and not unwrapping an optional, is in the else |_| clause. The two line version has catch in it which makes the error condition obvious.

Sometimes the else-clause-capturing if statement is the clearest way to deal with things, but given the topic, I wouldn’t call the one-liner “syntactically cleaner” .

I also think it’s mildly unsatisfying that you can’t handle an optional with if (!optionalReturning()) statement else |capture| statement, if that order makes more sense to you. And that it’s illegal to use capturing if with error-else clause if no error is possible. Defensible decisions, but it offends my sense of symmetry.

1 Like

At some point I was disappointed that you can’t write something like this:

if(booleanCondition() and optionalResult()) |value| {
   // do something ...
}

But after thinking about it for a bit I realized/concluded that it is probably better without that. If this mix of boolean and optional was allowed you also would expect something like this to work: if(booleanCondition() or optionalResult()) |value| { ... }
But what would that mean? If booleanCondition evaluated to true you would expect short-circuit behavior and the branch to be taken, but now value would be true instead of the payload of the optional, so I don’t think these things (making optionals behave like booleans), really make sense, they seem nice at first, but because they have important semantic differences, it seems they break down and only seem to make sense in some of the situations, while becoming confusing in others.
I think those semantic differences remain easier to reason about, if the syntactic forms remain dedicated to dealing with their specific types.

If we wanted some level of intermixing of types at that syntactic level, we should research whether there is some algebra of these types that actually integrates the different aspects in a way that is easy to understand and composable.
And only if somebody finds such a way to integrate those semantically in a satisfying way, then it would probably also result in a syntax that is satisfying, without the drawbacks of taking an approach that is more complex then the current minimal syntax.

Not being able to use error-else when there isn’t any error, is consistent with not being able to use a else prong in a switch that handles every case.

3 Likes

Hard agree. That’s truthiness, and truthiness doesn’t belong in any language which isn’t Lua.

What if has is overloading. I think that’s ok, but there’s a learning curve on it. One could imagine a language in which optional-unwrapping if is spelled given, and error-trapping if is spelled should, for instance:

given (maybeReturn()) |have| 
    fooUpon(have) 
else 
    callNullThing();

should (dodgyFunctionSucceed()) |did|
     barUpon(did) 
else |err|
     itsAnOopsie(err);

Zig isn’t at a point where a change this fundamental would be made, of course. But it would avoid beginners getting confused when they can’t or an optional with a boolean.

Doing if (val == null) instead of if (!val) is fine too. I would like it better if both worked, and in fact worked for all three meanings of if. if (!possible_err()) |err| crashImmediately() else |val| allIsWell(); is cogent, and this is in fact what ! means in a type context.

Or maybe I wouldn’t actually like it once I tried it. It runs the risk of making already-ambiguous code that much harder to read.

1 Like

Honestly I would never use a language that would allow this sort of a pattern or anything similar to this. Its a straight up footgun at the very least. What I had mentioned in my post is actually about explicit conversion of error unions into a boolean.

Agreed, sometimes the only way to tell if is unwrapping an error or an optional is to look at the corresponding else clause’s capture. Like if it exists, its an error handling if otherwise its an optional handling.

BTW I feel the same for the const keyword. I can never clearly figure out if a const statement is compile-time constant or a runtime-constant without looking at the right-hand expression. It feels a mix of both Rust’s const and JavaScript’s const (ignoring thr fact that JS is a interpreted language)

Can’t say for others as its quite subjective, but I don’t quite like that. I always take the if block as the positive action (even if the conditional is some sort of negation) and the else as the negative case. Just fits me better; again quite subjective.

2 Likes

[quote=“Sze, post:24, topic:4312, full:true”]
At some point I was disappointed that you can’t write something like this:

if(booleanCondition() and optionalResult()) |value| {
   // do something ...
}

It wouldn’t be too weird if there’s no mixing of bool and optional:

if(optionalResult1() and optionalResult2()) |value1, value2| {
   // do something ...
}

Not particularly helpful though, since you’ll inevitably have lines that are too wide. Short-circuiting on multiple lines is cleaner and easier to debug:

fetch: {
    const value1 = optionalResult1() orelse break :fetch;
    const value2 = optionalResult2() orelse break :fetch;
}

Plus that works with any combination of bools, optionals, and error unions.

3 Likes

Although maybe a bit jarring at first, there’s this for the and case:

if (booleanResult1()) if (optionalResult()) |value| { ... };
2 Likes

I quite agree with @Calder-Ty @mnemnion and others posts on how Rust’s errors and Zig’s errors are different and how their error handling strategies differ. However I am not convinced in anyway that you cannot check if a function returns an error without using a some *tricks* per se. I am not sure if this is possible but if the devs can do some compiler magic so that we can just write something like ( I know error is a keyword, please do not point it out)

if (return_error_union == error) { ... };

that will be all that I ask for

This seems like it would be the Zig way to do it. Right in line with optional == null.

It seems like what you are looking for might be more like a switch statement ? I think those are a pretty neat way of handling error case ?

Yup that’s exactly what I was wanting for for the whole time

Switches work but only when you need to unwrap something, not when you are chaining conditional logic.

1 Like

But null is a single value, whereas in the case of error union you’re matching against multiple possible values. Something like this would be more semantically sound:

if (result == anyerror.*) {
     // ...
}

But that sort of changes the nature of the == operator. The problem here is that there’s no in operator in Zig.

1 Like

I don’t know, it seems like anyerror is some sort of a pointer dereference.

It’s lousy syntax, I know. An in operator would be preferable:

if (result in anyerror) {
   // ...
}

Currently you can write:

const result = createError();
if (result) |_| {} else |_| {
    // ...
}

Would be kind of funny if you could just drop the then-case of the if statement:

const result = createError();
if (result) else |_| {
    // ...
}

Personally I find the currently possibly way good too, it is a bit of visual noise that draws attention to the then branch that does nothing and makes it more likely to notice that the multiline scope is for the else not for the then.

1 Like

There’s a way to handle all of these, which preserves the property which we don’t currently have: for all if (a) A else B;, we can also do if (!a) B else A; with the same meaning.

Fundamentally, it’s a requirement that an if on a !?T have two else branches, to handle all three cases. That would be a breaking change, but a detectable one which would have only local consequences and be easy to fix. The problem is that there would be, potentially, a lot of these to fix. I don’t know how much !?T appears in real code, but it’s a distinctive pattern which would be easy to search for, if someone wanted to try answering that question.

Errors are checked first, then optionals, then the value. So let’s say that the return signature of the function f() is !?T

if (f()) |t_val| {val_case} else {nil_case} else |err| {err_case};
if (!f()) |err| {err_case} else {nil_case} else |t_val| {val_case} ; 

This would also change the semantics for ordinary if statements (from the documentation):

// Right now it works like this

const a: anyerror!?u32 = 0;
if (a) |optional_value| {
    try expect(optional_value.? == 0);
} else |err| {
    _ = err;
    unreachable;
}

// But it could work like this:

if (a) |value| {  // note missing .? below
    try expect(value == 0);
} else {
    // here, we handle the null case
    // proposal for how to signal "we do nothing here" :
   _ = _;  
} else |err| {
    _ = err;
    unreachable;
}

I would prefer this. A !?T has three return branches, so it should be an if and two else, in that order, mandatory. Using ! on the predicate reverses the order of those three branches.

For just ?T and !T types it’s simpler:

if (!might_work()) |err| {err_case} else |t_val| {val_case};
if (!optional()) {nil_case} else |t_val| {val_case};

Unlike the idea of unique keywords for the three faces of if, I think this would be a good semantics for the language to have. It would be a pretty disruptive change, probably worth rejecting on that basis. But it does provide consistency for all permutations of boolean/optional/error.

You’d need to provide an inner if/else to handle !?bool, ?bool, !bool, as you do now. To do otherwise would be overly complex. My first draft had if (!!f()) and even if (!!!f()) and it was completely insane.

But I would venture the opinion that “if on a !?T must have all three branches” is in keeping with the spirit of the language. It’s much like exhaustive switching on enums: handle all your cases.

We can also do this, I believe:

const result = createError() catch null;
if (result == null) {
    std.debug.print("Some error happened\n", .{});
}

result should be an optional of the error union’s payload.

2 Likes