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