How do I convert functions with multiple return statements to EK9?

← Control Flow Without break/continue/return · Ref: Q289

This shows four common multi-return patterns and their EK9 equivalents side by side.

SCENARIO 1: GUARD CLAUSE RETURNS

Java: if (name == null || name.isEmpty()) return "invalid"; if (name.length() > 50) return "too long"; return "ok";
EK9 equivalent uses if/else with declared return:

  validateName() as pure
    -> name as String
    <- status as String: "ok"
    if length name == 0
      status: "invalid"
    else if length name > 50
      status: "too long"

The declared return starts with a default. Conditions modify it. Every path is visible.

SCENARIO 2: LOOKUP WITH DEFAULT

Java: for (String s : items) { if (s.equals(target)) return s; } return "not found";
EK9 equivalent uses unset return + for loop + :=? default:

  lookup()
    -> items as List of String, target as String
    <- found as String: String()
    for item in items
      if item == target and ~found?
        found :=? item

The caller checks if found? and uses a default if not. No loop break, no return inside loop.

SCENARIO 3: MULTI-BRANCH CLASSIFICATION

Java: if (score >= 90) return "A"; if (score >= 80) return "B"; if (score >= 70) return "C"; return "F";
EK9 equivalent uses declared return with conditional assignment:

  grade() as pure
    -> score as Integer
    <- letter as String: "F"
    if score >= 90
      letter: "A"
    else if score >= 80
      letter: "B"
    else if score >= 70
      letter: "C"

SCENARIO 4: TERNARY CHAIN

Java: return a > 0 ? "positive" : a < 0 ? "negative" : "zero";
EK9 equivalent uses :=? chain:

  describe() as pure
    -> num as Integer
    <- label as String?
    if num > 0
      label :=? "positive"
    else if num < 0
      label :=? "negative"
    else
      label :=? "zero"

Each branch assigns exactly once via :=?. The compiler verifies all paths set the value.

THE PATTERN

Every multi-return function becomes: declare the return variable with a sensible default, then use if/else to modify it. The compiler verifies every path initialises the return.

See Q50 for declared returns. See Q146 for decomposition. See Q274 for AI return patterns. See Q148 for migration.

Example

defines module qa.without.migratereturn

  defines function

    // Scenario 1: Guard clause returns
    validateName() as pure
      -> name as String
      <- status as String: "ok"
      maxNameLength <- 50
      if length name == 0
        status: "invalid"
      else if length name > maxNameLength
        status: "too long"

    // Scenario 2: Lookup with default
    lookup()
      ->
        items as List of String
        target as String
      <- found as String: String()
      for item in items
        if item == target and ~found?
          found :=? item

    // Scenario 3: Multi-branch classification
    grade() as pure
      -> score as Integer
      <- letter as String: "F"
      excellentMinScore <- 90
      goodMinScore <- 80
      averageMinScore <- 70
      if score >= excellentMinScore
        letter: "A"
      else if score >= goodMinScore
        letter: "B"
      else if score >= averageMinScore
        letter: "C"

    // Scenario 4: Ternary chain
    describeSign() as pure
      -> num as Integer
      <- label as String?
      if num > 0
        label :=? "positive"
      else if num < 0
        label :=? "negative"
      else
        label :=? "zero"

  defines program

    MigrateReturnDemo()
      stdout <- Stdout()

      // === SCENARIO 1: GUARD CLAUSE RETURNS ===

      stdout.println(`Empty name: ${validateName("")}`)
      stdout.println(`Long name: ${validateName("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz")}`)
      stdout.println(`Valid name: ${validateName("Alice")}`)

      // === SCENARIO 2: LOOKUP WITH DEFAULT ===

      fruits <- ["apple", "banana", "cherry"]
      if result <- lookup(fruits, "banana")
        stdout.println(`Found: ${result}`)

      missing <- lookup(fruits, "mango")
      if ~missing?
        stdout.println("Mango not found, using default")

      // === SCENARIO 3: MULTI-BRANCH CLASSIFICATION ===

      scores <- [95, 85, 75, 55]
      for score in scores
        stdout.println(`Score ${score}: grade ${grade(score)}`)

      // === SCENARIO 4: TERNARY CHAIN ===

      numbers <- [42, -7, 0]
      for num in numbers
        stdout.println(`${num} is ${describeSign(num)}`)

Common mistakes

E50001 — A pure function cannot call non-pure methods like stdout.println(). Pure functions must be free of side effects. See ek9 -h E50001 for details.

Incorrect:

grade() as pure
      -> score as Integer
      <- letter as String: "F"
      stdout.println(score)

Correct:

grade() as pure
      -> score as Integer
      <- letter as String: "F"
Other ways to ask this
  • What are before and after examples for converting multiple returns to EK9?
  • How do I migrate early return guard clauses to EK9?
  • How do I replace ternary chains and multi-return functions in EK9?

Coming from another language?

Java: return in every branch, ternary operator for simple cases, guard clauses at top. Python: return in each branch, or pattern for defaults. Rust: implicit return from last expression in each branch, or early return with ?. Go: return in each branch, error return pattern. Kotlin: return or when expression. Swift: return or switch expression. EK9: declared return variable, if/else modifies it, :=? for pure functions, compiler verifies all paths.

Keywords: function, declared, multiple, migrate, named, classification, ternary, return, before, after, null-safe, isset, no-return, safe, no-break, refactor, alternative, convert, guard