[Verse] [Feature Request] Adding a `for-else` syntax support for flow control

:bullseye: Feature Request: for-else in Verse

Currently, handling failed iterations or filtering in Verse often requires verbose if/else logic. This proposal introduces a cleaner and more idiomatic for-else control structure.

:light_bulb: Proposal

Introduce support for an optional else block in verse for loop syntax. This else block would be executed:

  • Never: if the loop has no failable expressions, or the expressions never fails;
  • Once: if the loop fails to “initialize” (e.g., an initial failure occurs when trying to retrieve collection of items to iterate);
  • During iteration: for elements that fail a conditional filter (based on already existing for loop failure context), if specified.

:puzzle_piece: Why This Is Useful

Verse already has built-in success/failure semantics, which makes the for-else concept even more natural and powerful.

  • Note: Other languages (like Python) already have for-else syntax, but they behaves differently than this proposal. This proposal has a specific behavior and powerful usage that is related to how verse can handle failure checks.

:wrench: Key Use Cases

  1. Fallback for failed iterations
    Cleanly detect when there are no valid elements to iterate over, without needing to enclose the for loop inside if statements to handle these cases;

  2. Per-element failure handling
    Apply fallback logic when specific elements fail a condition (IsActive[], etc.) without manual if/else per element inside the loop.

  3. Reduces boilerplate and improves clarity
    Avoids deeply nested conditionals and keeps failure logic unified, while still being readable and coherent.


:test_tube: Current Methods/Workarounds

I will show some examples of current ways to handle some scenarios. Later I will provide the suggested for-else alternative to account instead of using these methods:

Example 1: Normal Loop (basic example of always-succeeding iteration)

for (X := 0..100) {
    # Do Stuff
}

Example 2: Filtered Loop (basic example of filtering elements during iteration)

MyValues := for (Player : AvailablePlayers, Player.IsActive[]) {
    Player.GetScore()
}

Example 3: Handling failed iteration initialization with if

MyValues := if (ValidPlayers := GetValidPlayers[]) {
    for (Player : ValidPlayers) {
        # Do Stuff
    }
} else {
    Print("No valid player found")
    array{}
}

Example 4: Filtering inside the loop manually

for (Player : AvailablePlayers) {
    if (Player.IsActive[]) {
        # Do Stuff
    } else {
        Print("Player is not valid")
    }
}

Example 5: Handling failed element condition check with a fallback

MyValues := for (Player : AvailablePlayers) {
    if (Player.IsActive[]) {
        Player.GetScore()
    } else {
        0 # Assign Default
    }
}

Example 6: Handling failed element condition check with a fallback and/or with filtering behavior

MyValues := for (Player : AvailablePlayers) {
    if (Player.IsActive[]) {
        Player.GetScore()
    } else {
        StuffResult := DoStuff()
        if (StuffResult?) {
            0 # Assign Default if condition pass
        } else {
            continue # Skip current item to not be on the MyValues list if condition fails
        }
    }
}

:white_check_mark: Proposed for-else Syntax

Now, comparing to the 6 examples above, here is how it would be when using the proposed for-else syntax:

Example 1 (Compared to Normal and Basic for loop usage)

for (X := 0..100) {
    # Do Stuff
} else {
    Print("Unreachable") # This will never fire since iteration will never fail
}

Example 2 (Compared to Filtering behavior of for loop failable contexts)

MyValues := for (Player : AvailablePlayers, Player.IsActive[]) {
    Player.GetScore()
} # Filter from result by not having the else block

Example 3: Using for-else on iteration that fails initialization is unreachable, returning an empty array

MyValues := for (Player : GetValidPlayers[]) {
    # Do Stuff
} else {
    Print("Unreachable")
    # This will never fire due to failing
    # the iteration condition itself
}

(Note that the proposed syntax is not needed or used on these first three examples above, but I still let them here just to keep the proposal description clear and consistent)

Example 4: Usage of for-else to handle per-element fallbacks

for (Player : AvailablePlayers, Player.IsActive[]) {
    # Do Stuff
} else {
    Print("Player is not valid")
    # This will only be fired on inactive players (fail IsActive[])
    # Can be fired multiple times during the iteration (one for each "Player")
    # Current "Player" value from iteration is acessible since it's valid
}

Example 5: Usage of for-else to handle per-element fallbacks and assign a default

MyValues := for (Player : AvailablePlayers, Player.IsActive[]) {
    Player.GetScore()
} else {
    0 # Assign default
}

Example 6: Usage of for-else to handle per-element fallbacks, assigning defaults or more complex filtering logic

MyValues := for (Player : AvailablePlayers, Player.IsActive[]) {
    Player.GetScore()
} else {
    if (SomeCondition?). 0 # Assign Default if condition pass
    else. continue # Skip current item to not be on the MyValues list if condition fails
}

:magnifying_glass_tilted_left: Common Objections & Counterpoints

:red_question_mark: “We can already do this using if inside or outside the loop like on the first examples.”

→ True — But for-else offers a more concise, readable, and consistent way to handle these cases, especially when dealing with multiple dynamic failure contexts and more complex scenarios.

:red_question_mark: “Isn’t using else confusing since it’s usually tied to if?”

→ While valid, other languages (Python, etc.) already support for-else even with weird or incoherent behaviors. But, in verse, this makes more sense and is super coherent with this proposed behavior due to the presence of failure contexts on the language.

:red_question_mark: “What if this makes failure behavior less explicit or harder to debug?”

→ On the contrary — this makes failure more explicit and centralized. Rather than scattering fallback checks across multiple conditionals, for-else keeps the fallback logic right next to the loop context, making it easier to see what happens when a failure occurs, and easier to test or log accordingly.
All that without needing extra or complex nesting logic both inside or outside the loop such as if blocks.


:pushpin: Conclusion

Adding for-else to Verse:

  • Enhances clarity when working with failure-based iteration contexts
  • Reduces boilerplate in fallback, filtering and nested conditional logic scenarios
  • Aligns well with Verse’s failable context system and usage

This proposal does not replace other techniques — it complements them with a more elegant, readable, and easier to use alternative.

For other users and developers reading this feature request, feel free to share any ideas, questions, or additional suggestions related to the topic :slight_smile: