Can I put a guard on a stream 'for' pipeline?

← Control Flow · Ref: Q1381

Yes for the statement form, no for the expression form.

A stream's 'for' source is a for-range, so it takes exactly the same pre-flow as the statement loop. As a STATEMENT the guard gates the whole pipeline - if the guarded value is unset, nothing is piped at all:

  for limit <- getLimit() then i in 1 ... 3 > stdout
  //unset limit: nothing printed. Set limit: 1 2 3.

AS AN EXPRESSION IT IS AN ERROR (E08056)

  collected <- for limit <- getLimit() then i in 1 ... 3 | collect as List of Integer
  //E08056

Every other guarded expression form declares its own result, and that declaration's initial value IS the result when the guard skips - 'the initialiser is the guard's else':

  result <- switch target ?= getValue() with target
    <- rtn as String: "init"        //this is the answer if the guard skips
    case 1
      rtn: "one"
    default
      rtn: "other"

A stream expression ends in '| collect as T'. It has no returning param, so there is nowhere to write that initial value, and the result would be whatever T's default constructor produces. That differs by type in a way nothing at the call site shows: 'collect as List of Integer' would give an empty but SET list, while 'collect as Integer' would give an UNSET Integer. Rather than let that depend on the collect type, the compiler rejects the shape.

TWO WAYS TO WRITE WHAT YOU MEANT

1. Statement form with your own accumulator, so you choose the value that survives a skipped guard:

     total <- 0
     for limit <- getLimit() then i in 1 ... 3
       total += i

2. Hoist the guard into an enclosing 'if', leaving the stream unguarded:

     if limit <- getLimit()
       collected <- for i in 1 ... 3 | collect as List of Integer

':=' IS NOT A GUARD

Only the set-checking pre-flows gate the body: '<-', '?=' and ':=?'. A plain ':=' always assigns and the pipeline always runs, so it is accepted in both forms - and it is not doing what a reader might assume:

  for target := getLimit() with i in 1 ... 2 > stdout   //always runs, even if getLimit() is unset

A LOOP VARIABLE IS SCOPED TO THE ENCLOSING BLOCK

Two stream 'for's in the same block cannot both call their loop variable 'i' - that is E50050, duplicate variable. Give each one its own name.

See Q837 for guards in expression forms generally. See Q1357 for the for-in expression form. See Q89 for stream pipelines. See Q74 for guards in if statements.

Example

defines module qa.controlflow.streamguard

  defines function

    getLimit() as pure
      <- rtn as Integer: Integer()

  defines program

    StreamGuardDemo()
      stdout <- Stdout()

      // === STATEMENT FORM WITH A GUARD - LEGAL ===
      // The guard gates the whole pipeline. getLimit() is unset here, so nothing is piped.

      for skippedLimit <- getLimit() then skippedItem in 1 ... 3 > stdout
      stdout.println("unset guard skipped the pipeline")

      // === THE EXPRESSION FORM WOULD BE E08056 ===
      // collected <- for limit <- getLimit() then i in 1 ... 3 | collect as List of Integer
      // There is no returning param on which to say what the result is when the guard skips.

      // === WRITE IT AS A STATEMENT WITH YOUR OWN ACCUMULATOR ===
      // Now YOU choose the value that survives a skipped guard.

      total <- 0
      for accumulatedLimit <- getLimit() then accumulatedItem in 1 ... 3
        total += accumulatedItem
      stdout.println(`total after skipped guard: ${total}`)

      // === OR HOIST THE GUARD INTO AN 'if', LEAVING THE STREAM UNGUARDED ===

      if hoistedLimit <- getLimit()
        collected <- for hoistedItem in 1 ... 3 | collect as List of Integer
        stdout.println(`collected ${length collected}`)
      stdout.println("hoisted guard skipped the block")

      // === ':=' IS NOT A GUARD - THE PIPELINE ALWAYS RUNS ===

      plainTarget as Integer?
      for plainTarget := getLimit() with plainItem in 1 ... 2 > stdout
      stdout.println(`plain assign ran anyway, target set? ${plainTarget?}`)

Common mistakes

E08056 — A guard can skip the whole pipeline, and a stream expression has no returning param on which to declare the value produced on the skip path. Use the statement form with your own accumulator, or hoist the guard into an enclosing 'if'. See ek9 -h E08056 for details.

Incorrect:

      total <- for accumulatedLimit <- getLimit() then accumulatedItem in 1 ... 3 | collect as List of Integer

Correct:

      total <- 0
      for accumulatedLimit <- getLimit() then accumulatedItem in 1 ... 3
        total += accumulatedItem

E50050 — A stream loop variable is scoped to the enclosing block, not to the pipeline, so two pipelines in the same block cannot both declare 'skippedItem' (nor both re-declare the guard 'skippedLimit'). Rename one, or put one in its own block. See ek9 -h E50050 for details.

Incorrect:

      for skippedLimit <- getLimit() then skippedItem in 1 ... 3 > stdout
      for skippedLimit <- getLimit() then skippedItem in 1 ... 3 > stdout

Correct:

      for skippedLimit <- getLimit() then skippedItem in 1 ... 3 > stdout
Other ways to ask this
  • Why does my guarded stream pipeline give E08056?
  • Can I use a guard with 'collect as' in EK9?
  • How do I guard a stream for-range?
  • Guard on a stream pipeline expression not allowed
  • How do I skip a whole stream pipeline when a value is unset?
  • Why is a guard fine on a stream statement but not a stream expression?
  • E08056 guarded stream as expression

Coming from another language?

Java: Stream.of(...) has no guard concept; you write an if around the stream, or an empty-stream branch. Kotlin: '?.let { }' around the sequence, or 'takeIf'. Rust: iterators are lazy and a None short-circuits via '?', but there is no pre-flow that skips the whole chain. Python: a conditional expression around a comprehension. EK9: the guard is part of the loop head in both loop and stream form, and the compiler refuses the expression shape rather than silently handing back whatever the collect type default-constructs.

Keywords: guard, unset, skip, collect, statement, forrange, stream, expression, collect as, accumulator, E08056, for, pipeline, preflow, E50050