Write me a pure function categorise that takes an Integer score and returns a String category ('high', 'medium' or 'low'), proving the return variable is initialised on every branch.

← Data Flow Safety · Ref: Q1240

EK9 tracks the initialisation state of every variable through all control flow paths. A return variable can be made provably initialised in TWO ways: provide a default value at declaration time, or assign in EVERY branch of every conditional.

PATTERN 1: DEFAULT VALUE AT DECLARATION (RECOMMENDED)

The return variable starts with a sensible default. Branches override it as needed:

  categorise() as pure
    -> score as Integer
    <- rtn as String: "low"
    if score >= 80
      rtn: "high"
    else if score >= 50
      rtn: "medium"

The compiler accepts this because 'rtn' is initialised before any branch runs. The if/else only modifies an already-set value.

PATTERN 2: EXPLICIT ASSIGNMENT ON EVERY BRANCH

Declare the return variable without a default but assign in every branch:

  categorise() as pure
    -> score as Integer
    <- rtn as String?
    if score >= 80
      rtn: "high"
    else if score >= 50
      rtn: "medium"
    else
      rtn: "low"

The final 'else' is required — without it, a score below 50 would leave 'rtn' uninitialised and the compiler raises E08020.

WHY DEFAULT IS PREFERRED

Pattern 1 is more robust: adding a new branch (e.g. 'if score >= 95 then "excellent"') does not risk leaving rtn uninitialised because the default still applies to any path that does not override it. Pattern 2 requires updating the else clause whenever cases change.

USAGE

  stdout.println(categorise(95))   // "high"
  stdout.println(categorise(70))   // "medium"
  stdout.println(categorise(30))   // "low"

See Q633 for branch initialisation rules. See Q632 for definition order. See Q634 for guard-based safe access.

Example

defines module qa.dataflow.categorise

  defines constant

    HIGH_THRESHOLD <- 80

    MEDIUM_THRESHOLD <- 50

  defines function

    categorise() as pure
      -> score as Integer
      <- rtn as String: "low"
      if score >= HIGH_THRESHOLD
        rtn: "high"
      else if score >= MEDIUM_THRESHOLD
        rtn: "medium"

  defines program

    CategoriseDemo()
      stdout <- Stdout()

      stdout.println(`Score 95: ${categorise(95)}`)
      stdout.println(`Score 70: ${categorise(70)}`)
      stdout.println(`Score 30: ${categorise(30)}`)

Common mistakes

E08020 — Without a default value AND without a final else, rtn is uninitialised when score < 50. Either provide a default at declaration or add an else branch that assigns rtn. See ek9 -h E08020 for details.

Incorrect:

<- rtn as String?
      if score >= 80
        rtn: "high"
      else if score >= 50
        rtn: "medium"

Correct:

      <- rtn as String: "low"
      if score >= HIGH_THRESHOLD
        rtn: "high"
      else if score >= MEDIUM_THRESHOLD
        rtn: "medium"
Other ways to ask this
  • Create a function whose return value is provably set on every if/else branch.
  • Show me how to satisfy EK9 data flow analysis with branch initialisation.
  • Implement a categoriser that the compiler can prove always returns a value.
  • Write a function with a default-initialised return variable for safety.

Coming from another language?

Java: definite assignment analysis catches some of these but allows null. Rust: must assign on every path or use Option<T>. Go: zero-value initialisation always works but may hide bugs. Python: NameError at runtime. EK9: compile-time guarantee — either declare with default or assign on every branch, no third option.

Keywords: initialisation, data flow, uninitialised, branch, default value, E08020, if else, return variable