List comprehension reference

List comprehension provides a concise syntax for creating and manipulating lists in Brossa. This page covers syntax patterns, operators, and examples.

Common use cases

  • Dynamic form sections: Build repeatable form sections with variable-length data collection. See, e.g., Record construction.

  • Conditional data display: Show fields only when certain conditions are met within list items. See, e.g., Conditional field inclusion.

  • Business rule validation: Count and validate items based on their properties. See, e.g., Counting filtered items.

  • Structured output generation: Transform input data into formatted output structures. See, e.g., Data point aggregation.

Syntax

Basic form

The following are examples of list comprehension in Brossa:

  • Simple list comprehension with just a generator:

    sum_list([sov.value(id) | id <- sov.ids])
  • List comprehension with a guard condition to filter items:

    sum_list([sov.value(id) | id <- sov.ids, sov.coverage(id) == "All"])

The general pattern is:

[expression | generator]
[expression | generator, condition]
  • expression: The output value for each item.

  • generator: Iterates through a collection using variable ← collection, where variable represents each item from collection.

  • condition: An optional boolean expression to filter which items from the collection are included in the output.

The complete formal syntax is:

[ expression | item (, item)* [,] ]

Where each item can be a generator, local binding, or guard (explained below).

Types of items

Items are the components after the | symbol. They are processed left-to-right, and each can reference names bound by earlier items:

Generator: name ← expression

  • Iterates through a collection, where name represents each item from the collection.

  • The operator draws items from the collection one by one.

  • Accepts lists, sets, and table row lists (e.g., table_records(…​)).

  • For example, i ← [1, 2, 3] processes each number in turn.

  • See, e.g., Simple transformation.

Local binding: let name = expression

  • Defines variables that you can use within the comprehension.

  • These variables are calculated fresh for each item being processed.

  • Later bindings can redefine (shadow) earlier variable names.

  • For example, let doubled = 2 * i creates a variable called doubled.

  • See, e.g., Multiple generators with filtering.

Guard: expression

  • A boolean condition that must be yes for an item to be included in the output.

  • If the guard evaluates to no, that item is skipped entirely.

  • For example, x > 5 only includes items where x is greater than 5.

  • See, e.g., Filtering.

Rules

These rules govern how list comprehensions work and what you can do with them:

  • Multiple generators create combinations: When you have multiple generators like i ← [1,2], j ← [3,4], the comprehension creates all possible pairs: (1,3), (1,4), (2,3), (2,4).

  • Left-to-right processing: Each item can reference names bound by earlier items in the list. For example, [result | i ← numbers, let doubled = 2 * i, doubled > 10] works because doubled is defined after i.

  • Variable naming restrictions: The variable names you create with generators (i ← list) and let bindings (let x = value) cannot use names that Brossa reserves for built-in functions and types. For example, you cannot name a variable length because that’s a built-in function.

  • Comma formatting: Use commas to separate different parts of your comprehension. You can include a trailing comma at the end, which can make it easier to add more items later.

  • Comments allowed: You can include comments within list comprehensions using Brossa's standard comment syntax.

  • Obstruction handling: If any element or guard obstructs in a list comprehension, the full list will be obstructed. Use the alt operator (<|>) to provide fallback values and prevent obstruction:

    sum_list([sov.value(id) <|> 0 | id <- sov.ids, sov.deduction(id) == "All" <|> no])
  • Output type: List comprehensions always produce a list, even if the input was a set or other collection type.

Type considerations

Choose appropriate types for your list comprehensions to ensure they work correctly:

ID types: Use typed UUIDs for list item identification:

type IncidentId = Uuid<"incident">;
incidents_list: #{IncidentId};

Expression types: Ensure expression types match expected output:

  • Text for text collections.

  • Record types for structured data.

  • Int for numeric calculations.

Metakeys

Configure how list comprehensions appear and behave in the user interface:

Heading: Add section titles to goals containing list comprehensions:

incidents has heading: Text "Behaviour incidents";

Prompt: Configure UI prompts for list collections:

incidents_list has {
    prompt: Text = "Behaviour incident"
};

Examples

The following examples show how to use list comprehensions in practice:

Simple transformation

Transform each item in a list using a mathematical operation:

[2 * i | i <- [1, 2, 3]]
// Result: [2, 4, 6]

Filtering

Select only items that meet specific criteria. The following example selects only incidents marked as "Dangerous":

[id | id <- incidents_list, incident.level(id) == "Dangerous"]

Multiple generators with filtering

Combine items from multiple sources and apply complex logic:

[2 * i + j | let xs = [1,2,3], i <- xs, j <- xs, let ij = i + j, ij == 4]
// Result: [5, 6, 7]

Cross-product generation

Create all possible combinations from multiple lists:

[{name: n, role: r} | n <- ["Alice","Bob"], r <- ["Broker","Underwriter"]]

Record construction

Build structured data from collections of identifiers:

[{
    description: incident.description(incident_id),
    level: incident.level(incident_id),
    reported: if incident.level(incident_id) == "Dangerous" then incident.reported(incident_id)
} | incident_id <- incidents_list]

Common patterns

The following are frequently used patterns that solve common tasks:

Counting filtered items

Count how many items in a collection meet specific criteria:

dangerous_count = length([id | id <- incidents_list, incident.level(id) == "Dangerous"]);

Conditional field inclusion

Include fields in records only when certain conditions are met:

[{
    id: item_id,
    status: if condition(item_id) == yes then "active" else "inactive"
} | item_id <- item_list]

Data point aggregation

Collect related data points into structured output:

incidents = {
    incidents: [{
        description: incident.description(incident_id),
        level: incident.level(incident_id)
    } | incident_id <- incidents_list]
};

Usage contexts

List comprehensions are commonly used in the following contexts in Brossa specs:

Goal definitions

Use list comprehension to build structured goal outputs:

incidents = {
    incidents: [record | incident_id <- incidents_list]
};

Validation rules

Apply business logic with filtered counts:

when length([id | id <- list, condition(id)]) >= threshold
then refer with "message";

Derived data points

Calculate values from list data:

@derived
total_dangerous = length([id | id <- incidents_list, incident.level(id) == "Dangerous"]);

Buildtree v3 requirements

List comprehensions have specific requirements when working with Buildtree v3:

Subform definitions

All subforms must use list comprehension syntax:

my.subform = [stuff(id) | id <- my.subform.ids];

Literal list definitions are not supported in v3.

Table integration

Legacy syntax

List comprehension generator must be the table’s ID set:

table_data = [row(id) | id <- table.ids];

Requires table render metakey.

Modern syntax

Use generate_table syntax instead of list comprehension for new table implementations.