ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
34.85k stars 2.55k forks source link

documentation example: for else #2614

Open vishen opened 5 years ago

vishen commented 5 years ago

The documentation example for for else (https://ziglang.org/documentation/master/#for) doesn't run the else branch of the code:

test "for else" {
    // For allows an else attached to it, the same as a while loop.
    var items = []?i32 { 3, 4, null, 5 };

    // For loops can also be used as expressions.
    var sum: i32 = 0;
    const result = for (items) |value| {
        if (value == null) {
            break 9;
        } else {
            sum += value.?;
        }
    } else blk: {
        assert(sum == 7);
        break :blk sum;
    };
}

I don't know if the break is then supposed to cause the else block to run? But currently the else block isn't actually run.

I was just wondering what the expected behavior is here?

shritesh commented 5 years ago

When you break from a while/for loop, the else branch is not evaluated. I’ve submitted #2641 to clarify that in the docs.

vishen commented 5 years ago

Would it also be useful to have the example use the else branch in the documentation? For me, who is new to Zig, it was a bit confusing having an example that didn't use the else branch here (since it breaks out when it see's a null and therefore never runs the else branch).

andrewrk commented 5 years ago

Would it also be useful to have the example use the else branch in the documentation?

Yes, I think so

vishen commented 5 years ago

I was able to make the else branch run using the following (https://github.com/ziglang/zig/pull/2642):

test "for else" {
    // For allows an else attached to it, the same as a while loop.
    var items = []?i32{ 3, 4, null, 5 };

    // For loops can also be used as expressions.
    var sum: i32 = 0;
    const result = for (items) |value| {
        if (value != null) {
            sum += value.?;
        }
    } else blk: {
        assert(sum == 12);
        break :blk sum;
    };
    assert(result == 12);
}

However, I am wondering if this is the correct behaviour? I am probably using this wrong, but to me it looks like the else branch is only run once the for-loop has finished iterating.

emekoi commented 5 years ago

if i understand correctly the else branch is only run if the loop doesn't break.

andrewrk commented 5 years ago

The most helpful way to think of it is like the else of an if statement, but for a while loop. Specifically, for an error union:

while (getNextItem()) |item| {
    doSomething(item);
} else |err| {
    std.debug.warn("stopped processing items: {}\n", err);
}

Without the else there would be no way to capture the err value, which is the main reason that loops have the else clause.

daurnimator commented 5 years ago

But a for loop doesn't throw exceptions?

vishen commented 5 years ago

I think the following should work as you described, however I get a syntax error; just wondering what I am doing wrong?

test "for else" {
    // For allows an else attached to it, the same as a while loop.
    var items = []anyerror!i32{ 3, 4, error.NoneAvailable, 5 };

    // For loops can also be used as expressions.
    var sum: i32 = 0;
    for (items) |value| {
        sum += value;
    } else |err| {
        warn("error in items\n");
    }
    assert(sum == 7);
}
/home/jonathan/z.zig:12:12: error: invalid token: '|'
    } else |err| {
           ^
vishen commented 5 years ago

Looking at the parser.cpp file, it looks like the for-loop doesn't capture the error payload (yet?): https://github.com/ziglang/zig/blob/master/src/parser.cpp#L423-L438

// ForPrefix Body (KEYWORD_else Body)?
static AstNode *ast_parse_for_expr_helper(ParseContext *pc, AstNode *(*body_parser)(ParseContext*)) {
    AstNode *res = ast_parse_for_prefix(pc);
    if (res == nullptr)
        return nullptr;

    AstNode *body = ast_expect(pc, body_parser);
    AstNode *else_body = nullptr;
    if (eat_token_if(pc, TokenIdKeywordElse) != nullptr)
        else_body = ast_expect(pc, body_parser);

    assert(res->type == NodeTypeForExpr);
    res->data.for_expr.body = body;
    res->data.for_expr.else_node = else_body;
    return res;
}

However, the while-loop does capture the error payload: https://github.com/ziglang/zig/blob/master/src/parser.cpp#L440-L459

// WhilePrefix Body (KEYWORD_else Payload? Body)?
static AstNode *ast_parse_while_expr_helper(ParseContext *pc, AstNode *(*body_parser)(ParseContext*)) {
    AstNode *res = ast_parse_while_prefix(pc);
    if (res == nullptr)
        return nullptr;

    AstNode *body = ast_expect(pc, body_parser);
    Token *err_payload = nullptr;
    AstNode *else_body = nullptr;
    if (eat_token_if(pc, TokenIdKeywordElse) != nullptr) {
        err_payload = ast_parse_payload(pc);
        else_body = ast_expect(pc, body_parser);
    }

    assert(res->type == NodeTypeWhileExpr);
    res->data.while_expr.body = body;
    res->data.while_expr.err_symbol = token_buf(err_payload);
    res->data.while_expr.else_node = else_body;
    return res;
}
andrewrk commented 5 years ago

For loops don't capture error values. But the control flow is the same with else on all 7 of these:

my point is that the "while error union" example is the reasoning for else existing on loops at all. everything else is for consistency.

vishen commented 5 years ago

Ah, sorry, gotcha!

benjaminmordaunt commented 2 years ago

I still find the for/else example in the docs (as of 0.9.1) to be a confusing use case, and not really demonstrate the purpose of the else clause in a for loop.

It seems to be evaluating the else clause as a kind of useless finally block?

redmaner commented 3 months ago

I was going through ziglings.org exercises and it provides another good example for using the else block in a loop expression. See the exercise here: https://codeberg.org/ziglings/exercises/src/branch/main/exercises/062_loop_expressions.zig

It basically mentions you can use a while or for loop as an expression, by using break $value. The else block is executed when the loop doesn't use break.

TL:DR example:

// Given an array of numbers 
const numbers: [4]u8 = .{5, 6, 9, 12};

// Search for a number higher than 10 and assign to `higher_than_ten` variable
var higher_than_ten: ?u8 = for (numbers) | num| {
  if (num > 10) break num;
} else null; // this else block is executed when the for loop runs out of items. The default else block when omitted returns void.