Lists
A list holds items of one type in order, like an array in TypeScript,
except that it never changes: every operation that changes a list gives
you a new one. Each operation is called through the list namespace, as
list.<name>. In the table below, T is the item type.
The list fixes the item type. A list written out of values fixes it for
the call it is passed to, as in list.sort_by([oat, zopf], product -> ...). A list with a literal in it takes the parameter's type.
collection is a reserved namespace with no operations. A call through
it is semantic.standard_operation_moved, and its edit writes list.,
set. or map.: chosen by the type of the call's first value when that
value is a name or a field, and list. otherwise. The call is then
checked as that operation.
Some operations take a function, written inline as item -> expression
(Passing functions). Three operations
give their function two values: list.zip_with gives an item from each
list, and map.filter and map.map_values give an entry's key and its
value. Their function takes two parameters, written in parentheses,
(key, value) -> ..., and only that way. A function with one parameter
is never written (item) ->. Each parameter takes the type of the value
the operation gives in its place, and you don't have to use every one.
(The operations' declarations write such a function as
Function<K, V, U>, the last type being its result.)
A function with a different number of parameters than the operation
gives is type.inline_function_parameters
(Two parameters). A function with two
parameters anywhere but these three operations is
type.inline_function_placement, and its reason names the three.
| Operation | Gives | Fails | SQL form |
|---|---|---|---|
count(items) |
Int, at least 0 |
jsonb_array_length(x)::bigint |
|
contains(items, item) |
Bool: an item == item |
none | |
map(items, each) |
List<U>, in order |
none | |
filter(items, keep) |
List<T>, the items keep accepts, in order |
none | |
sum(items) |
Int or Decimal, the items added; 0 for none. Items built on Int or Decimal; Money uses money.sum |
overflow is a defect | none |
any(items, check) |
Bool: check accepts at least one; false for none |
none | |
all(items, check) |
Bool: check accepts every one; true for none |
none | |
find(items, check) |
T?, the first item check accepts |
none | |
first(items), last(items) |
T? |
none | |
at(items, position) |
T?, the item at position (0 is the first); position never negative |
none | |
min_by(items, key), max_by(items, key) |
T?, the item with the smallest or largest key; the first of equal ones |
none | |
sort_by(items, key, locale:) |
List<T>, ordered by key ascending; equal keys keep their order. key gives a number, a time, a date, a duration, text or a standard type the contract orders (Money of one currency): text in code points as queries sort it, or by a locale's rules with locale: |
none | |
reverse(items) |
List<T>, last item first |
none | |
distinct(items) |
List<T>, each item's first occurrence, by == |
none | |
slice(items, from, count) |
List<T>, up to count items from position from, cut short at the end; both never negative |
none | |
keep_first(items, n), keep_last(items, n) |
List<T>, at most n items from that end; n never negative |
none | |
drop_first(items, n), drop_last(items, n) |
List<T> without n items from that end; all gone when n is longer; n never negative |
none | |
append(items, item) |
List<T>, item added at the end |
none | |
concat(first, second) |
List<T>, first's items then second's |
none | |
flatten(lists) |
List<T> from a List<List<T>>, in order |
none | |
group_by(items, key) |
Map<K, List<T>>: each key with its items in order |
none | |
zip_with(first, second, (a, b) -> ...) |
List<U>, pairing items by position, as long as the shorter |
none |
- No push or pop. A list's items never change
(Values never change). To
add an item,
list.append(items, item)gives a new list. To take the last item off, read it withlist.last, thenlist.drop_last(items, 1)gives the rest. - The largest is
max_by, notfirst(reverse(sort_by(...))), which sorts the whole list to read one item. In a query, you still writeorder:andlimit:. - Cutting from either end.
keep_first,keep_last,drop_firstanddrop_lastcut items from either end, and text has the same four.sliceis for a window in the middle. No position counts back from the end: a count or a position is never negative, and one that might be doesn't compile (the operation'snever_negative). A literal below0, or a value whose type has nominof at least0, istype.argument_negative. A call's result,list.count's included, is a plainInt, which proves nothing about its sign, so build a count as a type withmin: 0first. - Keys that order.
sort_by,min_byandmax_byneed a key that can be put in order: one built on a number,Text,Instant,CalendarDateorDuration, or a standard type the contract marks ordered. The key can never benone. AZonedTimehas no order of its own, just as<doesn't compare one (Expressions and operators), so use its instant as the key (temporal.instant). Any other key istype.ordering_operand. Keys compare in the canonical order, the one fixed order Jadpo gives every type's values (Collections, "Set order"), so text sorts by code point, or by a locale's rules withlocale:(Text). - Named types. An operation that gives
T,T?orList<T>keeps the items' named type, such asBatchorBatchWeight. It doesn't keep a named list type, becausefilterordrop_firstcan break that type'smin_length; build the result back into it by construction (Named collection construction).flattentakes aList<List<T>>. Collections are invariant: a list of a named list type doesn't stand in for aList<List<T>>. - Equal means
==.group_bygroups items whose keys are==, and gives the keys in the canonical order, so a key may be of any type.distinctkeeps each item's first occurrence, found through the canonical order, which agrees with==for values of one type. - There is no
items[i]. Uselist.at, which givesT?, and handle the missing case withmatch. An index can be out of range, soitems[i]would need a failure arm or a defect. sumgives a representation, as+does: anIntor aDecimal, by its items' representation. An empty list gives0or0.00, as its type says.Stock(list.sum(counts))builds the result back into a named type. When the items' type has aminof at least0, the sum is proved to be at least0, so building it needs no arm formin. It still needs one formaxwhen the type has one.- No
reduce,foldor comparator. To accumulate, use aforloop with avar mut, orsumfor the common case. To sort, givesort_bya key, andreversethe result for the other direction; there is no sort with a comparator function.
A day's baking, through every list operation:
type BatchName = Text { min_length: 1 max_length: 40}type BatchWeight = Mass {}type Shelf = Int { min: 1 max: 6}type LoafPrice = Decimal { scale: 2 min: 0.00}type Batch = Object { name: BatchName weight: BatchWeight shelf: Shelf price: LoafPrice}type OverPlan = Bool {}/// The flour a day's batches take: `list.sum` adds numbers, and/// `units.sum` adds quantities of one kind.function flour_needed(bakings: List<Batch>) -> BatchWeight { return BatchWeight(units.sum(list.map(bakings, batch -> batch.weight)))}/// The day's takings at full price.function takings(bakings: List<Batch>) -> LoafPrice { return LoafPrice(list.sum(list.map(bakings, batch -> batch.price)))}/// The heaviest batch, if there is one: `max_by` reads each batch once,/// where sorting would order them all to read one.function heaviest(bakings: List<Batch>) -> Batch? { return list.max_by(bakings, batch -> batch.weight)}/// The batches on each shelf, in baking order.function by_shelf(bakings: List<Batch>) -> Map<Shelf, List<Batch>> { return list.group_by(bakings, batch -> batch.shelf)}/// Whether each batch came out heavier than planned, batch by batch: a/// two-parameter inline function is given one item from each list.function over_plan(bakings: List<Batch>, goals: List<BatchWeight>) -> List<OverPlan> { return list.zip_with(bakings, goals, (batch, goal) -> OverPlan(batch.weight > goal))}
Each operation gives a new list, or an item of the list as it is, still
a Batch. takings([]) is 0.00, a Decimal, because a sum has its
items' representation even when there are none. by_shelf's map is read
with map.get and map.keys (Maps).
test "list operations give new lists and keep the items as they are" { var rye: Batch = Batch { name: "Rye" weight: 800g shelf: 2 price: 4.20 } var bap: Batch = Batch { name: "Bap" weight: 90g shelf: 1 price: 0.60 } var spelt: Batch = Batch { name: "Spelt" weight: 800g shelf: 2 price: 3.80 } var day: List<Batch> = [rye, bap, spelt] var weights: List<BatchWeight> = [800g, 90g, 800g] assert flour_needed(day) == 1690g assert takings(day) == 8.60 assert takings([]) == 0.00 assert list.any(day, batch -> batch.shelf == 1) assert not list.all(day, batch -> batch.weight > 100g) assert list.find(day, batch -> batch.shelf == 2) == rye assert list.first(day) == rye assert list.last(day) == spelt assert list.at(day, 1) == bap assert list.at(day, 3) == none assert heaviest(day) == rye // the first of equal keys assert list.min_by(day, batch -> batch.price) == bap assert list.sort_by(day, batch -> batch.name) == [bap, rye, spelt] assert list.sort_by(day, batch -> batch.weight) == [bap, rye, spelt] // equal keys keep their order assert list.reverse(day) == [spelt, bap, rye] assert list.distinct(weights) == [800g, 90g] assert list.slice(day, 1, 5) == [bap, spelt] assert list.keep_first(day, 2) == [rye, bap] assert list.keep_last(day, 2) == [bap, spelt] assert list.drop_first(day, 1) == [bap, spelt] assert list.drop_last(day, 5) == [] assert list.append([rye], bap) == [rye, bap] assert list.concat([rye], [bap, spelt]) == day assert list.flatten([[rye], [], [bap, spelt]]) == day assert over_plan(day, [700g, 100g, 800g]) == [true, false, false] assert over_plan(day, [700g]) == [true] // as long as the shorter}test "group_by keeps each shelf's batches in baking order" { var rye: Batch = Batch { name: "Rye" weight: 800g shelf: 2 price: 4.20 } var bap: Batch = Batch { name: "Bap" weight: 90g shelf: 1 price: 0.60 } var spelt: Batch = Batch { name: "Spelt" weight: 800g shelf: 2 price: 3.80 } var shelves: Map<Shelf, List<Batch>> = by_shelf([rye, bap, spelt]) assert map.keys(shelves) == [1, 2] assert map.get(shelves, 2) == [rye, spelt] assert map.get(shelves, 1) == [bap] assert map.get(shelves, 3) == none assert map.count(by_shelf([])) == 0}
A count or a position that might be negative is type.argument_negative
(Errors), and a key that gives something with no order, such
as a Bool, is type.ordering_operand.
Markdown for agents: /docs/lists.md.Built from Jadpo main at a707efb.