Home Documentation Templates Examples Showcase GitHub ↗
Theme

Collection operations

Nift can transform and aggregate immutable JSON arrays at build time with a small set of pure, composable operations. Intermediate results remain typed while Nift consumes them and serialize as JSON only when they reach generated output.

One model, two destinations.

@sort(values) can be rendered directly into JavaScript or JSON, passed into @for or @join, or consumed by another collection operation. Nift keeps the intermediate value as an array rather than flattening it to text and reparsing it.

At a glance

OperationResult
@filter(item : items => expression)Array containing items whose expression is truthy.
@map(item : items => expression)Array containing each expression result.
@sort(items)Stable ascending copy of a scalar number/string array.
@sort(item : items => expression asc|desc)Stable copy ordered by a numeric or string key.
@slice(items, pos, length)Array slice using zero-based position and non-negative length.
@find(item : items => expression)First matching item, or null.
@some(item : items => expression)true when any item matches.
@every(item : items => expression)true when every item matches; true for an empty array.
@distinct(items)Array with later JSON-equal duplicates removed, preserving first occurrence order.
@reverse(items)Reversed copy of the array.
@sum(items)Numeric sum; zero for an empty array.
@prod(items)Numeric product; one for an empty array.
@min(items) / @max(items)Smallest/largest homogeneous number or string; empty arrays are an error.
@reduce(item : items & acc = initial => expression)Fold the array into one value using a pure accumulator expression.

Simple forms

@sort(scores)
@reverse(posts)
@distinct(tags)
@sum(prices)
@prod(weights)
@min(scores)
@max(scores)

@sum and @prod require numeric values. @min and @max accept homogeneous numbers or homogeneous strings and use the same ordering rules as sorting.

Binding forms use =>

When an operation needs a per-item predicate, key or projection, => separates the binding/source from the expression. The expression uses exactly the same pure expression rules as $[expression] and @if(expression).

@filter(post : posts => post.published && post.score >= 80)
@map(product : products => product.price * product.quantity)
@sort(post : posts => post.date desc)
@find(user : users => user.id == target_id)
@some(post : posts => post.featured)
@every(product : products => product.price > 0)
@sum(product : products => product.price * product.quantity)
@max(player : players => player.score)

The old comma-shaped advanced form is not part of this contract: use binding : collection => expression. The binding exists only while that operation evaluates an item. Source arrays and existing bindings are not mutated.

Tuple bindings

When each array element is itself an array, a tuple binding can unpack the positions explicitly. Names are ordinary local bindings; Nift does not infer object-member names from them.

@sum((price, quantity) : pairs => price * quantity)
@prod((a, b, c) : triples => a + b + c)

The tuple arity must exactly match each source element. This keeps unpacking predictable rather than silently dropping or inventing values.

Composition

Collection results remain typed while another Nift construct consumes them, so operations can be nested naturally.

@for(post : @sort(p : @filter(p : posts => p.published) => p.date desc)) {
    <h2>$[post.title]</h2>
}

Or map a filtered collection and hand the resulting string array directly to @join:

@join(
    @map(post : @filter(post : posts => post.published) => post.title),
    ', '
)

@slice makes limiting a transformed collection explicit without adding separate take/skip operations:

@for(post : @slice(@sort(p : posts => p.date desc), 0, 10)) {
    ...
}

Aggregation

@sum, @prod, @min and @max are specialised folds for common cases. Their binding forms avoid a separate @map when the aggregated value is derived from each item.

Cart total: @sum(product : products => product.price * product.quantity)
Highest score: @max(player : players => player.score)

@reduce is the general form:

@reduce(item : values & acc = 0 => acc + item)

@reduce(
    product : products & total = 5
    => total + product.price * product.quantity
)

The initial accumulator is evaluated once. For each item, Nift evaluates the pure reducer expression with both the item binding and the current accumulator binding, then carries the resulting immutable value into the next iteration. Reducing an empty array simply returns the initial value.

A functional-programming flavour, deliberately bounded

This part of Nift intentionally has a functional flavour: values are immutable, collection transforms return new values, operations compose, predicates/projections are pure expressions, and @reduce folds a collection without exposing mutable loop state.

immutable input
    ↓
filter / map / sort / slice
    ↓
new immutable collection
    ↓
sum / max / reduce / for / join / JSON output

That does not turn Nift into a general functional programming language. There is still no assignment statement, collection mutation, user-defined function system, side-effecting callbacks, arbitrary runtime execution or general SQL/LINQ-style query engine. The functional flavour is useful precisely because the operations remain pure and small.

Rendering typed values

When a collection operation reaches output instead of another Nift consumer, Nift serializes the typed result as valid JSON. This makes the operations useful in generated JavaScript as well as HTML templates.

<script>
const published = @filter(post : posts => post.published);
const totals = @map(product : products => product.price * product.quantity);
const grandTotal = @sum(product : products => product.price * product.quantity);
</script>

The generated values preserve their JSON types: arrays, objects, strings, numbers, booleans and null do not become ad-hoc strings inside Nift.

Finding and testing

@find returns the original first matching value rather than a transformed copy. No match renders null. @some short-circuits on the first truthy match; @every short-circuits on the first falsey match and is true for an empty array.

Expressions reference →   Loops & conditions →