Parentheses in Control Flow Statements

I’m with you on this, but we’re unlikely to get it. There are some basic aspects of the syntax which come down to Andrew’s opinion on what is the clearest expression in terms of reading and understanding code. Parentheses around control-flow statements is one of those. I agree that they do a good job of visually separating what’s going on, I’m not as convinced that it’s actually important. What’s readable is inherently subjective, which always makes syntax questions a rich substrate for bikeshedding.

If I were Syntax Czar the language would look pretty different:

  • Optional semicolons
  • Mandatory braces on if / else, except on a single line†
  • Optional parentheses for if and for, also optional for a one-clause while, but not two-clause: so while i < thing.len {...} is legal, while (i < thing.len) : (i += 1) {...} is required.

Thing is, I don’t think that the status quo has made any bad decisions. The longer I use the language, the less convinced I am that making semicolons optional would constitute an improvement. It would make things cleaner, in my subjective aesthetic judgement, but not necessarily clearer, and I do agree that the latter is most important.

Mostly it just doesn’t matter much, I’ve used enough languages that syntax is largely background noise. Unless it’s getting in my way, which Zig’s basically doesn’t. Although see footnote.

if assignments would use something which I think the language should have anyway, which is that assigning if can use break (but statement if cannot).

const a = if pred {
     break 5;
} else {
    break 7;
}

Or just add the parens back for a real proposal, because while I would prefer the ability to drop parens, it’s not something I’d go to bat for.

I not-infrequently do an awkward dance to get around this lack already:

const a = a_val: {
    if (predicate) {
        const intermediate = aThingIHaveToDoFirst();
        break :a_val outer_var + intermediate;
   } else {
        break :a_val outer_var;
   }
};

The extra layer of scope and the label aren’t really bringing much to the table, they’re just letting me both do something in an if statement and return a value from it.

That also means that there’s a lot of extraneous work involved in converting a simple assignment if to this form: I have to add the braces (which aren’t allowed in the assignment form), wrap it in a scope, think up a label, and turn both prongs of the if into break statements.

1 Like

There’s also an ambiguity thing which needs to be recognized:

if (moe | shemp | curly) { ...}

// Without parens, it could be a capture
if moe |shemp| curly {...}

Illustrating that this would require lookahead to resolve, which has actual cognitive consequences. What’s hard for a parser to parse is generally hard for a human as well.

There are ways around that, require parens for a capturing if is the straightforward one, and it’s consonant with requiring parens for two-clause while. But the status quo gets to avoid these little wrinkles, and that’s beneficial.

2 Likes

Personally I don’t have a big interest in optional parenthesis and that wasn’t (in general) what I was arguing for, instead I was arguing for being able to replace the whole the ( ... ) of the for loop with a tuple specifically.

I think it makes sense for the language to avoid too many special purpose syntax constructs and if they exist keep them fairly similar to another.
But we already have several syntactically different if statements that look similar but are actually distinct if(<boolean>), if(<optional>), if(<errorunion>)

So for(<tuple>) |a, b, c| wouldn’t be so unexpected, I just would find it personally less confusing as for <tuple> because with the former I would expect the capture to be just the elements of the tuple, instead of a multi capture for loop.

2 Likes

Ah, that wasn’t clear to me.

On the one hand, an if (tuple) |val| statement is a natural extension of the existing syntax for arrays and slices, on the other hand, it means that val doesn’t have a well-defined type. I think that’s a problem actually.

But I think we’re talking about a tuple of slices, then destructuring each of the slices, and that could work. We’d need some way of syntactically distinguishing a destructuring of a tuple-of-slices, from a destructuring of a slice-of-tuples, or a function call returning tuples or null.

I don’t think implicitly destructuring by dropping the parenthesis is a great answer to that question though, it’s too implicit for my taste. I don’t really have a better answer, either. for (.{tuple_of_slices}) |a, b, c| is my first thought, but that definitely overloads the vocabulary in an unexpected way.

What we have already is for (tuple[0], tuple[1], tuple[2]) |a, b, c|, and it might be hard to beat that. It’s explicit and reasonably compact.

But what if tuple[1] is a slice of tuples? Should we allow for (tuple[0], tuple[1], tuple[2]) |a, (b, c, d), e, f|?

Hmm. I think that would be fine actually. How features like this cooperate with each other, and the rest of the language, is always the hard part.

1 Like

What about having a general @splice that splices the elements of a tuple into an argument list?

So you could use for(@splice(tuple)) |a, b, c| or function(@splice(args_tuple)).

The latter is already possible via @call but still @splice would be a more direct ability to replace a function call when your arguments are in a tuple.

Maybe @splice could also be used in const array:[_]u8 = .{ @splice(header_bytes), 0, 255, @splice(meta_data) };

So maybe it could even be a replacement for ++?

These are half finished thoughts, I would have to think more about it.

I guess yet another possibility would be to have a @for that expects a tuple and otherwise functions like a for, similar to how @call is used to make function calls with tuples.

1 Like

I think @split would be more accurate since splice means to join or concatenate, no?

3 Likes

Splice into the surrounding argument list context, I am used to splice from lisp/Racket land, but split also would make sense.

2 Likes