How do guard variables work in while loops?

← Control Flow · Ref: Q77

In EK9, a while loop can start with a guard: a declaration (or assignment) followed by 'with' (or 'then') and the loop condition. The guard runs ONCE, before the loop, exactly like a for loop guard. It is an entry gate: if it leaves its variable unset, the loop does not run at all. Once the loop is running, only the condition after 'with' is re-checked on each iteration; the guard is not called again.

BASIC WHILE GUARD

The guard declares the variable once; the condition controls the iterations:

  while value <- supplyCounter() with counter > 0
    stdout.println(`Counter: ${counter} value: ${value}`)
    counter: counter - 1

supplyCounter() is called once. The loop then runs while 'counter > 0' holds.

UNSET GUARD SKIPS THE LOOP

If the guard's value is unset, the condition is never checked and the body never runs. No exception is thrown. This is a safe alternative to checking for null before a while loop.

CONSUMING AN ITERATOR

Because the guard runs once, it is the place to get an iterator; the condition re-checks it each time:

  while iter <- names.iterator() then iter.hasNext()
    stdout.println(iter.next())

LOOPING UNTIL A VALUE BECOMES UNSET

The guard is not re-evaluated, so a guard such as 'while item <- source.next() with item?' would test the same first item forever. To poll until a source runs dry, fetch before the loop and again at the end of the body:

  item <- source.next()
  while item?
    process(item)
    item: source.next()

WHILE EXPRESSION WITH GUARD

While loops can be expressions that return a value; the guard sets up the loop variable once:

  total <- while counter <- 0 with counter < limit
    <- rtn <- 0
    rtn: rtn + counter
    counter: counter + 1

If the guard is unset, the expression evaluates to the initial return value.

THREE GUARD OPERATORS IN WHILE

All of them run once, before the loop:

  1. Declaration (<-): creates the variable; the loop runs only if it is set
     while value <- getNext() with condition
  2. Assignment (:=): assigns an existing variable, NO isSet check; the condition alone decides
     while existing := getNext() with condition
  3. Assign-if-unset (:=?): fills an existing variable only while it is unset, and the loop runs only if it is set
     while existing :=? getNext() with condition

To re-point an existing variable and loop only while the new value is set, check it in the condition: 'while existing := getNext() then existing? and (condition)'.

See Q66 for basic while loops. See Q67 for do-while loops. See Q74 for guards in if. See Q76 for guards in for loops. See Q81 for while expressions.

Example

defines module qa.flow.guard.whileloop

  defines function

    supplyCounter()
      <- rtn <- Integer()
      rtn: 5

    supplyNothing()
      <- rtn as Integer: Integer()

    supplyStart() as pure
      -> initial as Integer
      <- rtn as Integer: initial

  defines program

    GuardWhileDemo()
      stdout <- Stdout()

      // === BASIC WHILE GUARD: evaluated ONCE, before the loop ===

      counter <- supplyStart(3)
      while guardedCount <- supplyCounter() with counter > 0
        stdout.println(`Counter: ${counter} value: ${guardedCount}`)
        counter: counter - 1

      // === UNSET GUARD: the loop does not run at all ===

      attempts <- supplyStart(2)
      while missing <- supplyNothing() with attempts > 0
        stdout.println(`Never printed: ${missing}`)
        attempts: attempts - 1
      stdout.println(`Unset guard skipped the loop, attempts still ${attempts}`)

      // === CONSUMING AN ITERATOR: get it once, re-check hasNext() each time ===

      names <- ["Ada", "Grace", "Alan"]
      while iter <- names.iterator() then iter.hasNext()
        stdout.println(`Name: ${iter.next()}`)

      // === ASSIGNMENT (no isSet check) ===

      loops <- supplyStart(2)
      existing <- Integer()
      while existing := supplyCounter() with loops > 0
        stdout.println("Assigned value: " + $existing)
        loops: loops - 1

      stdout.println("While guard demo complete")

Common mistakes

E07390 — 'while true' is a constant Boolean condition, which EK9 rejects as pointless (and the break such a loop needs does not exist in EK9, E01070). A guard plus a 'with' condition replaces it: the guard runs once, before the loop, and the condition decides each iteration. See ek9 -h E07390 for details.

Incorrect:

      while true
        guardedCount <- supplyCounter()
        if not guardedCount?
          break
        if counter <= 0
          break
        stdout.println(`Counter: ${$counter} value: ${$guardedCount}`)
        counter: counter - 1

Correct:

      while guardedCount <- supplyCounter() with counter > 0
        stdout.println(`Counter: ${counter} value: ${guardedCount}`)
        counter: counter - 1
Other ways to ask this
  • Can I guard a while loop with a variable declaration?
  • How does the while loop guard work in EK9?
  • Is a while loop guard evaluated on every iteration?

Coming from another language?

Java: No guard in while. Must write: 'T v; while ((v = getNext()) != null && v > 0) { ... }' mixing assignment and null check in condition. Error-prone. Python: No guard in while. Walrus operator helps: 'while (v := get_next()) is not None and v > 0:' but only handles None. Rust: 'while let Some(v) = get_next()' for iterator consumption. Close to EK9 but limited to pattern matching. Go: No guard in while (Go only has for). Must use: 'for v := getNext(); v != nil; v = getNext() { ... }'. EK9: 'while value <- getNext() with value > 0' combines declaration, isSet check, and loop condition. The guard runs once, before the loop (an unset guard skips the loop); only the condition after 'with' is re-checked each iteration.

Keywords: iteration, poll, once, branch, terminate, loop, guard, safe, flow, while, null-safe, isset, declaration, condition, consume, control