How do guards work in EK9 if statements?

← Control Flow · Ref: Q971

Guards combine variable handling with isSet checking in a single statement. The block only executes if the guarded variable is SET.

THE GUARD FORMS:

1. DECLARATION GUARD (<-):

  if name <- getName()
    stdout.println(name)

Declares name; the block runs only if it is SET.

2. RE-POINT AND CHECK (:= then x?):

  selectedItem <- String()
  if selectedItem := findItem("key") then selectedItem?
    stdout.println(selectedItem)

Always assigns the result to the existing variable (an unset result included); the explicit 'selectedItem?' then runs the block only if it is SET.

3. ASSIGN IF UNSET (:=?):

  config <- loadDefaults()
  if config :=? loadOverrides()
    stdout.println(config)

':=?' tests the LEFT side: it assigns (and only then calls loadOverrides()) when config is currently UNSET. Here config is already set by loadDefaults(), so it is kept, loadOverrides() is not called, and the block runs with the defaults.

PLAIN ASSIGNMENT (:=) CHECKS NOTHING

':=' in a control-flow head is a blind assignment: no isSet check. So 'if x := expr()' on its own has no condition and is rejected (E07340); add one with 'with' or 'then', or use a guard. In 'switch v := expr()' the switch runs whatever the value is, and an unset value goes to default.

SCOPE AND ORDER

A '<-' guard DECLARES a new variable that belongs to the if/else chain: the else branches can read it, and after the chain it is gone (naming it there is E50001). ':=' and ':=?' assign an EXISTING variable, so the value is still there after the chain - and ':=' on a name nothing declared is E50001, because ':=' never declares. Each pre-flow runs once, when its block is REACHED: in 'else if v <- next()' next() is called only after the earlier conditions were false, never when an earlier branch ran.

WITH ADDITIONAL CONDITION:

  if name <- getName() then name.length() > minLength
    stdout.println(name)

Runs only if name is SET AND the condition holds.

The same guard forms work in every control-flow statement:

  if v <- expr()                     declaration guard
  switch v <- expr()                 declaration guard in switch
  while v <- expr() with condition   the guard runs ONCE, before the loop
  for v <- expr() then i in items    the guard runs ONCE, before the loop
  try v <- expr()                    an unset guard skips the whole try

while and for always need their own condition or loop after 'with'/'then': the guard is an entry gate that runs once, and only that condition is re-checked on each iteration.

Bridge: Like Go's 'if err := doSomething(); err != nil'. Like Rust's 'if let Some(v) = expr()'. Like Swift's 'if let v = optionalExpr'.

See Q74 for more guard examples. See Q75 for guard switch. See Q77 for while guards. See Q1023 for the guard operators side by side.

Example

defines module qa.controlflow.guardif

  defines function

    <?-
      Returns a greeting if the name has content, or unset if empty.
    -?>
    findGreeting() as pure
      -> personName as String
      <- rtn as String: String()

      if personName?
        rtn: "Hello, " + personName

  defines program

    GuardIfDemo()
      stdout <- Stdout()

      // BASIC GUARD: only enters if findGreeting returns SET
      if greeting <- findGreeting("Steve")
        stdout.println(greeting)

      // GUARD WITH UNSET: block is skipped
      if greeting <- findGreeting(String())
        stdout.println("This won't print")
      else
        stdout.println("Greeting was unset - skipped")

      // GUARD WITH CONDITION: SET and passes additional test
      minLength <- 3
      if greeting <- findGreeting("Alice") then greeting.length() > minLength
        stdout.println(`Long greeting: ${greeting}`)

      // ASSIGN IF UNSET (:=?): the target is already set, so it is kept
      chosen <- findGreeting("Dana")
      if chosen :=? findGreeting("Eve")
        stdout.println(`Kept: ${chosen}`)

      // GUARD IN SWITCH: declares and checks in one step
      switch greet <- findGreeting("Charlie") with greet
        case == "Hello, Charlie"
          stdout.println("Found Charlie's greeting")
        default
          stdout.println(`Other greeting: ${greet}`)

Common mistakes

E01073 — 'null' does not exist in EK9 — use tri-state semantics (unset/set) with declaration guards such as 'if name <- expr()' instead of comparing against null. See ek9 -h E01073 for details.

Incorrect:

greeting.length() > minLength and greeting <> null

Correct:

greeting.length() > minLength
Other ways to ask this
  • What is a guard in EK9?
  • How does if v <- expr() work in EK9?
  • How do I combine declaration with null checking in EK9?
  • What does the <- guard do inside an if statement?

Coming from another language?

Go: if err := f(); err != nil { } — same concept. Rust: if let Some(v) = expr { } — same concept. Swift: if let v = optional { } — same concept. Java: requires separate null check. EK9 guards work in if, switch, while, for and try; in while and for the guard runs once, before the loop; ':=?' assigns only when the target is unset.

Keywords: null safety, guard, declaration guard, control flow, isSet check