First-class relations
Area: Relations Teaches: relations are first-class constructs — n-ary, with their own properties, cardinality, specialization, and rule participation. Prerequisites: concepts and
<:(see concepts and hierarchies). Run:ox build examples/first_class_relations && ox query examples/first_class_relations
In most data languages a relationship is a second-class thing: a foreign key with no identity and no data of its own, or something you reify by hand into a stand-in node. Argon makes a relation a construct, declared with a rel keyword exactly as a concept is declared with a type keyword. An edge has the same standing as a node.
What to read in root.ar
A relation carries its own data. An employment has a salary — the salary belongs to the relationship, not to the person or the org:
pub rel Employment(employee: Person, employer: Org) { mut salary: Int };
Cardinality is per endpoint (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, and [0..*] on the asset slot means a person may own any number.
pub rel Owns(owner: Person, asset: Asset) [0..1] [0..*];
Relations are n-ary, not just binary — a sale relates three participants without an intermediate node:
pub rel Sale(seller: Person, buyer: Person, item: Asset);
Relations specialize, like concepts — every internship is an employment:
pub rel Internship(employee: Person, employer: Org) <: Employment;
Relations participate in rules, read in a rule body exactly as a concept’s extent is:
pub derive colleagues(a: Person, b: Person) :- Employment(a, o), Employment(b, o);
Running it
With Alice and Bob both employed by Acme, colleague_pairs returns the four pairings the rule derives — including each person with themselves, since the rule as written does not exclude a = b (adding an inequality guard is a natural exercise).
Honest caveats (what runs today)
- Maximum cardinality is enforced at the write path; a minimum above zero (e.g.
[1..1]) is recorded on the wire but not yet enforced in v0 —ox checkemitsOW1342naming the relation and position. This example uses[0..1]/[0..*], so it has no unenforced minimum.
This example is compiled and run in CI — the corpus auto-discovery parses and resolves it, and a corpus test (oxc-runtime/tests/examples_corpus.rs) pins the colleague_pairs behaviour to exactly the four pairings the rule derives. If the language changes underneath it, the build breaks rather than the docs going stale.
Source
The package’s real Argon source, transcluded from the file this corpus compiles — what you read here is exactly what CI builds.
//! First-class relations: an edge is a citizen, the same as a node.
//!
//! In most data languages a relationship is either a foreign key (no identity,
//! no data of its own) or something you reify by hand. In Argon a relation is a
//! first-class construct, declared with a `rel` keyword the way a concept is
//! declared with a `type` keyword. It can be n-ary, carry its own properties,
//! constrain its endpoints' cardinality, specialize another relation, and take
//! part in rules — every affordance a concept has.
//!
//! Demonstrates:
//! * a relation that carries its OWN data (`Employment` has a salary)
//! * per-endpoint cardinality (UML association-end multiplicity)
//! * an n-ary (ternary) relation — relations are not limited to binary
//! * relation specialization with `<:`
//! * a relation participating in a `derive` rule, exactly as a concept does
pub type Person {
name: String,
}
pub type Org {
name: String,
}
pub type Asset {
label: String,
}
// ANCHOR: carries_data
// 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,
}
// ANCHOR_END: carries_data
// ANCHOR: cardinality
// 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..*];
// ANCHOR_END: cardinality
// ANCHOR: nary
// 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);
// ANCHOR_END: nary
// ANCHOR: specialize
// Relations specialize just like concepts: every Internship is an Employment.
pub rel Internship(employee: Person, employer: Org) <: Employment;
// ANCHOR_END: specialize
// ANCHOR: derive
// 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);
// ANCHOR_END: derive
// Ground facts: two people employed by the same org.
pub fact Person(alice);
pub fact Person(bob);
pub fact Org(acme);
pub fact Employment(alice, acme);
pub fact Employment(bob, acme);
pub query colleague_pairs() -> colleagues;