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, wherevariablerepresents each item fromcollection. -
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
namerepresents 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 * icreates a variable calleddoubled. -
See, e.g., Multiple generators with filtering.
Guard: expression
-
A boolean condition that must be
yesfor an item to be included in the output. -
If the guard evaluates to
no, that item is skipped entirely. -
For example,
x > 5only 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 becausedoubledis defined afteri. -
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 variablelengthbecause 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:
-
Textfor text collections. -
Record types for structured data.
-
Intfor 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]
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"]);
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]
};