What do <-, :=? and := each do in the head of an if, switch, for, while or try?
← Control Flow · Ref: Q1980
The part of a control-flow head before 'then'/'with' is the pre-flow. It runs exactly ONCE, before the body. Two operators there GUARD (the body is skipped when the variable ends up unset); ':=' does not, so it takes a condition after 'then' - and the explicit 'then x?' makes it re-point and guard.
<- DECLARE AND GUARD
if fresh <- fetchReading()
Declares a NEW variable scoped to the construct and enters only if it is set.
:= THEN x? - RE-POINT, THEN CHECK
if latest := fetchNothing() then latest?
Assigns an EXISTING variable every time - an unset result included, so a value it held before is REPLACED - and the explicit check enters only if it is set. Here latest was 42 and is unset afterwards: the else branch runs.
:=? FILL IF UNSET, THEN GUARD
if cache :=? expensiveReading()
Looks at the TARGET first. If cache is unset, expensiveReading() is called and assigned; if cache is already set it is kept and expensiveReading() is NOT called. Either way the body runs when cache is set afterwards - so the second ':=?' enters the body with the value it already had.
:= WITH ANOTHER CONDITION
if count := fetchNothing() then count < threshold
Assigns with no set-check at all, so the head needs its own condition after 'then' (without one it is E07340). An unset count makes 'count < threshold' unset, which does not enter the body. To check the value AND a condition, put the check first and parenthesise the rest: 'then count? and (count < threshold)'.
CHOOSING
- a new variable, used only when set: <-
- an existing variable, take the new value and enter only if set: := then x?
- an existing variable, keep a value it already has: :=?
- a setup step with your own condition: :=
The same operators work in switch, for, while, do/while and try heads (and their expression forms); a switch, for, try or do head has no condition, so there re-point first and wrap the construct in 'if x?'. The variable a ':=?' or ':=' pre-flow assigns keeps its value after the construct. See Q1981 for assigning and then checking outside a head, Q1982 for the misuses the compiler rejects, Q1983 for guards in loops, Q1023 and Q1024 for switch.
Example
defines module qa.controlflow.guardfamily.head defines function fetchReading() <- rtn as Integer: 42 fetchNothing() <- rtn as Integer: Integer() expensiveReading() <- rtn as Integer: 99 Stdout().println(" expensiveReading() called") defines program GuardFamilyInAHead() stdout <- Stdout() threshold <- fetchReading() //<- declares a NEW variable and enters only if it is set if fresh <- fetchReading() stdout.println(`<- body: fresh=${fresh}`) //':=' ALWAYS assigns an existing variable; 'then latest?' enters only if it is now set latest <- fetchReading() stdout.println(`re-point before: latest? ${latest?}`) if latest := fetchNothing() then latest? stdout.println("re-point body ran") else stdout.println("re-point else: the unset result was assigned") stdout.println(`re-point after: latest? ${latest?}`) //:=? assigns only if the variable is unset - and only then calls the right side cache <- fetchNothing() stdout.println(`:=? before: cache? ${cache?}`) if cache :=? expensiveReading() stdout.println(`:=? body: cache=${cache}`) if cache :=? expensiveReading() stdout.println(`:=? body again: cache=${cache} - kept, not recomputed`) //:= is a blind assignment: no set-check, so it needs its own condition after 'then' count <- fetchReading() if count := fetchNothing() then count < threshold stdout.println(":= body ran") else stdout.println(":= else: an unset count makes the condition unset, not true") stdout.println(`:= after: count? ${count?}`)
Common mistakes
E07340 — ':=' in a head is a blind assignment, not a guard, so an 'if' with only ':=' has no condition at all. Add 'then <condition>' - 'then count?' to gate on the value being set. See ek9 -h E07340 for details.
Incorrect:
if count := fetchNothing()
Correct:
if count := fetchNothing() then count < threshold
E50001 — ':=' assigns an EXISTING variable; it never declares one, so 'fresh' does not resolve. Use '<-' to declare a new variable in the head. See ek9 -h E50001 for details.
Incorrect:
if fresh := fetchReading() then fresh?
Correct:
if fresh <- fetchReading()
E50050 — '<-' always DECLARES, and 'latest' already exists, so the head would declare it a second time - EK9 does not shadow. To assign an existing variable and guard on it, use 'if latest := fetchNothing() then latest?'. See ek9 -h E50050 for details.
Incorrect:
if latest <- fetchNothing()
Correct:
if latest := fetchNothing() then latest?
Other ways to ask this
- Show the guard operators side by side in an if statement
- Does ':=' in a head overwrite a variable that is already set when the new value is unset?
- When does :=? call the right-hand side in a control-flow head?
- Why does 'if x := f()' need a 'then' condition?
- Which pre-flow keeps the old value and which one replaces it?
Coming from another language?
Java: 'x = f(); if (x != null) {...}' is EK9's 'if x := f() then x?', and :=? has no one-line form ('if (cache == null) cache = compute(); if (cache != null) {...}'). Kotlin: 'f()?.let { x = it; ... }' assigns only a non-null value - EK9's ':=' assigns the unset value too, so the variable always reflects the latest call. Go: 'if x := f(); x != nil {}' declares like <- but needs the explicit check. EK9: one head shape in every control-flow construct.
Keywords: lazy, :=, while, if, :=?, assign, pre-flow, switch, head, <-, unset, overwrite, guard, try, for