Skip to content

Predicates & Selectors

Predicates are inline filter expressions used inside patch selectors and collection traversals. They select which elements to update without writing explicit loops or branches.

Predicate syntax

A predicate appears inside square brackets and uses dot-prefixed field access:

aivi
[.active == True]

The dot (.) refers to the current element being tested. The expression must evaluate to Bool.

Predicates in patches

Combine predicates with the <| patch operator to update only matching elements:

aivi
type User = {
    name: Text,
    role: Text,
    active: Bool
}

type { users: List User } -> { users: List User }
func promoteActive = .
 <| { users[.active == True].role: "admin" }

This updates the role field only for users where .active is True. Non-matching users are left unchanged.

Predicate expressions

Predicates support the same comparison operators as regular expressions:

PredicateMeaning
[.active == True]Field equals a value
[.score >= 100]Numeric comparison
[.name == "Ada"]Text equality
[.role == "guest"]Match a specific field value

The dot prefix accesses fields on each element:

aivi
type Item = { name: Text, price: Int, inStock: Bool }

type { items: List Item } -> { items: List Item }
func discountExpensive = .
 <| { items[.price >= 50].price: halve }

Selectors

Selectors are the path expressions inside patch braces that determine what to update. They chain left to right:

SelectorMeaning
fieldSelect a record field
.fieldSame as above (dot-prefixed form)
a.b.cNested field path
[*]Traverse all List elements or Map values
[predicate]Filter elements by predicate
["key"]Select a Map entry by key
[.key == "id"]Select Map entries matching a predicate
ConstructorFocus through a constructor with one payload

Examples of chaining:

aivi
// update all prices in a list
{ items[*].price: double }

// update prices of in-stock items only
{ items[.inStock == True].price: double }

// nested record field
{ profile.address.city: toUpperCase }

// focus through an Option
{ config.Some.retries: increment }

Constructor focus

For single-payload constructors like Some, Ok, Err, Valid, and Invalid, the selector can focus through the constructor:

aivi
type Config = {
    retries: Option Int,
    name: Text
}

value bumpRetries : (Config -> Config) =
    patch { retries.Some: increment }

If the value does not match the constructor (e.g. it is None), the patch leaves it unchanged.

Store syntax

Use := to store a function value as data instead of applying it:

aivi
type Counter = {
    step: Int -> Int
}

value setStep : (Counter -> Counter) =
    patch { step: := increment }

Without :=, the function would be called during patch application. With :=, the function itself becomes the new field value.

Removal syntax

Use : - to remove a field from a record:

aivi
value cleaned = record <| { tempField: - }

The result type reflects the removal — it has one fewer field than the input. See Record Patterns § Patch removal for details.

AIVI Language Manual