Lists and comprehensions
A field can hold a list, and a list is where most of the interesting expressions live.
form basket version = "1.0" {
field items { type = array(0..20) of string }
}
The comprehensions
Five of them, and they all read the same way: what to compute, over what, for which element.
form basket version = "1.0" {
field items { type = array(0..20) of string }
field quantity { type = integer }
field total { type = number value = sum(item in items, toNumber(item) * quantity) }
}
| The form | What it gives you |
|---|---|
sum(x in list, expr) |
the total |
any(x in list, expr) |
is it true for at least one? |
all(x in list, expr) |
is it true for all of them? |
map(x in list, expr) |
a new list |
reduce(x in list, start, expr) |
fold the list into one value |
sum, any and all also work over a group or a repeat:
field overdue { type = boolean value = any(loan in loans, loan.overdue) }
field adults { type = boolean value = all(member in household.members, member.age >= 18) }
The bound name
The name between in and the comma is bound for the body of the expression
and only there:
field names { type = array of string value = map(m in members, m.name) }
Inside the body, m is the current element. Outside it, m means nothing —
a comprehension does not leak its name.
$this and position()
Inside a comprehension, the bound identifier is the element being looked at —
and $this is another name for it. You can use either.
field names { type = array(0..5) of string value = map(row in dependants, $this.name) }
position() is the other half of the story, and it is the one you need inside
a repeat — a list of rows the person fills in:
form household version = "1.0" {
repeat dependants {
max = 5
field name { type = string }
field age { type = integer }
field row_number { type = integer value = position() }
}
}
$thisis the element currently being looked at.position()is its 1-based position in the repeat.
So the first row's row_number is 1, the second 2, the third 3. A
language that can repeat a group but cannot number its elements cannot carry a
household roster — every row would be the same row.
position() outside a repeat has no current element, and the specification
says what that means: the error value.
reduce — folding a list
field running_total { type = number value = reduce(item in items, 0, toNumber(item)) }
reduce takes the list, a starting value, and an expression that gives each
element's contribution. The contributions are added together, starting from
the initial value. It is the one comprehension with
three arguments.
The list builtins
Not everything needs a comprehension. These work on a list directly:
| The call | What it does |
|---|---|
count(list) |
how many |
first(list) / last(list) |
the ends |
sort(list) |
in order |
unique(list) |
without the duplicates |
flatten(list) |
one level deeper |
take(list, n) / skip(list, n) |
a part of the list |
minOf(list) / maxOf(list) |
the smallest or largest |
sum(list) |
the total |
avg(list) |
the mean |
field total { type = number value = sum(amounts) }
field cheapest { type = number value = minOf(prices) }
field first_name{ type = string value = first(names) }
An iterated list must have a bound
Every collection that a comprehension walks over has to declare how long it
can be. A repeat declares it with max; an array declares it in its
type:
form basket version = "1.0" {
field items { type = array(0..20) of string }
field total { type = number value = sum(item in items, toNumber(item)) }
}
An array with no bound — type = array of string — is E-COST-01 when
something walks over it: "the expression iterates an unbounded collection".
The runtime has to be able to say how much work a form is before anybody
fills it in, and it cannot do that for a list that could be any length.
The same rule is why a repeat must declare max, and why the cost model
can bound a whole form.
Lists of objects
A list can hold objects, and then the members are reached with a path:
form basket version = "1.0" {
field lines { type = array(0..20) of object { sku: string, quantity: integer, price: number } }
field total { type = number value = sum(line in lines, line.price * line.quantity) }
field skus { type = array of string value = map(line in lines, line.sku) }
}
line.price is a dotted path that names the member price of the element
line. The rule about dotted paths is in
section 5.
An empty list is not an absent one
A comprehension over an inapplicable collection yields unknown rather than an empty result. The question was not asked, so the answer is not "none".
That matters when you write sum(item in items, …): if items does not apply
— a repeat the person has not started, or a group whose condition is false —
then total is unknown, not 0. The form will say "unknown" rather than
claim that the basket is empty.
An empty list is a different thing: the person answered, and the list has
nothing in it. sum of an empty list is 0, and avg of an empty list is
the error value.
A worked example
form basket version = "1.0" {
field lines { type = array(0..20) of object { sku: string, quantity: integer, price: number } }
field item_count { type = integer value = count(lines) }
field total { type = number value = sum(line in lines, line.price * line.quantity) }
field average { type = number value = avg(map(line in lines, line.price)) }
field in_stock { type = boolean value = all(line in lines, line.quantity > 0) }
field skus { type = array of string value = map(line in lines, line.sku) }
field expensive { type = boolean value = any(line in lines, line.price > 100) }
output {
item_count: integer
total: number
average: number
in_stock: boolean
skus: array of string
expensive: boolean
}
}
Read the value lines one at a time: each is one sentence about the lines,
written as an expression.