What is the difference between :=, :=? and the explicit re-point-and-check in EK9 control flow?

← Control Flow · Ref: Q1023

EK9 has two assignment pre-flows for EXISTING variables, and one explicit pattern for re-pointing a variable and entering only when the new value is set.

:= (BLIND ASSIGNMENT)

Always evaluates the expression. Always assigns, an unset result included. No checks: the body always runs.

  switch mainValue := getValue()
    case 1 ...

Use when: the value is known safe, or a 'with'/'then' condition validates it.

:=? (ASSIGN-IF-UNSET - checks the LEFT side)
First checks whether the target is already set. Only evaluates and assigns the expression if the target is UNSET; a target that is already set keeps its value. The body then runs only if the target is SET.

  switch mainValue :=? getValue()
    case 1 ...

Use when: the variable may already have a value (caching, defaults, priority chains). Avoids re-evaluation.

RE-POINT, THEN CHECK (assign, then run only if the new value is set)
Assign with ':=' (an unset result OVERWRITES the old value), then check it. In an if or while the check fits in the head: 'if mainValue := getValue() then mainValue?'. A switch, for, try or do head has no condition to carry it, so write the three steps:

  mainValue := getValue()
  if mainValue?
    switch mainValue
      case 1 ...

DECISION TREE:

  First time using this variable? -> Use <- (declaration guard)
  Variable exists, keep it if already set? -> Use :=? (lazy, avoids re-evaluation)
  Variable exists, take the new value and enter only if it is set? -> ':=' then check it ('then x?', or 'if x?' around the construct)
  Variable exists, value known safe? -> Use := (blind, no checks)

See Q74 for the declaration guard (<-). See Q79 for :=? standalone usage. See Q759 for assigning and then checking. See Q1024 for re-point-and-check across control flow. See Q1025 for := vs :=? across control flow.

Example

defines module qa.controlflow.guard.operator.contrast

  defines function

    getZero()
      <- rtn as Integer?
      rtn: 0

    getUnsetValue()
      <- rtn as Integer?
      rtn: Integer()

    getOne()
      <- rtn as Integer?
      rtn: 1

  defines program

    GuardOperatorContrastDemo()
      stdout <- Stdout()

      // === := BLIND ASSIGNMENT ===
      // Always assigns. getZero() returns 0, matches no case -> default.
      blindResult <- Integer()

      switch blindResult := getZero()
        case 1
          stdout.println(":= matched case 1")
        default
          stdout.println(`:= hit default (value: ${blindResult})`)

      // === RE-POINT, THEN CHECK (assign, then switch only if the new value is set) ===
      // guardedResult starts SET (1). getUnsetValue() returns an unset Integer.
      // ':=' still ASSIGNS it, so guardedResult becomes unset and the 'if' skips the switch.
      guardedResult <- getOne()

      guardedResult := getUnsetValue()
      if guardedResult?
        switch guardedResult
          case 1
            stdout.println("re-pointed matched case 1")
          default
            stdout.println("re-pointed hit default")
        stdout.println(`re-point kept a value (value: ${guardedResult})`)
      else
        stdout.println("re-point assigned the unset result, so the switch was skipped")

      // === :=? ASSIGNMENT IF UNSET (checks LEFT side) ===
      // lazyResult starts unset, so :=? WILL evaluate getOne().
      // getOne() returns 1, which is set, so assignment happens and matches case 1.
      lazyResult <- Integer()

      switch lazyResult :=? getOne()
        case 1
          stdout.println(`:=? assigned (value: ${lazyResult})`)
        default
          stdout.println(":=? hit default")

      // lazyResult is now SET, so :=? does not call getZero() and keeps the 1.
      switch lazyResult :=? getZero()
        case 1
          stdout.println(`:=? kept the existing value (value: ${lazyResult})`)
        default
          stdout.println(`:=? replaced it (value: ${lazyResult})`)

Common mistakes

E01073 — EK9 has no null; use the tri-state '?' isSet operator (or a guard) instead of comparing to null. See ek9 -h E01073 for details.

Incorrect:

if guardedResult <> null

Correct:

if guardedResult?
Other ways to ask this
  • When should I use := vs :=? in a switch statement?
  • How do the assignment pre-flows differ in EK9?
  • What guard operator should I use in EK9 control flow?
  • How do I re-point a variable and skip a switch when the new value is unset?

Coming from another language?

Java: No equivalent. Must manually write if/else chains for each pattern. Optional.orElse() handles one level. Kotlin: Elvis (?:) handles null-only cases, not tri-state isSet. Rust: if let handles pattern matching, no assignment-if-unset. Go: if err := f(); err != nil — closest to := but no :=? equivalent. EK9: blind assignment (:=), lazy assign-if-unset (:=?) and the declaration guard (<-), plus the explicit 'x := f() then x?' to re-point and check — one syntax across all control flow.

Keywords: lazy, decision, operator, contrast, blind, switch, control, comparison, unset, guard, guarded, assignment, difference, choose, flow