Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Relations

A relation is a first-class citizen, declared with rel the way a concept is declared with type. It is an n-ary edge that carries its own data, bounds its endpoints, specializes other relations, and takes part in rules — every affordance a concept has.

The data lives on the edge. An employment has a salary; the salary belongs to neither endpoint but to the relationship between them.

// A relationship that is the thing holding the data: an employment HAS a
// salary. The salary belongs to the relation, not to either endpoint.
pub rel Employment(employee: Person, employer: Org) {
    mut salary: Int,
}

Most data languages cannot say this directly. A foreign key has no identity and no fields of its own; to attach a salary you reify the relationship into a stand-in individual that exists only to hold it. Argon does not reify — the relation is the thing that holds the data.

Cardinality bounds each endpoint independently, read as UML association-end multiplicity:

// Cardinality is per endpoint, read as UML association-end multiplicity: the
// i-th bracket bounds the distinct position-i values for a fixed combination
// of the other endpoints. `[0..1]` on the owner slot means an asset has at
// most one owner; `[0..*]` on the asset slot means a person may own any
// number. The maximum is enforced at the write path; see the README on minimums.
pub rel Owns(owner: Person, asset: Asset) [0..1] [0..*];

[0..1] on the owner slot says an asset has at most one owner; [0..*] on the asset slot says a person may own any number. The maximum is enforced on every write.

Relations are not limited to two endpoints. A sale relates seller, buyer, and item at once — no intermediate SaleRecord to carry the third leg:

// Relations are n-ary. A sale relates three participants at once — no
// intermediate "SaleRecord" node is needed to hold the third leg.
pub rel Sale(seller: Person, buyer: Person, item: Asset);

And a relation specializes another, exactly as a concept does — every Internship is an Employment — so a rule that ranges over Employment tuples sees internships too. The consequence is uniformity: a relation appears in a rule body the way a concept’s extent does, with no special case for “edges.”

// Relations specialize just like concepts: every Internship is an Employment.
pub rel Internship(employee: Person, employer: Org) <: Employment;
// And relations take part in rules. `colleagues` reads the `Employment`
// relation in a rule body exactly as it would read a concept's extent.
pub derive colleagues(a: Person, b: Person) :- Employment(a, o), Employment(b, o);

colleagues reads the Employment relation as a body atom and joins it to itself on the shared employer. The full program runs in Argon by Example at first_class_relations; the precise endpoint and cardinality rules are in Relations.

Relation ends are immutable unless marked mut. An immutable binding is lifetime-bound: it leaves only through the dependent-context cascade triggered by retracting one of its individuals. The mutation forms (retract x; and the atomic set retract {x, y};), the OE1404 coverage refusal, and the remediation for a wrongly named binding are in mutate and the mutation chapter.