Skip to main content

Collections

In the Compute Engine, collections group together multiple elements into one unit. Each element in a collection is a Expression.

Introduction

The most common types of collection are:

TypeDescriptionSee
listCollection of elements accessible by their index, duplicates allowedList
setCollection of unique elementsSet
tupleCollection with a fixed size and optional namesTuple
rangeAn index span: a contiguous, ascending run of 1-based indexesRange
stringAn indexed collection of character — the string's grapheme clustersStrings
dictionaryCollection of key-value pairs with string keysDictionary
recordStructured data with a fixed set of known string keys

Collections are immutable: they cannot be modified in place.
Instead, operations on collections produce new collections.

A string is an indexed collection too: its elements are its characters (grapheme clusters), so Length, At, Contains, Map, Filter and the rest apply to it directly. Which operators give a string back rather than a list is covered by the string-preservation rule in the Strings reference; in short, operators that select or reorder the source's own characters (Reverse, Take, Sort, Unique, Filter, …) return a string, and element-transforming ones (Map, FlatMap, Scan, Zip) always return a list.

Collections can be used to represent vectors, matrices, sets, mappings, or records — in both finite and infinite forms.

Core Properties of Collections

All collections share these basic properties:

  • Elements of the collection can be enumerated
  • Elements of the collection can be counted
  • Membership of an element can be checked
  • Subset relationships with another collection can be checked

Note: Depending on the collection, counting and membership checking can be an expensive operation. See the information on specific collections for details.

In addition, indexed collections support:

  • Index-based access: elements can be accessed by their index.
  • Finding elements: elements matching a predicate can be found by their index.

Indexed Collections and Non-indexed Collections

Collections fall into two broad categories:

  • Indexed collections, such as List, Tuple and a string

    → Elements can be accessed by an index, an integer that indicates the position of the element in the collection.

  • Non-indexed collections, such as Set and Record

    → Elements cannot be accessed by index. They can be enumerated or looked up by key.

The first element of an indexed collection has index 1, the second element has index 2, and so on. The last element has index equal to the length of the collection.

Negative indexes can also be used to access elements from the end of the collection, if the collection is finite.

The last element has index -1, the second to last element has index -2, and so on. This is useful for accessing elements without knowing the length of the collection.

["At", ["List", 2, 5, 7, 11], 3]
// ➔ 7

["At", ["List", 2, 5, 7, 11], -3]
// ➔ 5

Nested Collections

The elements of a collection are its top-level elements: a nested collection such as a matrix (a list of lists) is a collection of its rows.

Collection operations apply to those top-level elements. For example Count of a 3×3 matrix is 3 (the number of rows), First is its first row, and Take, Drop and Slice select rows.

["Count", ["List", ["List", 2, 3, 4], ["List", 6, 7, 9]]]
// ➔ 2

["First", ["List", ["List", 2, 3, 4], ["List", 6, 7, 9]]]
// ➔ ["List", 2, 3, 4]

To access a scalar entry of a nested collection, use At with multiple indexes (e.g. ["At", matrix, i, j]), and to operate on the scalar entries, flatten the collection first with Flatten.

Finite and Infinite Collections

Collections may be:

  • Finite: containing a definite number of elements
  • Infinite: continuing indefinitely (for example, a sequence of all natural numbers)
  • Indeterminate: containing an unknown number of elements, such as a stream of data that may end at some point

Compute Engine supports lazy evaluation to make working with infinite collections possible.

Lazy Collections and Eager Collections

Collections can be:

  • Eager: elements are fully evaluated when the collection is created.
  • Lazy: elements are evaluated only as they are accessed.

Lazy collections are useful when working with expensive computations and necessary when working with infinite collections.

Some operations like Range, Cycle, Iterate, Repeat create lazy collections.

Reading a lazy collection repeatedly is cheap. The elements of a lazy collection are computed on first read and cached per instance: walking the same collection again — or reading it by index — serves the cached elements instead of re-evaluating them. The cache is invalidated precisely: it is refreshed when a symbol the collection (transitively) depends on is reassigned, when an assumption or definition changes, or when an engine setting such as precision or tolerance changes — but not by assignments to unrelated symbols, so an animation loop updating one variable does not force unrelated collections to recompute.

One consequence for random-valued elements: a collection whose elements draw random values (["Map", ["Function", ["Random"], "x"], xs]) draws once per instance — every reader of that instance sees the same values, like a list. A re-created instance draws fresh values. See the WithRandomSeed notes for making draws reproducible.

Materializing a lazy collection involves evaluating all its elements and storing them in memory, resulting in an eager collection. This is also known as realizing the collection.

To materialize a collection use ListFrom or SetFrom. These functions enumerate all elements of a finite collection and produce a matching eager collection.

["ListFrom", ["Range", 1, 10]]
// ➔ ["List", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Lazy infinite collections provide a natural way to model mathematical sequences, iterative processes, or cyclic patterns, with minimal memory use.

Common examples include:

  • Natural numbers (["Range"])
  • Cyclic patterns (["Cycle"])
  • Iterative computations (["Iterate"])

For example, let's say you want to express the first 10 prime numbers:

["ListFrom",
["Take",
["Filter", ["Range", 1, "Infinity"], ["IsPrime", "_"]],
10
]
]
// ➔ ["List", 2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

In this expression, only the first 10 prime numbers are computed, and only as the elements are accessed. Without the ListFrom, the Take stays a lazy collection: ["Take", ["Filter", ...], 10] is finite (at most 10 elements), but its elements are only produced on demand.

Use Range, not Integers, for an infinite indexed source

Integers and the other number domains are sets: unordered, so they have no indexes, and operators that require an indexed collection — Take, Drop, At, First, Second, Third, Last, Rest, Most — reject them with an incompatible-type error. Filter preserves the kind of its source, so filtering a set yields a set, and Take rejects that too.

For an infinite source that can be indexed and Taken, use ["Range", 1, "Infinity"] (the positive integers in their natural order).

Lazy collections are partially materialized when converting an expression to a string representation, such as when using the expr.latex, expr.toString() or expr.print() methods. A placeholder is inserted to indicate missing elements.

const expr = ce.expr(["Map", ["Square", "_"], ["Range", 1, "Infinity"]]);
expr.print();
// ➔ [1,4,9,16,25,...]

Materialization Cap

The ce.maxCollectionSize property (default 10_000) bounds how many elements a lazy collection may have when it is converted to a concrete List. Sites that would otherwise build an oversize list — such as Repeat(value, count) with a large count, or the eager-materialization path for a finite indexed collection — leave the expression in its lazy form instead. Elements remain individually accessible via .at() and the iterator.

ce.maxCollectionSize = 5;
ce.expr(['Repeat', 7, 100]).evaluate();
// ➔ ["Repeat", 7, 100] (stays lazy; would exceed the cap)

ce.expr(['Repeat', 7, 3]).evaluate();
// ➔ ["List", 7, 7, 7]

Set ce.maxCollectionSize to Infinity (or to 0 / a negative number) to disable the cap. The setter normalizes any non-positive value to Infinity, mirroring ce.iterationLimit and ce.recursionLimit.

The cap applies to materialization specifically. Element-wise operations on lazy collections (broadcasting an operator like Add over a Range, or applying a user-defined lambda to a list) are not currently bounded by maxCollectionSize; use a finite, explicitly-materialized list if strict size enforcement is required throughout the evaluation pipeline.

Eager Collections

Eager collections are fully materialized when they are created. This means that all elements are computed and stored in memory, making them immediately available for access.

Some of the eager collections include:

  • List: indexed collections of elements, which are also used to represent vectors and matrices. Elements in a list are accessed by their index, which starts at 1. Lists can contain duplicate elements and they can contain an infinite number of elements.

    Type: list<T> where T is the type of the elements.

  • Sequence: a sequence is a list but it is handled differently in the Compute Engine. It is used to splice elements into an expression where an element is expected. The Nothing symbol is a synonym for the empty sequence.

  • Set: non-indexed collections of unique elements. The elements in a set are not accessed by index, they are enumerated. A set can contain an infinite number of elements.

    Type: set<T> where T is the type of the elements.

  • Tuple: indexed collections of elements, but with a fixed number of elements that have a specific type and an optional name.

    Type: tuple<T1, T2, ..., Tn> where T1, T2, ..., Tn are the types of the elements.

  • Dictionary: non-indexed collections of key-value pairs, where each key is unique.

    Type: either dictionary<V> where V is the type of the values, the keys are strings or record{K1: T1, K2: T2, ..., Kn: Tn} where K1, K2, ..., Kn are the keys and T1, T2, ..., Tn are the types of the values. The dictionary type is used when the set of keys is not known in advance, for example when a dictionary is used as a cache. The record type is used when the set of keys is known in advance and fixed, for example to represent a structured data type.

Lazy Collections

Some functions evaluate to a lazy collection. This is useful for creating infinite collections or for collections that are expensive to compute.

Examples of function evaluating to a lazy collection include:

  • Range and Linspace: indexed sequences of numbers (integers and reals, respectively) with a specified start, end and step size.
  • Cycle: infinite collections that repeat a finite collection.
  • Iterate: infinite collections that apply a function to an initial value repeatedly.
  • Repeat: infinite collections that repeat a single value.
  • Fill and Tabulate: collections of a specified size, where each element is computed by a function or set to a specific value.

Types

  • The type collection represents any collection, whether indexed or not, finite or infinite.
  • The type indexed_collection applies to collections that support index-based access, such as List, and Tuple.

Operations on Collections

Operations on all collections, whether indexed or not, include:

  • Filter, Map, and Reduce: operations that create new collections by applying a function to each element of an existing collection.
  • Count, IsEmpty: check the number of elements of a collection.
  • Join, Zip: combine multiple collections into one.
  • Tally: count the number of occurrences of each element in a collection.

Operations on indexed collections:

Predicate and key arguments accept a shorthand

Wherever an operation below takes a predicate or a key function, the argument can be written either as a full function literal — ["Function", ["Greater", "x", 5], "x"] — or as a shorthand function literal: an expression whose wildcards (_, _1, _2, …) or free unknowns become its parameters. The two are equivalent:

["CountIf", ["List", 5, 2, 10, 18], ["Greater", "_", 5]]
// ➔ 2

A bare "_" is the identity function — the shorthand of the shorthand:

["Map", "_", ["List", 1, 2, 3]]
// ➔ ["List", 1, 2, 3]

["ChunkBy", ["List", 1, 1, 2, 2, 3], "_"]
// ➔ ["List", ["List", 1, 1], ["List", 2, 2], ["List", 3]]

Only the bare "_" means the identity: "_1", "_2", … are the positional parameters of an enclosing shorthand, and any other symbol may name a function, so those are left alone.

Creating Eager Collections

This section contains functions that create eager collections from some elements.

Sequence(...elements:any) -> collection

A sequence is a collection of elements. When a sequence is used where an element is expected, the elements of the sequence are spliced into the expression.

["List", 1, ["Sequence", 2, 3], 4]
// ➔ ["List", 1, 2, 3, 4]

The Nothing symbol is a synonym for the empty sequence ["Sequence"]. When the Nothing symbol is used in a context where an element is expected, it is ignored.

["List", 1, "Nothing", 2]
// ➔ ["List", 1, 2]

List(...elements:any) -> list

A List is an indexed collection of elements. An element in a list may be repeated.

\lbrack 42, 3.14, x, y \rbrack
$$$\lbrack 42, 3.14, x, y \rbrack$$
["List", 42, 3.14, "x", "y"]

The type of a list is list<T>, where T is the type of the elements in the list. The type list is a shorthand for list<any>, meaning the list can contain elements of any type.

A ["Spread", xs] element splices the elements of the collection xs into the list (in Epsil, [...xs, c]). The splice happens at canonicalization with Join's semantics: a literal list splices immediately, a symbolic or lazy operand lowers to the equivalent Join expression (a lone spread ["List", ["Spread", "xs"]] is ["Join", "xs"]), and an infinite operand stays lazy. A tuple does not spread — tuples are units; use ListFrom to convert one explicitly — so a provably-tuple operand is a spread-tuple error, and a scalar operand is an incompatible-type error. A string is an indexed collection of characters, so it spreads into its characters ([..."ab"] is ["a", "b"]). Set literals accept Spread elements the same way (deduplicating), and a Dictionary literal merges spread dictionaries with later entries winning on key collisions.

["List", ["Spread", ["List", 1, 2]], 3]
// ➔ ["List", 1, 2, 3]

The visual presentation of a List expression can be customized using the Delimiter function.

const xs = ce.expr(["List", 5, 2, 10, 18]);

xs.latex
// ➔ "\bigl\lbrack5, 2, 10, 18\bigr\rbrack"

ce.expr(["Delimiter", xs, "<;>"]).latex;
// ➔ "\langle5;2;10;18\rangle"

A vector is represented using a List of numbers.

\lbrack 1, 2, 3 \rbrack
$$$\lbrack 1, 2, 3 \rbrack$$
["List", 1, 2, 3]

A matrix is represented using a List of rows of numbers, where each row is a List of numbers.

\lbrack \lbrack 1, 2, 3 \rbrack, \lbrack 4, 5, 6 \rbrack, \lbrack 7, 8, 9 \rbrack \rbrack
$$$\lbrack \lbrack 1, 2, 3 \rbrack, \lbrack 4, 5, 6 \rbrack, \lbrack 7, 8, 9 \rbrack \rbrack$$
["List",
["List", 1, 2, 3],
["List", 4, 5, 6],
["List", 7, 8, 9]
]

In LaTeX, lists of lists can also be represented using a ; separator:

\lbrack 1, 2, 3 ; 4, 5, 6 ; 7, 8, 9 \rbrack
$$$\lbrack 1, 2, 3 ; 4, 5, 6 ; 7, 8, 9 \rbrack$$

And matrices can be represented using LaTeX environments with the \begin{} and \end{} commands:

\begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{pmatrix}
$$$\begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{pmatrix}$$
MathJSONLaTeX
["List", "x", "y", 7, 11]\lbrack x, y, 7, 11\rbrack
["List", "x", "Nothing", "y"]\lbrack x,,y\rbrack

Set(...elements:any) -> set

A non-indexed collection of unique elements.

\lbrace 12, 15, 17 \rbrace
$$$\lbrace 12, 15, 17 \rbrace$$
["Set", 12, 15, 17]

The type of a set is set<T>, where T is the type of the elements in the set.

The type set is a shorthand for set<any>, meaning the set can contain elements of any type.

If the same element is repeated, it is included only once in the set. The elements are compared using the IsSame function.

["Set", 12, 15, 17, 12, 15]
// ➔ ["Set", 12, 15, 17]

The elements in a set are not ordered. When enumerating a set, the elements are returned in an arbitrary order, and two successive enumerations may return the elements in a different order.

The elements in a set are counted in constant time.

A ["Spread", xs] element splices the elements of the collection xs into the set, deduplicating as usual (in Epsil, {1, ...s}). The same rules as for a List spread apply: tuples do not spread (a spread-tuple error; use ListFrom to convert), a scalar operand is an incompatible-type error, and a string spreads into its characters.

["Set", 1, ["Spread", ["List", 2, 2, 3]]]
// ➔ ["Set", 1, 2, 3]

IndexedSequence(term, index, lower)

IndexedSequence(term, index, lower, upper)

The sequence-braces notation \{a_n\}_{n=1}^{\infty} parses to an IndexedSequence: the indexed family whose term is a_n, with index symbol n ranging from lower (optionally up to upper).

\{a_n\}_{n=1}^{\infty}
$$$\{a_n\}_{n=1}^{\infty}$$
["IndexedSequence", ["a_", "n"], "n", 1, "PositiveInfinity"]

The term uses the operator-call form (["a_", "n"]) so that the index binding survives. A set-membership subscript maps the set's least element to the lower bound:

// \{a_n\}_{n\in\mathbb{N}}
["IndexedSequence", ["a_", "n"], "n", 0]

IndexedSequence is currently inert: it is unchanged by evaluate() and simplify(), and round-trips through LaTeX (it does not yet carry collection semantics).

Note that the bare braces \{a_n\} remain a Set, and the parenthesized form (a_n)_{n\in\mathbb{N}} is unchanged.

Creating Lazy Collections

Range(upper:number) -> range | indexed_collection<integer>

Range(lower:number, upper:number) -> range | indexed_collection<integer>

Range(lower:number, upper:number, step:number) -> range | indexed_collection<number>

A sequence of numbers, starting with lower, ending with upper, and incrementing by step.

If the step is not specified, it is assumed to be 1.

["Range", 3, 9]
// ➔ ["List", 3, 4, 5, 6, 7, 8, 9]

["Range", 1, 10, 2]
// ➔ ["List", 1, 3, 5, 7, 9]

If there is a single argument, it is assumed to be the upper bound, and the lower bound is assumed to be 1.

["Range", 7]
// ➔ ["List", 1, 2, 3, 4, 5, 6, 7]

If the lower bound is greater than the upper bound, the step must be negative.

["Range", 10, 1, -1]
// ➔ ["List", 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

Element type narrows by step. When the step is omitted or is an integer literal, the result is indexed_collection<integer>. When the step is non-integer (e.g. 0.1) or a symbolic expression, the result widens to indexed_collection<number>.

["Range", 0, 1, 0.1]
// type: indexed_collection<number>
// ➔ ["List", 0, 0.1, 0.2, 0.3, ..., 1.0]

A range that describes collection indexes has the range type. When the bounds are integers, at least 1, ascending, and one apart — that is, when the value is a contiguous run of valid 1-based indexes — the result takes the narrower range type, an index span. Every other case keeps the indexed_collection types above.

["Range", 2, 5] // type: range (an index span)
["Range", 7] // type: range (the one-argument form means 1..7)
["Range", 1, 10, 2] // type: indexed_collection<integer> (stepped: a gather, not a span)
["Range", 5, 2] // type: indexed_collection<integer> (descending)
["Range", 0, 5] // type: indexed_collection<integer> (0 is not an index)

The narrowing loses no information — a range is still an indexed_collection<integer>, so anything that accepted a Range before still does. What it adds is the ability for an operator that consumes an index span to require a usable one at the type level, rejecting a descending or stepped range at the call site instead of at run time. Note that range is an index span, not a mathematical interval — for those, see Interval — and not the statistical range of a data set, which is Max minus Min.

LaTeX Syntax

In addition to \operatorname{range}(...) and the .. infix form, Range can be authored inside a list literal using ellipsis notation:

[1...9] % endpoint-only form
[1, 3, ..., 9] % inferred-step form (step = 3 - 1 = 2)
[0, 0.1, 0.2, ..., 1] % inferred float step

The ellipsis token can be ... (three periods), \ldots, or \dots. In the inferred-step form, the step is computed from the first sample pair and all intermediate samples are validated against the inferred step within ce.tolerance. Inconsistent samples (e.g. [0, 0.1, 0.5, ..., 1]) produce a parse error.

Outside [...] brackets, the ellipsis tokens continue to parse as the ContinuationPlaceholder symbol.

Linspace(upper:real) -> indexed_collection

Linspace(lower:real, upper:real) -> indexed_collection

Linspace(lower:real, upper:real, count:integer) -> indexed_collection

Linspace is short for "linearly spaced", from the MATLAB function of the same name.

A sequence of numbers evenly spaced between lower and upper. Similar to Range but the number of elements in the collection is specified with count instead of a step value.

If the count is not specified, it is assumed to be 50.

If there is a single argument, it is assumed to be the upper bound, and the lower bound is assumed to be 1.

["Linspace", 3, 10]
// ➔ ["List", 3, 3.142857142857143, 3.2857142857142856,
// 3.4285714285714284, ..., 9.714285714285715, 9.857142857142858, 10]
// (50 elements, spaced by 7/49)

["Linspace", 2]
// ➔ ["List", 1, 1.0204081632653061, 1.0408163265306123,
// 1.0612244897959184, ..., 1.9591836734693877, 1.9795918367346939, 2]
// (50 elements from the default lower bound 1, spaced by 1/49)

["Linspace", 1, 10, 5]
// ➔ ["List", 1, 3.25, 5.5, 7.75, 10]

["Linspace", 10, 1, 10]
// ➔ ["List", 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

Fill(f:function, dimensions:tuple) -> list

lazy

Produce a matrix of the given dimensions by applying f to each pair of row and column indexes. The function comes first and the dimensions are a Tuple, so ["Fill", f, ["Tuple", 2, 3]] is a 2×3 matrix whose element at row i, column j is f(i, j).

["Fill",
["Function", ["Add", "i", "j"], "i", "j"],
["Tuple", 2, 3]
]
// ➔ ["List", ["List", 2, 3, 4], ["List", 3, 4, 5]]

A constant function fills every cell with the same value:

["Fill", ["Function", 0], ["Tuple", 2, 3]]
// ➔ ["List", ["List", 0, 0, 0], ["List", 0, 0, 0]]

Fill always builds a two-dimensional result: the dimension tuple must carry both a row count and a column count. To build a one-dimensional collection from an index, use Tabulate instead.

Tabulate(f:function, dimension:integer) -> collection

Tabulate(f:function, rows:integer, columns:integer) -> collection

Create a collection by applying a function to every index in the specified dimensions. Indices start at 1.

["Tabulate", ["Function", ["Square", "i"], "i"], 5]
// ➔ ["List", 1, 4, 9, 16, 25]

With multiple dimensions, the function receives one index for each dimension and the result is a nested list.

["Tabulate", ["Function", ["Add", "i", "j"], "i", "j"], 2, 3]
// ➔ ["List", ["List", 2, 3, 4], ["List", 3, 4, 5]]

Unlike Fill, Tabulate takes each dimension as a separate argument.

Table(f:function, dimension:integer, ...) -> collection

Table(body, spec-1, ...spec-n) -> collection

An alias for Tabulate that additionally accepts Mathematica-style iterator specs. Each spec names an index variable and its bounds, and body is evaluated once per index value.

A spec may be written with braces or with parentheses — {k, lo, hi} (a Set) and (k, lo, hi) (a Tuple) are equivalent:

["Table", ["Square", "k"], ["Tuple", "k", 1, 5]]
// ➔ ["List", 1, 4, 9, 16, 25]

["Table", ["Square", "k"], ["Set", "k", 1, 5]]
// ➔ ["List", 1, 4, 9, 16, 25]

A fourth element is a step, in either spelling:

["Table", "k", ["Tuple", "k", 1, 10, 4]]
// ➔ ["List", 1, 5, 9]

Multiple specs iterate as nested loops, the first spec being the outermost, and produce a nested list:

["Table", ["Multiply", "i", "j"], ["Tuple", "i", 1, 2], ["Tuple", "j", 1, 3]]
// ➔ ["List", ["List", 1, 2, 3], ["List", 2, 4, 6]]

With no iterator spec — for example ["Table", f, 5]Table behaves exactly like Tabulate.

The tuple and brace spellings are interchangeable, step included: ["Sum", "k", ["Tuple", "k", 1, 10, 2]] and ["Sum", "k", ["Set", "k", 1, 10, 2]] both evaluate to 25. Integrate has no step slot, so a 4-element spec is not recognized there in either spelling (the expression stays unevaluated).

Repeat(value: any) -> indexed_collection

An infinite collection of the same element.

Repeat(value: any, count: integer?) -> indexed_collection

A collection of the same element repeated count times.

["Repeat", 42, 5]
// ➔ ["List", 42, 42, 42, 42, 42]

Note: ["Repeat", n] is equivalent to ["Cycle", ["List", n]]. See Cycle for more information.

Cycle(seed:collection) -> indexed_collection

A collection that repeats the elements of the seed collection. The seed collection must be finite.

["Cycle", ["List", 5, 7, 2]]
// ➔ ["List", 5, 7, 2, 5, 7, 2, 5, 7, ...]

["Cycle", ["Range", 3]]
// ➔ ["List", 1, 2, 3, 1, 2, 3, 1, 2, ...]

Use Take to get a finite number of elements.

["Take", ["Cycle", ["List", 5, 7, 2]], 5]
// ➔ ["List", 5, 7, 2, 5, 7]

Iterate(f:function) -> indexed_collection

Iterate(f:function, initial:any) -> indexed_collection

An infinite collection built by applying f repeatedly, starting from the initial value: element k is f(k, element(k-1)), and element(0) is initial. The initial value is not itself an element of the collection.

If initial is not specified, it is Nothing.

A f declared with two parameters receives the 1-based index and the previous element. A unary f — including the wildcard shorthand below — receives the previous element only.

Use Take to get a finite number of elements.

["Take", ["Iterate", ["Multiply", "_", 2], 1], 5]
// ➔ ["List", 2, 4, 8, 16, 32]

["Take", ["Iterate", ["Add", "_", 2], 7], 5]
// ➔ ["List", 9, 11, 13, 15, 17]

With the two-parameter form, the index is available — here the factorials:

["Take", ["Iterate", ["Function", ["Multiply", "n", "acc"], "n", "acc"], 1], 5]
// ➔ ["List", 1, 2, 6, 24, 120]

Accessing Elements of Collections

Elements of indexed collections can be accessed using their index.

Indexes start at 1 for the first element. Negative indexes access elements from the end of the collection, with -1 being the last element.

At(xs: indexed_collection, index: integer)

Returns the element at the specified index.

["At", ["List", 5, 2, 10, 18], 2]
// ➔ 2

["At", ["List", 5, 2, 10, 18], -2]
// ➔ 10

At(xs: indexed_collection, ...indexes: integer)

If the collection is nested, the indexes are applied in order.

["At", ["List", ["List", 1, 2], ["List", 3, 4]], 2, 1]
// ➔ 3

Applying At repeatedly is equivalent to supplying several indexes at once. In Epsil syntax, both m[2][1] and m[2, 1] select the same matrix element. Indexing a matrix once returns a row with its collection type preserved, so the result can be indexed again.

At(xs: indexed_collection, indices: indexed_collection<integer>)

When the index is a collection of integers, At returns a new list containing the elements of xs at those positions. Out-of-range positions are silently filtered.

["At", ["List", 10, 20, 30, 40, 50], ["List", 1, 3, 5]]
// ➔ ["List", 10, 30, 50]

At(xs: indexed_collection, mask: indexed_collection<boolean>)

When the index is a collection of True/False values, At returns the elements of xs where the mask is True. If the mask is shorter than xs, the iteration stops at the end of the mask.

["At", ["List", 10, 20, 30, 40], ["List", "True", "False", "True", "False"]]
// ➔ ["List", 10, 30]

Filtering with a Condition

A boolean condition in index position filters the collection: relational operators (<, <=, >, >=, =, !=) broadcast elementwise over a list operand, producing the boolean mask that At then applies.

L[L>0]
$$$L[L>0]$$
ce.assign("L", ce.parse("[-1, 2, -3, 4]"));
ce.parse("L[L>0]").evaluate();
// ➔ ["List", 2, 4]

The condition does not have to reference the filtered list itself — it can be another list of the same length (L[d=4] where d is a list), or a positional mask computed from a Range:

// Remove the k-th element of L. (The index symbol here is `k`, not `i`:
// a bare `i` parses as the imaginary unit.)
ce.parse("L[|[1...\\operatorname{length}(L)]-k|>0]");
// ➔ ["At", "L", ["Less", 0, ["Abs", ["Add", ["Negate", "k"], ["Range", 1, ["Length", "L"]]]]]]

The mask is applied positionally and truncates to the shorter of the collection and the mask. A comparison between two collections is not elementwise — [1,2,3] = [1,2,3] evaluates to True (whole-value equality); only a collection-versus-scalar comparison broadcasts.

Subscript Notation

When a symbol is declared as a collection type, subscripts in LaTeX are automatically converted to At expressions:

ce.declare('v', 'list<number>');
ce.parse('v_n'); // → ["At", "v", "n"]
ce.parse('v_{n+1}'); // → ["At", "v", ["Add", "n", 1]]
ce.parse('v_{p,q}'); // → ["At", "v", "p", "q"]

You can also use bracket notation, which always produces At regardless of the symbol's type:

v[n]
$$$v[n]$$
["At", "v", "n"]

The type of the At expression is inferred from the collection's element type, so v_n where v is list<number> has type number and can be used in arithmetic expressions.

Keys(dictionary: dictionary) -> list<string>

Return the dictionary keys as strings, in dictionary iteration order.

["Keys", ["Dictionary", ["KeyValuePair", "a", 1], ["KeyValuePair", "b", 2]]]
// ➔ ["List", "a", "b"]

Values(dictionary: dictionary) -> list

Return the dictionary values in dictionary iteration order.

["Values", ["Dictionary", ["KeyValuePair", "a", 1], ["KeyValuePair", "b", 2]]]
// ➔ ["List", 1, 2]

Field(value: any, field: string)

Access a named field of a value — p.x in Epsil.

On a record or dictionary value, ["Field", d, "'x'"] behaves exactly as ["At", d, "'x'"], including the position-preserving absence marker for a key a dictionary may not have.

["Field", ["Dictionary", ["KeyValuePair", "a", 10]], "'a'"]
// ➔ 10

On a value of a nominal type whose definition body has named fields — a record body, or a named-tuple body — the field resolves through the type's definition. This is the sanctioned accessor window of the nominal-types design: it reads one named field off the definition's field map and does not make the value a collection (First(p) and p["x"] keep rejecting). A field name that is not in a record or named-tuple definition is an unknown-field error value.

On an operand whose type is unknown, Field stays symbolic.

First(xs: indexed_collection)

Return the first element of the collection.

["First", ["List", 5, 2, 10, 18]]
// ➔ 5

["First", ["Tuple", "x", "y"]]
// ➔ "x"

It's equivalent to ["At", xs, 1].

Second(xs: indexed_collection)

Return the second element of the collection.

["Second", ["Tuple", "x", "y"]]
// ➔ "y"

It's equivalent to ["At", xs, 2].

Third(xs: indexed_collection)

Return the third element of the collection.

["Third", ["List", 5, 2, 10, 18]]
// ➔ 10

It's equivalent to ["At", xs, 3].

Last(xs: indexed_collection)

Return the last element of the collection.

["Last", ["List", 5, 2, 10, 18]]
// ➔ 18

It's equivalent to ["At", xs, -1].

Most(xs: indexed_collection) -> list

Return everything but the last element of the collection.

["Most", ["List", 5, 2, 10, 18]]
// ➔ ["List", 5, 2, 10]

It's equivalent to ["Reverse", ["Drop", ["Reverse", xs], 1]].

Rest(xs: indexed_collection) -> list

Return everything but the first element of the collection.

["Rest", ["List", 5, 2, 10, 18]]
// ➔ ["List", 2, 10, 18]

It's equivalent to ["Drop", xs, 1].

Take(xs: indexed_collection, n: integer) -> indexed_collection

lazy

Return a list of the first n elements of xs. The collection xs must be indexed.

A non-positive n is clamped to zero: Take never counts from the end, so n <= 0 yields the empty collection whatever xs is. To take a suffix, drop the prefix before it (["Drop", xs, ["Subtract", ["Length", xs], 2]]) or slice with negative bounds (["Slice", xs, -2, -1]).

["Take", ["List", 5, 2, 10, 18], 2]
// ➔ ["List", 5, 2]

["Take", ["List", 5, 2, 10, 18], -2]
// ➔ ["List"]

See Drop for a function that returns everything but the first n elements.

Drop(xs:collection, n:integer) -> collection

Return a list without the first n elements.

A negative n drops nothing: the count is clamped to zero, so the collection comes back unchanged. To drop a suffix, take the prefix that remains (["Take", xs, ["Subtract", ["Length", xs], 2]]).

["Drop", ["List", 5, 2, 10, 18], 2]
// ➔ ["List", 10, 18]

["Drop", ["List", 5, 2, 10, 18], -2]
// ➔ ["List", 5, 2, 10, 18]

See Take for a function that returns the first n elements.

Slice(xs:indexed_collection, start:integer, end:integer) -> list

lazy

Return the elements from the 1-based start index through the end index, inclusive. Negative indices are counted from the end of the collection, and out-of-bounds indices are clamped (a start past the end yields an empty list).

["Slice", ["List", 5, 2, 10, 18], 2, 3]
// ➔ ["List", 2, 10]

["Slice", ["List", 5, 2, 10, 18], -3, -1]
// ➔ ["List", 2, 10, 18]

Slice(xs:indexed_collection, span:range) -> list

Return the elements at the indexes of an index span: ["Slice", xs, r] is ["Slice", xs, ["First", r], ["Last", r]], with the same clamping.

The argument must be a range — an ascending, step-1, finite span of 1-based indexes such as ["Range", 2, 3]. A descending or stepped Range (["Range", 3, 2], ["Range", 1, 9, 2]) is not a range and is rejected as a type error: unpacking it into (start, end) bounds would contradict its own meaning (["Slice", xs, 3, 2] is empty, but the collection ["Range", 3, 2] is the pair [3, 2]). To gather elements at arbitrary indexes, in any order or with any step, use At with a collection of indexes.

["Slice", ["List", 5, 2, 10, 18], ["Range", 2, 3]]
// ➔ ["List", 2, 10]

["Slice", ["List", 5, 2, 10, 18], ["Range", 3, 9]]
// ➔ ["List", 10, 18]

Slice(xs:indexed_collection, span:range | nothing) -> list | nothing

A Nothing span passes through as Nothing. This arm exists so that a span produced by RangeOf — which answers Nothing when the needle is absent — can be sliced without a test in between:

["Slice", ["List", 9, 7, 5, 3], ["RangeOf", ["List", 9, 7, 5, 3], ["List", 7, 5]]]
// ➔ ["List", 7, 5]

["Slice", ["List", 9, 7, 5, 3], ["RangeOf", ["List", 9, 7, 5, 3], ["List", 1, 2]]]
// ➔ "Nothing"

TakeWhile(xs:collection, predicate:function) -> collection

lazy

Returns the leading elements of xs for as long as the predicate is True, stopping at (and excluding) the first element for which the predicate is not True.

["TakeWhile", ["List", 1, 2, 3, 10, 1], ["Function", ["Less", "x", 5], "x"]]
// ➔ ["List", 1, 2, 3]

Because it is lazy and stops early, TakeWhile composes with infinite collections.

["TakeWhile", ["Range", 1, "Infinity"], ["Function", ["Less", "x", 4], "x"]]
// ➔ ["List", 1, 2, 3]

See DropWhile for the complementary operation.

DropWhile(xs:collection, predicate:function) -> collection

lazy

Skips the leading elements of xs for as long as the predicate is True, then yields the remaining elements. Once an element fails the predicate, the rest of the collection is returned unchanged (the predicate is not applied again).

["DropWhile", ["List", 1, 2, 3, 10, 1], ["Function", ["Less", "x", 5], "x"]]
// ➔ ["List", 10, 1]

See TakeWhile for the complementary operation.

Changing the Order of Elements

Reverse(xs: string) -> string

Reverse(xs: list) -> list

Reverse(xs: indexed_collection) -> list

lazy

Return the collection in reverse order.

A list operand keeps its type, shape included (a vector<3> reversed is a vector<3>), and a string operand gives back a string. Any other indexed collection — a tuple, a range, an opaque indexed collection — results in a list of the same elements: a reversed tuple's element types would come back in the wrong order, and a reversed range is descending, which the range type excludes.

["Reverse", ["List", 5, 2, 10, 18]]
// ➔ ["List", 18, 10, 2, 5]

RotateLeft(xs: list, count: integer) -> list

RotateLeft(xs: indexed_collection, count: integer) -> list

Returns a collection where the elements are rotated to the left by the specified count.

["RotateLeft", ["List", 5, 2, 10, 18], 2]
// ➔ ["List", 10, 18, 5, 2]

RotateRight(xs: list, count: integer) -> list

RotateRight(xs: indexed_collection, count: integer) -> list

Returns a collection where the elements are rotated to the right by the specified count.

["RotateRight", ["List", 5, 2, 10, 18], 2]
// ➔ ["List", 10, 18, 5, 2]

RandomShuffle(xs: indexed_collection) -> indexed_collection

Return the collection in random order.

["RandomShuffle", ["List", 5, 2, 10, 18]]
// ➔ ["List", 10, 18, 5, 2]

There is no seed argument: wrap the call in WithRandomSeed(seed, …) to make the permutation reproducible.

["WithRandomSeed", 42, ["RandomShuffle", ["List", 5, 2, 10, 18]]]
// ➔ the same permutation on every evaluation

A permutation needs every element, so the collection is materialized; a collection larger than 1,000,000 elements is refused with an out-of-range error rather than attempted.

A permutation of a string is a string — the result is built from the source's own characters, so the kind is preserved, as it is for Reverse and Take. The same holds for RandomSample.

["RandomShuffle", {str: "abcdef"}]
// ➔ "dbeafc" (a string, not a list of characters)

Shuffle was renamed to RandomShuffle; the old name throws an operator-removed error for one release.

Sort(xs: indexed_collection) -> indexed_collection

Sort(xs: indexed_collection, order-function: function) -> indexed_collection

Return the collection in sorted order. The collection must be indexed — an unordered collection such as a Set has no positions to permute, and is an incompatible-type error. A string subject sorts its characters and comes back as a string (["Sort", "dcba"] is "abcd"), like every other element-preserving operator.

["Sort", ["List", 18, 5, 2, 10]]
// ➔ ["List", 2, 5, 10, 18]

The optional function is interpreted by its arity:

  • A two-argument comparator f(a, b) returns a negative number when a should come before b, zero when they are equivalent, and a positive number otherwise (like a conventional compare function).

    ["Sort", ["List", 3, 1, 2], ["Function", ["Subtract", "b", "a"], "a", "b"]]
    // ➔ ["List", 3, 2, 1]
  • A one-argument key function f(x) sorts the elements ascending by the key value f(x). The sort is stable: elements with equal keys keep their original relative order.

    ["Sort", ["List", -3, 1, -2], ["Abs", "_"]]
    // ➔ ["List", 1, -2, -3]

Ordering(collection) -> indexed_collection

Ordering(collection, order-function) -> indexed_collection

Return the indexes of the collection in sorted order.

["Ordering", ["List", 5, 2, 10, 18]]
// ➔ ["List", 2, 1, 3, 4]

To get the values in sorted order, index the collection with the ordering — At accepts a collection of indexes:

["At", ["List", 5, 2, 10, 18], ["Ordering", ["List", 5, 2, 10, 18]]]
// ➔ ["List", 2, 5, 10, 18]

// Same as Sort:
["Sort", ["List", 5, 2, 10, 18]]
// ➔ ["List", 2, 5, 10, 18]

MaxBy(xs: collection, f: function) -> value

Return the element of xs for which the key f(x) is largest. The first occurrence wins on ties.

["MaxBy", ["List", -3, 1, -2], ["Function", ["Abs", "x"], "x"]]
// ➔ -3

MaxBy stays unevaluated on an empty or infinite collection, or when a key comparison is undetermined.

MinBy(xs: collection, f: function) -> value

Return the element of xs for which the key f(x) is smallest. The first occurrence wins on ties.

["MinBy", ["List", -3, 1, -2], ["Function", ["Abs", "x"], "x"]]
// ➔ 1

MinBy stays unevaluated on an empty or infinite collection, or when a key comparison is undetermined.

ArgMax(xs: indexed_collection) -> integer

ArgMax(xs: indexed_collection, f: function) -> integer

Return the 1-based index of the element of xs for which the key f(x) is largest. When no key function is given, the elements themselves are compared. The first occurrence wins on ties.

["ArgMax", ["List", 5, 2, 10, 18]]
// ➔ 4

["ArgMax", ["List", -3, 1, -2], ["Function", ["Abs", "x"], "x"]]
// ➔ 1

ArgMax stays unevaluated on an empty or infinite collection, or when a key comparison is undetermined.

ArgMin(xs: indexed_collection) -> integer

ArgMin(xs: indexed_collection, f: function) -> integer

Return the 1-based index of the element of xs for which the key f(x) is smallest. When no key function is given, the elements themselves are compared. The first occurrence wins on ties.

["ArgMin", ["List", 5, 2, 10, 18]]
// ➔ 2

["ArgMin", ["List", -3, 1, -2], ["Function", ["Abs", "x"], "x"]]
// ➔ 2

ArgMin stays unevaluated on an empty or infinite collection, or when a key comparison is undetermined.

Operating On Collections

Length(xs:any) -> integer

Return the number of elements in a finite collection. If the argument is not a collection or is infinite, the expression remains unevaluated.

["Length", ["List", 5, 2, 10, 18]]
// ➔ 4

For collections, Length and Count produce the same result; Count is restricted to collection arguments by its signature.

Count(xs: collection) -> integer

Returns the number of elements in the collection.

When the collection is a matrix (list of lists), Count returns the number of rows.

["Count", ["List", 5, 2, 10, 18]]
// ➔ 4

Count(xs: collection, value: any) -> integer

With a second argument that is not a function, returns how many elements of xs are equal to value. The value is compared using structural identity, like the Same operator — the same comparison Contains uses. Number leaves compare by exact value, so 0.5 counts as an occurrence of 1/2.

["Count", ["List", 1, 2, 2, 3, 2], 2]
// ➔ 3

["Count", ["List", 1, 2, 2, 3], 5]
// ➔ 0

Count(xs: collection, pred: function) -> integer

With a second argument that is a function, returns how many elements satisfy the predicate. The predicate follows the Filter contract: it must return True or False for each element.

["Count", ["List", 1, 2, 3, 4, 5], ["Greater", "_", 2]]
// ➔ 3

A shorthand predicate is told from a value by its wildcard: ["Greater", "_", 2] carries one, so it is applied as a predicate, while True carries none and is counted as an occurrence.

["Count", ["List", "True", "False", "True"], "True"]
// ➔ 2

Both two-argument forms require a finite collection; over an unbounded collection the expression remains unevaluated.

IsEmpty(xs: collection) -> boolean

Returns the symbol True if the collection has no elements.

["IsEmpty", ["List", 5, 2, 10, 18]]
// ➔ "False"

["IsEmpty", ["List"]]
// ➔ "True"

["IsEmpty", "x"]
// ➔ ["IsEmpty", "x"] (undecided: `x` is not known to be a collection)

["IsEmpty", {str: "Hello"}]
// ➔ "False"

Contains(xs: collection, value: any) -> boolean

Returns True if the collection contains the given value, False otherwise. The value is compared using the IsSame function (structural identity, like the Same operator).

Contains(xs, v) is the value-membership specialization of Any: it is equivalent to Any(xs, (e) |-> e === v). To test an arbitrary predicate instead of a specific value, use Any.

["Contains", ["List", 5, 2, 10, 18], 10]
// ➔ "True"

["Contains", ["List", 5, 2, 10, 18], 42]
// ➔ "False"

Contains tests for one element. To test for a contiguous run of several elements — a sublist, or a substring — use ContainsSequence.

IndexOf(xs:collection, value:any) -> integer

Return the 1-based index of the first occurrence of value, or 0 if it is not present.

["IndexOf", ["List", 5, 2, 10, 2], 2]
// ➔ 2

["IndexOf", ["List", 5, 2, 10, 2], 42]
// ➔ 0

IndexOf searches for one element. To search for a contiguous run of several elements — a sublist, or a substring — use RangeOf.

RangeOf(xs:indexed_collection, needle:indexed_collection) -> range | nothing

RangeOf(xs:indexed_collection, needle:indexed_collection, from:integer) -> range | nothing

The span of the first occurrence of needle as a contiguous subsequence of xs, as a 1-based inclusive range, or Nothing when the needle does not occur.

["RangeOf", ["List", 9, 7, 5, 3], ["List", 7, 5]]
// ➔ ["Range", 2, 3]

["RangeOf", ["List", 9, 7, 5, 3], ["List", 7, 3]]
// ➔ "Nothing" (7 and 3 are not adjacent)

RangeOf and its three companions ContainsSequence, StartsWith and EndsWith read their second argument as a sequence of elements, which is what distinguishes them from Contains and IndexOf:

QuestionOperator
Is v one of the elements?Contains(xs, v)
At what index is the element v?IndexOf(xs, v)
Do these elements occur consecutively?ContainsSequence(xs, needle)
At what indexes?RangeOf(xs, needle)

Keeping them apart is what avoids an ambiguity the collection library cannot resolve: with nested lists, a single overloaded operator could not tell IndexOf([[1,2],[3,4]], [3,4]) — "the element equal to [3,4]" — from "the subsequence 3-then-4".

The optional from is the index to start searching at (default 1). The returned span is always in the original collection's indexes, so finding the next occurrence is RangeOf(xs, needle, Last(r) + 1) for non-overlapping matches (or First(r) + 1 to allow overlaps), and finding all of them is that loop run until it answers Nothing.

Domain rules:

CaseResult
Needle absentNothing
from past the end of xsNothing — never an error, since a match at the very end legitimately produces Length(xs) + 1
from less than 1, or not an integerAn error value
Empty needleAn error value — an empty span is not representable (["Range", 1, 0] is the descending range [1, 0], not an empty one)
Infinite or unknown-length subject or needleThe expression stays symbolic — searching one would not terminate when the needle is absent

The span composes with Slice: when the needle is found, Slice(xs, RangeOf(xs, needle)) has the same element sequence as the needle.

["Slice", ["List", 9, 7, 5, 3], ["RangeOf", ["List", 9, 7, 5, 3], ["List", 7, 5]]]
// ➔ ["List", 7, 5]

Slice also accepts the Nothing that a failed search answers, and passes it through, so the two compose without a test in between.

On a string the elements are characters, so the search is character-wise and the span is in character indexes; that case, including the grapheme guarantees it gives, is documented in the strings reference.

ContainsSequence(xs:indexed_collection, needle:indexed_collection) -> boolean

Whether needle occurs in xs as a contiguous subsequence. For a non-empty needle this is RangeOf not answering Nothing.

["ContainsSequence", ["List", 1, 2, 3], ["List", 2, 3]]
// ➔ "True"

["ContainsSequence", ["List", 1, 2, 3], ["List", 3, 2]]
// ➔ "False"

An empty needle is True: the empty sequence is a subsequence of everything. This is the one edge rule that diverges from RangeOf's, which has to reject an empty needle because it must return a span.

StartsWith(xs:indexed_collection, prefix:indexed_collection) -> boolean

EndsWith(xs:indexed_collection, suffix:indexed_collection) -> boolean

Whether xs begins with prefix, or ends with suffix, as a contiguous subsequence. An empty prefix or suffix is True.

["StartsWith", ["List", 1, 2, 3], ["List", 1, 2]]
// ➔ "True"

["EndsWith", ["List", 1, 2, 3], ["List", 2, 3]]
// ➔ "True"

EndsWith has to inspect the tail, so beyond the finiteness rule it needs a known length: over a collection whose length is not known the expression stays symbolic.

IndexWhere(xs: indexed_collection, predicate:function) -> number

Returns the 1-based index of the first element in the collection that satisfies the predicate, or 0 if not found.

["IndexWhere", ["List", 5, 2, 10, 18], ["Greater", "_", 9]]
// ➔ 3

Find(xs: indexed_collection, predicate:function)

Returns the first element in the collection that satisfies the predicate, or Nothing if none found.

["Find", ["List", 5, 2, 10, 18], ["Greater", "_", 9]]
// ➔ 10
["Find", ["List", 5, 2, 10, 18], ["Greater", "_", 100]]
// ➔ "Nothing"

CountIf(xs: indexed_collection, predicate:function) -> number

Returns the number of elements in the collection that satisfy the predicate.

["CountIf", ["List", 5, 2, 10, 18], ["Greater", "_", 5]]
// ➔ 2

Position(collection, predicate:function)

Returns a list of indexes of elements in the collection that satisfy the predicate.

["Position", ["List", 5, 2, 10, 18], ["Greater", "_", 5]]
// ➔ ["List", 3, 4]
To test a predicate over a collection, use Any/All

Exists and ForAll are logical quantifiers, not collection operators. They take a condition and a proposition — not a collection and a predicate function — and they bind a variable. For "does any/every element of this collection satisfy this predicate?", reach for Any and All instead.

["Any", ["List", 5, 2, 10, 18], ["Greater", "_", 15]]
// ➔ "True"

["All", ["List", 5, 2, 10, 18], ["Greater", "_", 0]]
// ➔ "True"

Exists(condition, proposition:boolean)

The existential quantifier: True when the proposition holds for at least one value of the quantified variable.

The variable is introduced by the first operand, either as a bare symbol or — so the engine can decide the proposition by enumeration — as an ["Element", _variable_, _domain_] condition over a finite domain.

["Exists", ["Element", "x", ["Set", 5, 2, 10, 18]], ["Greater", "x", 15]]
// ➔ "True"

["Exists", ["Element", "x", ["Set", 5, 2, 10]], ["Greater", "x", 15]]
// ➔ "False"

Exists is a binder: the quantified variable is scoped to the proposition and shadows any outer symbol of the same name, so an assigned value never leaks into the quantified formula.

See also NotExists and ExistsUnique in the Logic reference.

ForAll(condition, proposition:boolean)

The universal quantifier: True when the proposition holds for every value of the quantified variable. Like Exists, it binds the variable introduced by its first operand, and it is decided by enumeration when that operand is an ["Element", _variable_, _domain_] condition over a finite domain.

["ForAll", ["Element", "x", ["Set", 5, 2, 10, 18]], ["Greater", "x", 0]]
// ➔ "True"

["ForAll", ["Element", "x", ["Set", 5, 2, 10, 18]], ["Greater", "x", 5]]
// ➔ "False"

Written in LaTeX, \forall x \in \{1, 2, 3\}, x > 0 parses to ["ForAll", ["Element", "x", ["Set", 1, 2, 3]], ["Greater", "x", 0]].

See also NotForAll in the Logic reference.

Any(xs: collection) -> boolean

Any(xs: collection, predicate: function) -> boolean

Returns True if at least one element of the collection satisfies the predicate, False otherwise.

When no predicate is given, the elements themselves are treated as booleans.

["Any", ["List", 1, 2, 3], ["Function", ["Greater", "x", 2], "x"]]
// ➔ "True"

["Any", ["List", "False", "True", "False"]]
// ➔ "True"

Any short-circuits: it stops at the first element that satisfies the predicate, so it can return a definite answer even for an infinite collection.

["Any", ["Range", 1, "Infinity"], ["Function", ["Greater", "x", 5], "x"]]
// ➔ "True"

Any of an empty collection is False. When the answer depends on symbolic or undetermined elements, the expression stays unevaluated.

To test whether a collection contains a specific value, use ContainsContains(xs, v) is equivalent to Any(xs, (e) |-> e === v) (structural identity, not the tolerant ==).

["Any", ["List"]]
// ➔ "False"

["Any", ["List", "a", "b"], ["Function", ["Greater", "x", 0], "x"]]
// ➔ ["Any", ["List", "a", "b"], ["Function", ["Greater", "x", 0], "x"]]

All(xs: collection) -> boolean

All(xs: collection, predicate: function) -> boolean

Returns True if every element of the collection satisfies the predicate, False otherwise.

When no predicate is given, the elements themselves are treated as booleans.

["All", ["List", 1, 2, 3], ["Function", ["Greater", "x", 0], "x"]]
// ➔ "True"

["All", ["List", 1, 2, 3], ["Function", ["Greater", "x", 2], "x"]]
// ➔ "False"

All short-circuits: it stops at the first element that fails the predicate, so it can return False even for an infinite collection.

["All", ["Range", 1, "Infinity"], ["Function", ["Less", "x", 5], "x"]]
// ➔ "False"

All of an empty collection is True. When the answer depends on symbolic or undetermined elements, the expression stays unevaluated.

["All", ["List"]]
// ➔ "True"

Filter(xs: collection, pred: function) -> collection

Returns a collection where pred is applied to each element of the collection. Only the elements for which the predicate returns "True" are kept.

An indexed source (a list, tuple or range) yields a list of the kept elements — never the source's own shape or arity, since filtering changes the length. A set source yields a set.

["Filter", ["List", 5, 2, 10, 18], ["Less", "_", 10]]
// ➔ ["List", 5, 2]

Map(f:function, ...xss:collection) -> collection

Returns a collection where f is applied to each element of xs. The mapping function is the first argument, followed by one or more source collections.

["Map", ["Function", ["Add", "x", 1], "x"], ["List", 5, 2, 10, 18]]
// ➔ ["List", 6, 3, 11, 19]
["Map", ["Multiply", "_", 2], ["List", 5, 2, 10, 18]]
// ➔ ["List", 10, 4, 20, 36]

Map is variadic over its sources: when several collections are given, f is applied element-wise across them (a zipWith), receiving one element from each collection. The result has the length of the shortest input collection.

["Map",
["Function", ["Add", "x", "y"], "x", "y"],
["List", 1, 2, 3],
["List", 10, 20, 30]]
// ➔ ["List", 11, 22, 33]

The number of source collections is the number of arguments f receives. A function literal (or a symbol with a known, non-generic signature) whose parameter count cannot match it is a callback-arity error at canonicalization — the closures a partially applied callback would otherwise produce are never what was meant. This contract holds for every callback-taking collection operator: Filter, Any, All, Count, TakeWhile, FlatMap and their kin supply one argument (the element), Reduce, Fold and Scan supply two (the accumulator and the element), Fill supplies two (row and column), and Sort/Ordering (a key or a comparator) and Iterate (f(previous) or f(index, previous)) accept either of their two arities. A nullary literal such as ["Function", 42] is a constant and is applied at any arity. To take a pair apart inside a callback, use a tuple-pattern parameter, ["Function", body, ["Tuple", "p", "q"]] (Epsil ((p, q)) => …), which is one parameter.

["Map", ["Function", ["Add", "p", "q"], "p", "q"], ["List", 1, 2, 3]]
// ➔ ["Error", ["ErrorCode", "'callback-arity'", ...]]

FlatMap(xs:collection, f:function) -> list

lazy

Applies f to each element of xs and concatenates the results into a single list. When f returns a collection, its elements are spliced into the output; a non-collection result is included as a single element.

["FlatMap", ["List", 1, 2, 3], ["Function", ["List", "x", "x"], "x"]]
// ➔ ["List", 1, 1, 2, 2, 3, 3]
["FlatMap", ["List", 1, 2, 3], ["Function", ["Range", 1, "x"], "x"]]
// ➔ ["List", 1, 1, 2, 1, 2, 3]

A scalar result is kept as a single element rather than raising an error.

["FlatMap", ["List", 1, 2, 3], ["Function", ["Multiply", "x", 2], "x"]]
// ➔ ["List", 2, 4, 6]

Comprehension(body, element-1, element-2, ...) -> list

The list-comprehension operator. Evaluates body once for each combination of one or more ["Element", _name_, _collection_] clauses and collects the results into a List.

["Comprehension", ["Square", "x"], ["Element", "x", ["Range", 1, 3]]]
// ➔ ["List", 1, 4, 9]

Comprehension(body, Element(x, xs)) is equivalent to Map(x ↦ body, xs). Comprehension additionally supports multiple, possibly dependent, clauses.

Bindings are evaluated as nested loops, outermost = first Element clause. Later clauses see earlier bindings in scope, so a clause's collection can depend on a name bound by an earlier clause.

When all clauses are independent, the result is the Cartesian product:

["Comprehension",
["Tuple", "x", "y"],
["Element", "x", ["Range", 1, 2]],
["Element", "y", ["Range", 1, 2]]]
// ➔ [(1,1), (1,2), (2,1), (2,2)] — 4 tuples

When a later clause depends on an earlier binding, the iteration follows the dependency (and the Cartesian product collapses):

["Comprehension",
["Tuple", "x", "y"],
["Element", "x", ["Range", 1, 3]],
["Element", "y", ["Range", 1, "x"]]]
// ➔ [(1,1), (2,1), (2,2), (3,1), (3,2), (3,3)] — 6 tuples (triangle)

Comprehension is scope-hygienic: bound names do not leak into the enclosing scope.

Comprehension is a value expression, not control flow: Break, Continue and Return inside body are not intercepted by Comprehension. To filter elements, use Filter or Map instead.

The trailing \operatorname{for} LaTeX syntax produces a Comprehension:

(x, y) \keyword{for} x = [1...3], y = [1...x]

Reduce(xs:indexed_collection, fn:function, initial:value?) -> value

Returns a value by applying the reducing function fn to each element of the collection.

Reduce performs a left fold operation: the reducing function is applied to the first two elements, then to the result of the previous application and the next element, etc...

When an initial value is provided, the reducing function is applied to the initial value and the first element of the collection, then to the result of the previous application and the next element, etc...

[
"Reduce",
["List", 5, 2, 10, 18],
["Function", ["Add", "_1", "_2"]],
]
// ➔ 35

The name of a function can be used as a shorthand for a function that takes two arguments.

["Reduce", ["List", 5, 2, 10, 18], "Add"]
// ➔ 35

Scan(xs:collection, f:function) -> collection

Scan(xs:collection, f:function, initial:value) -> collection

lazy

Returns the running fold of f over xs: a collection of the same length as xs whose n-th element is the accumulated result of f applied through the first n elements. Unlike Reduce, which returns only the final value, Scan returns every intermediate value.

["Scan", ["List", 1, 2, 3, 4], "Add"]
// ➔ ["List", 1, 3, 6, 10]

When an initial value is provided, the accumulation starts from it and the first output is f(initial, x1).

["Scan", ["List", 1, 2, 3, 4], "Add", 100]
// ➔ ["List", 101, 103, 106, 110]

Differences(xs:collection) -> collection

lazy

Returns the collection of successive differences of xs, that is x_{n+1} - x_n. The result has one fewer element than the input.

Differences are computed exactly, preserving the type of the operands (integers, rationals, etc.).

["Differences", ["List", 1, 4, 9, 16]]
// ➔ ["List", 3, 5, 7]

Tally(xs:collection) -> tuple<elements:list, counts:list>

Evaluate to a tuple of two lists:

  • The first list contains the unique elements of the collection.
  • The second list contains the number of times each element appears in the collection.
["Tally", ["List", 5, 2, 10, 18, 5, 2, 5]]
// ➔ ["Tuple", ["List", 5, 2, 10, 18], ["List", 3, 2, 1, 1]]

Zip(...xss: indexed_collection)

Returns a collection of tuples where the first element of each tuple is the first element of the first collection, the second element of each tuple is the second element of the second collection, etc.

The length of the resulting collection is the length of the shortest collection.

["Zip", ["List", 1, 2, 3], ["List", 4, 5, 6]]
// ➔ ["List", ["Tuple", 1, 4], ["Tuple", 2, 5], ["Tuple", 3, 6]]

Partition(collection, size:integer)

Partition(collection, size:integer, step:integer)

Partition(collection, predicate:function)

Partitions a collection into chunks of size elements. The trailing chunk may be shorter when size does not divide the length of the collection.

With a step, returns sliding windows of size elements whose starting positions are step apart; only complete windows are included.

If a predicate function is given, splits into two groups: elements for which the predicate is true, and those for which it is false.

To split a collection into a given number of groups, use Chunk instead.

["Partition", ["List", 1, 2, 3, 4, 5], 2]
// ➔ ["List", ["List", 1, 2], ["List", 3, 4], ["List", 5]]
["Partition", ["List", 1, 2, 3, 4, 5], 2, 1]
// ➔ ["List", ["List", 1, 2], ["List", 2, 3], ["List", 3, 4], ["List", 4, 5]]
["Partition", ["List", 1, 2, 3, 4, 5, 6], ["IsEven", "_"]]
// ➔ ["List", ["List", 2, 4, 6], ["List", 1, 3, 5]]

For a string source every part — a chunk, a window or a predicate group — is itself a string, so the result is a list<string>. This holds for all three forms.

["Partition", {str: "abcd"}, 2]
// ➔ ["ab", "cd"]

Chunk(collection, count:integer)

Splits the collection into count nearly equal-sized groups.

To split a collection into chunks of a given size, use Partition instead.

["Chunk", ["List", 1, 2, 3, 4, 5], 2]
// ➔ ["List", ["List", 1, 2, 3], ["List", 4, 5]]

For a string source each group is itself a string, so the result is a list<string>.

["Chunk", {str: "abcdef"}, 2]
// ➔ ["abc", "def"]

GroupBy(collection, function:function)

Partitions the collection into groups according to the value of the grouping function applied to each element. Returns a dictionary mapping group keys to lists of elements. Dictionary keys are strings: the key value returned by the function is stringified.

["GroupBy", ["List", 1, 2, 3, 4], ["IsEven", "_"]]
// ➔ {"dict": {"False": [1, 3], "True": [2, 4]}}

ChunkBy(collection, function:function) -> list

Splits the collection into maximal runs of consecutive elements that share the same key value f(x). Unlike GroupBy, which gathers all elements with the same key regardless of position, ChunkBy only groups elements that are adjacent.

["ChunkBy", ["List", 1, 1, 2, 2, 2, 1], ["Function", "x", "x"]]
// ➔ ["List", ["List", 1, 1], ["List", 2, 2, 2], ["List", 1]]

For a string source each run is itself a string, so the result is a list<string>. The same rule applies to SlidingWindow, Permutations and Combinations.

["ChunkBy", {str: "aabbc"}, ["Function", "x", "x"]]
// ➔ ["aa", "bb", "c"]

Transforming Collections

This section contains functions whose argument is a collection and which return a collection made of a subset of the elements of the input.

Collections are immutable. These functions do not modify the input collection, but return a new collection.

Join(...collection) -> list

Join(...set) -> set

Join(...string) -> string

If the collections are of different types, the result is a List containing the elements of the first collection followed by the elements of the second collection.

["Join", ["List", 5, 2, 10, 18], ["List", 1, 2, 3]]
// ➔ ["List", 5, 2, 10, 18, 1, 2, 3]

If any operand is a set, the result is a Set of the elements of the collections, and repeated elements are kept only once — a set holds distinct elements, however often its operands repeat one.

["Join", ["Set", 5, 2, 10, 18], ["Set", 1, 2, 3]]
// ➔ ["Set", 5, 2, 10, 18, 1, 3]

["Join", ["Set", 1, 2], ["List", 2, 3]]
// ➔ ["Set", 1, 2, 3]

If the arguments are all strings, the result is a string — this is the variadic string concatenation:

["Join", {str: "ab"}, {str: "cd"}]
// ➔ "abcd"

The arm is chosen by the arguments, not by the surrounding types: as soon as one argument is not a string, the generic arm applies and a string operand contributes its characters.

["Join", {str: "ab"}, ["Characters", {str: "cd"}]]
// ➔ ["a", "b", "c", "d"] (a list<character>)

To join the elements of one collection of strings into a string — optionally with a separator — use StringJoin, described in the strings reference.

Append(collection, element) -> collection

Return a collection with element added at the end. Collections are immutable; the input is not modified.

["Append", ["List", 1, 2], 3]
// ➔ ["List", 1, 2, 3]

Insert(collection, index:integer, value:any) -> collection

Return a new collection with value inserted at the 1-based index. The element previously at that index and everything after it shift right. An index equal to n+1 (one past the end) appends.

["Insert", ["List", 1, 2, 3], 2, 99]
// ➔ ["List", 1, 99, 2, 3]

["Insert", ["List", 1, 2, 3], 4, 99]
// ➔ ["List", 1, 2, 3, 99]

Negative indices count from the end, Elixir-style: -1 appends at the very end, -2 inserts before the last element, and so on.

["Insert", ["List", 1, 2, 3], -1, 99]
// ➔ ["List", 1, 2, 3, 99]

["Insert", ["List", 1, 2, 3], -2, 99]
// ➔ ["List", 1, 2, 99, 3]

Collections are immutable; the input is not modified. An out-of-range or symbolic index leaves the expression unevaluated.

DeleteAt(collection, index:integer) -> collection

Return a new collection with the element at the 1-based index removed. Negative indices count from the end (-1 is the last element).

["DeleteAt", ["List", 1, 2, 3], 2]
// ➔ ["List", 1, 3]

["DeleteAt", ["List", 1, 2, 3], -1]
// ➔ ["List", 1, 2]

Collections are immutable; the input is not modified. An out-of-range or symbolic index leaves the expression unevaluated.

The result keeps the kind of the source, so deleting from a string gives a string:

["DeleteAt", {str: "abcdef"}, 2]
// ➔ "acdef"

["DeleteAt", {str: "abcdef"}, -1]
// ➔ "abcde"

ReplaceAt(collection, index:integer, value:any) -> collection

Return a new collection with the element at the 1-based index replaced by value. Negative indices count from the end (-1 is the last element).

["ReplaceAt", ["List", 1, 2, 3], 2, 99]
// ➔ ["List", 1, 99, 3]

["ReplaceAt", ["List", 1, 2, 3], -1, 99]
// ➔ ["List", 1, 2, 99]

Collections are immutable; the input is not modified. An out-of-range or symbolic index leaves the expression unevaluated.

Fold(function, initial, collection)

Apply function from left to right, starting with initial. Fold is the function-first form of a left fold.

["Fold", "Add", 0, ["List", 1, 2, 3, 4]]
// ➔ 10

Unique(xs: collection) -> collection

Returns a list of the elements in xs without duplicates.

This is equivalent to the first element of the result of Tally: ["First", ["Tally", xs]].

["Unique", ["List", 5, 2, 10, 18, 5, 2, 5]]
// ➔ ["List", 5, 2, 10, 18]

Dedup(xs: collection) -> collection

lazy

Collapses consecutive runs of duplicate elements into a single element. Unlike Unique, which removes every later duplicate anywhere in the collection, Dedup only removes duplicates that are adjacent, so a value that reappears after a different value is kept.

["Dedup", ["List", 1, 1, 2, 2, 1]]
// ➔ ["List", 1, 2, 1]

Materializing Collections

Materializing a collection means converting it from a lazy representation to an eager one. This involves evaluating all elements of the collection and storing them in memory.

ListFrom(xs: collection) -> list

SetFrom(xs: collection) -> set

TupleFrom(xs: collection) -> tuple

Returns a materialized list, set or tuple containing the elements of the collection xs.

The collection xs should be a finite collection.

["ListFrom", ["Range", 1, 3]]
// ➔ ["List", 1, 2, 3]

["SetFrom", ["Range", 1, 3]]
// ➔ ["Set", 1, 2, 3]

["TupleFrom", ["Range", 1, 3]]
// ➔ ["Tuple", 1, 2, 3]

A string is a collection of characters, so it materializes into its characters:

["ListFrom", {str: "abc"}]
// ➔ ["List", "a", "b", "c"]

DictionaryFrom(xs: collection) -> map

Returns a dictionary containing the elements of the collection xs.

The collection xs should be a finite collection of key-value pairs, each key being a string.

["DictionaryFrom", ["List", ["Tuple", "'a'", 1], ["Tuple", "'b'", 2]]]
// ➔ {"dict": {"a": 1, "b": 2}}

When every key is a bare identifier, the resulting value's type is a record{…}, so DictionaryFrom is also how you build a record from pairs. There is no separate RecordFrom: a record and a dictionary differ only in the type world, and record-ness is derived from the value.

When the collection contains several pairs with the same key, the last one wins. This is what makes DictionaryFrom the engine of the dictionary merge: a Dictionary literal with ["Spread", d] entries (in Epsil, {...defaults, "verbose" -> True}, or {->, ...d1, ...d2} for a pure merge) lowers to DictionaryFrom over the concatenated entries, so a later entry — literal or spread — overrides an earlier one. (Duplicate literal keys written side by side in one literal are treated as typos instead: first wins, with a diagnostic.)

["DictionaryFrom",
["List", ["Tuple", "'a'", 1], ["Tuple", "'a'", 9], ["Tuple", "'b'", 2]]]
// ➔ {"dict": {"a": 9, "b": 2}}