Connecting concepts and relations
A relation is first-class, but where it is declared expresses its semantic association with the participating concepts. Three placements coexist; the modeler picks by intent:
- Module level — a free predicate owned by no single endpoint:
pub rel ConnectedTo(a: Node, b: Node) [0..*] [0..*];, accessed asConnectedTo(x, y)anywhere in the module. - Topic
mod— grouped with related concepts; accessed asfamily::ParentOf(a, b)afteruse family::*;. impl Type(impl Type { … }— grouping) — conceptually “about” the type, alongside its methods; accessed asPerson::ParentOf(a, b)only, not at module level.
Navigation from a concept value to its participations. Three mechanisms link an instance to the relations it participates in.
1. Field-form navigation view (cheapest, declarative):
pub type Person {
parents: [Person] from ParentOf.parent, // alice.parents
children: [Person] from ParentOf.child, // alice.children
}
Each field is a derived view over the relation. The compiler verifies the field type matches the role’s endpoint type and that cardinality is consistent with the relation’s per-endpoint cardinality. The view materializes against the relation’s closed extent: because relation subsumption (Relations) makes a subsumed relation’s tuples tuples of its supersuming relation, a view over Rel.endpoint includes the endpoints contributed by every <:-subsumed relation — so a subtype that refines an inherited collection by projecting from a subsumed relation (SatisfactionAccount.records from satisfactionAccountRecords <: recordAccountRecords) sees exactly its narrower elements.
2. UFCS method via impl (richer navigation):
impl Person {
pub query siblings(self) -> [Person] {
select s from ParentOf(p, self), ParentOf(p, s) where s != self
}
}
Methods are appropriate when navigation requires filtering, joining, ordering, or aggregation beyond a simple projection. Accessed as alice.siblings().
3. Direct rule-body invocation (always available):
pub derive grandparent(g: Person, c: Person) :- ParentOf(g, p), ParentOf(p, c);
Inside any rule body, the relation is a predicate addressable by name (or by qualified path: family::ParentOf(...), Person::ParentOf(...)).
Inverse declaration (when both sides are field-form):
pub type Country { citizens: [Citizen] }
pub type Citizen {
#[inverse(Country.citizens)]
countries: [Country],
}
The compiler verifies cardinality consistency and exposes both navigation directions over a single synthesized relation.
Naming convention. Relation names are CamelCase (per Identifiers) — ParentOf, Marriage, IsCitizenOf — symmetric with the CamelCase of metatype-introduced concepts. The name plus argument order encodes semantic direction; ParentOf(p, c) reads “p is the parent of c.” The convention is the modeler’s responsibility — the language does not enforce it. Use field-form views or methods to expose semantically-named navigation in the opposite direction.