How do := and :=? work differently as guard operators in EK9 control flow?

← Control Flow · Ref: Q1025

The key difference is WHAT gets checked and WHEN the expression evaluates.

:= (BLIND ASSIGNMENT)

Always evaluates the expression. Always assigns. No isSet checks.

  if count := getCount() with count > 0
    process(count)

getCount() is called unconditionally. count is assigned unconditionally. The 'with' condition is the only gate.

:=? (ASSIGNMENT IF UNSET)

First checks if the target variable is already SET. Only evaluates the expression if the target is UNSET, and then assigns the result. The body runs only if the target is SET afterwards - so a target that already held a value enters the body with that value, and an unset result skips it.

  if cache :=? expensiveCompute() with cache > threshold
    useCache(cache)

If cache already has a value, expensiveCompute() is NEVER called. This is lazy evaluation.

WHEN TO USE :=

  - Fetching fresh data every time (sensor readings, API polls)
  - The value changes between calls and you want the latest
  - Combined with 'with' condition for validation

WHEN TO USE :=?

  - Caching: avoid re-computing if already have a result
  - Configuration defaults: try sources in priority order
  - First-match: assign from first source that provides a value
  - Loop with lazy init: only initialize on first iteration

See Q1023 for all three operators compared. See Q79 for :=? standalone. See Q1024 for re-point-and-check in control flow.

Example

defines module qa.controlflow.assign.vs.assignifunset

  defines function

    fetchFreshData()
      <- rtn as Integer?
      rtn: 10

    expensiveCompute()
      <- rtn as Integer?
      rtn: 99

  defines program

    AssignVsAssignIfUnsetDemo()
      stdout <- Stdout()
      minCounter <- 5
      threshold <- 50

      // === := BLIND ASSIGNMENT in IF ===
      // Always evaluates, always assigns.
      counter <- Integer()

      if counter := fetchFreshData() with counter > minCounter
        stdout.println(":= fetched and condition met: " + $counter)

      // === :=? ASSIGNMENT IF UNSET in IF ===
      // cache starts unset -> expensiveCompute() IS called
      cache <- Integer()

      if cache :=? expensiveCompute() with cache > threshold
        stdout.println(":=? computed (was unset): " + $cache)

      // cache is now set to 99 -> expensiveCompute() is NOT called again
      if cache :=? expensiveCompute() with cache > threshold
        stdout.println(":=? skipped compute (already set): " + $cache)

      // === := ALWAYS RE-FETCHES (contrast with :=?) ===
      // counter already has value 10, but := overwrites it
      if counter := fetchFreshData() with counter > minCounter
        stdout.println(":= re-fetched (always evaluates): " + $counter)

Common mistakes

E01072 — 'return' does not exist in EK9 — declare the return value with '<-' and assign it with 'rtn:'. See ek9 -h E01072 for details.

Incorrect:

      return 10

Correct:

      rtn: 10
Other ways to ask this
  • When should I use := vs :=? in a while loop or if statement?
  • What is the difference between blind assignment and assignment-if-unset in guards?
  • How does :=? avoid re-evaluation in EK9 control flow?
  • Show me := vs :=? in if and while in EK9

Coming from another language?

Java: ':=' is like simple assignment 'x = expr;'. ':=?' requires 'if (x == null || !x.isSet()) { var temp = expr(); if (temp != null && temp.isSet()) x = temp; }' — 4 lines vs 1. Go: No ':=?' equivalent. Must write 'if x == nil { x = compute() }'. Python: No direct equivalent. 'x = x or compute()' breaks for falsy values.

Keywords: lazy, if, assign, conditional, operator, blind, evaluation, control, default, compare, unset, cache, guard, flow, while