Skip to content

Parallel collection (++)

The dual of ||: where || runs blocks until one succeeds, ++ runs every block and collects every surviving result into an array.

Syntax

{ Rule_A1() Rule_A2() }(:result)
++ { Rule_B1() }(:result)
++ { Rule_C1() Rule_C2() }(:result)

Each block declares its output with (:var). All blocks must use the same variable name and produce the same value type — checked at compile time.

Semantics

  • All blocks are evaluated independently, each starting from the same input state.
  • Variables modified inside a block stay inside that block — there is no leakage between paths.
  • Each block that produces a value for :var contributes to the result.
  • Blocks that fail (an Assert fires) contribute nothing and are silently dropped — failure of one path does not abort the others.
  • The output of ++ is always Array(T), where T is the declared type of :var.

Result flattening

Outputs are flattened one level:

Path A produces Path B produces ++ output
scalar 1 scalar 2 Array(Int) = [1, 2]
[1, 2] scalar 3 Array(Int) = [1, 2, 3]
[1, 2] [3, 4] Array(Int) = [1, 2, 3, 4]

So a path that internally produces multi-results — typically through UsagiMap()[] — contributes each of those individually to the collected output. Array(Array(T)) never occurs for scalar/array outputs. Tuple outputs are kept as Array(Tuple(...)) to preserve the correlation between fields.

Worked example — concepts from two parallel maps

Same gender_concept_id field as the fallback example, but here we want every concept any map can offer — both the precise and the broad — and let downstream rules deduplicate.

logic: { UsagiMap("map3.csv")[] }(:gender)
logic: ++ { UsagiMap("map4.csv")[] }(:gender)

Read it as: run map3.csv and map4.csv against the same input, keep all matches from both, deliver a single flattened Array(Int) containing every match.

The CDM-table-level LoopOver([:person_id, :gender_concept_id]) then expands the array into one row per match.

When to reach for ++ vs ||

  • ||prefer one path; only fall back to the next if the preferred fails. Used when one path is canonical and others are backups.
  • ++ — every path is equally valid; the union of their results is the answer. Used when source data has multiple valid interpretations or when you want to apply several mappings without picking a winner.

When ++ is overkill

If the parallel paths produce scalar results that you want combined with arithmetic — e.g. compute several values and add them — you do not need ++. Just write the rules sequentially and combine with Add, Selector, etc. ++ is specifically for the "collect array of results" case.

Restrictions

  • ++ blocks cannot directly contain another ++. Indirect containment (e.g. inside an If branch within a ++ block) is fine.
  • ||, If, and multi-result rules (UsagiMap()[]) are all valid inside a ++ path.
  • All blocks must declare the same output variable with the same type.

See also

Source tests

tests/test091/ — basic ++ between two UsagiMap blocks.