When is an isSet check on a collection genuinely needed?

← Comparison Patterns · Ref: Q640

Collections (List, Dict) are always set after construction, even when empty. Checking a newly constructed collection with ? is always redundant (E08088). A List returned from a function is set too: every route to a List goes through a constructor, so a function cannot hand back an unset List. The compiler does not flag a guard on a function's result (it does not look inside the function), but that guard always passes. An isSet check is genuinely needed only when the value can really be missing, and the way to say 'there may be no list at all' is to wrap it, for example in an Optional.

WHEN ISSET IS REDUNDANT (E08088)

After constructing a collection directly:

  items <- List() of String
  if items?    <- this is ALWAYS true (redundant)

The compiler knows the constructor always returns a set value.

NO RESULTS: TEST WITH EMPTY

A search that finds nothing returns an empty List, which is SET. Test it with 'empty':

  noResults <- searchByKeyword("unknown")
  if noResults empty
    stdout.println("No results")

A guard such as 'if noResults <- searchByKeyword("unknown")' would ENTER its body here, because the empty List is set.

WHEN ISSET IS NEEDED

When there may be no collection at all, return an Optional of the collection and guard on that:

  if scores <- lookupScores(studentId)
    stdout.println(`Found ${length scores.get()} scores`)

An Optional with nothing in it is unset, so the guard skips the body.

EMPTY VS UNSET

An empty list (0 items) is SET. It exists, it is valid, it just has no elements. An unset value has no meaningful value yet. These are different states. Use 'empty' to check for no elements; use '?' (or a guard) on something that can really be unset, such as an Optional.

CORRECT PATTERN FOR EMPTY CHECK

Use the empty operator to check for no elements:

  if items empty
    stdout.println("No items")

Do NOT use '?' to check if a list has elements.

See Q559 for redundant isSet detection. See Q558 for tautological conditions. See Q641 for return value capture.

Example

defines module qa.comparison.collectionisset

  defines function

    <?-
      Simulates a search. It always returns a SET List: empty when nothing matches.
    -?>
    searchByKeyword()
      -> keyword as String
      <- results as List of String: List() of String

      if keyword == "ek9"
        results += "Introduction to EK9"
        results += "EK9 Best Practices"

    <?-
      Simulates a lookup. Like every List, the result is set; it is empty for an unknown student.
    -?>
    findScores()
      -> studentId as Integer
      <- scores as List of Integer: List() of Integer

      if studentId == 1
        scores += 85
        scores += 92
        scores += 78

    <?-
      A lookup where there may be NO list at all: the Optional is unset for an unknown student.
    -?>
    lookupScores()
      -> studentId as Integer
      <- rtn as Optional of List of Integer: Optional() of List of Integer

      found <- findScores(studentId)
      if found not empty
        rtn: Optional(found)

    <?-
      Pure function that processes a list.
    -?>
    countAboveThreshold() as pure
      ->
        items as List of Integer
        threshold as Integer
      <- counted as Integer: 0

      for item in items
        if item > threshold
          counted: counted + 1

  defines program

    CollectionIsSetDemo()
      stdout <- Stdout()

      //A constructed collection is set, with or without items: test it with 'empty', never '?'
      fresh <- List() of String
      for title in searchByKeyword("rust")
        fresh += title
      if fresh empty
        stdout.println("Nothing collected, but the List is still set")

      //A List returned from a function is set too, so this guard always enters
      if results <- searchByKeyword("ek9")
        stdout.println(`Found ${length results} results`)
        for item in results
          stdout.println(`  ${item}`)

      //No results: the List is SET but empty, so test it with 'empty', not '?'
      noResults <- searchByKeyword("unknown")
      if noResults empty
        stdout.println("No results for 'unknown'")

      //Genuinely needed: an Optional that may hold no List at all
      if scores <- lookupScores(1)
        above80 <- countAboveThreshold(scores.get(), 80)
        stdout.println(`Scores above 80: ${above80}`)

      missing <- lookupScores(2)
      stdout.println(`Scores held for student 2: ${missing?}`)

      //Empty check on a returned List
      for studentId in [1, 2]
        knownScores <- findScores(studentId)
        if knownScores empty
          stdout.println(`Empty scores for student ${studentId}`)
        else
          stdout.println(`Has ${length knownScores} scores`)

Common mistakes

E08088 — A collection made by its constructor is always set, so checking it with ? is always true and EK9 rejects the check. Test for no elements with 'empty'. See ek9 -h E08088 for details.

Incorrect:

fresh <- List() of String
      if fresh?

Correct:

fresh <- List() of String
      for title in searchByKeyword("rust")
        fresh += title
      if fresh empty
Other ways to ask this
  • Is checking a list with ? redundant after construction?
  • What is E08088 redundant isSet check on collection?
  • When should I check if a collection is set?

Coming from another language?

Java: no tracking of collection initialization state. Kotlin: nullable collection vs empty collection distinguished by type. Rust: Option<Vec<T>> vs empty Vec<T>. Go: nil slice vs empty slice. Python: None vs empty list. EK9: tri-state (absent, unset, set), collections are always set after construction (a List returned from a function included), E08088 for redundant isSet check, 'empty' for no elements, Optional of List when there may be no list at all.

Keywords: collection, redundant, check, comparison, dict, constructed, E08088, pattern, compare, E08089, empty, isSet, list