Properties and Relations
Concepts on their own describe a vocabulary. To say what holds between things in that vocabulary, you need properties (data attached to a concept) and relations (links between concepts).
In Argon, properties are concept fields — we have already seen them. Relations are a separate item kind, with two surface forms. This chapter teaches both, plus multi-valued fields, the relator pattern, and refinement-typed fields. The relator pattern uses UFO’s relator metatype, declared in UFO’s prelude with the substrate forms from Chapter 2.1.
Single-valued fields
We have written these. The shape is familiar:
pub kind Person {
id: String,
name: String,
}
Each field declares a name and a type. Types can be primitives (Nat, Int, Real, Decimal, String, Bool, Date, DateTime, Duration, Money — all in std::math) or other concepts (Tenant, Lease, Property).
A field of concept type holds a reference to an instance — Argon handles the indirection automatically. There is no notion of borrowing or ownership; fields read like values.
Multi-valued fields
Sometimes a concept needs to hold more than one of something — a property with several rooms, a lease with several tenants. Use the collection-type syntax [T]:
pub kind Property {
id: String,
address: String,
rooms: [Room],
}
rooms: [Room] reads “an unordered collection of Room.” Argon does not commit you to a list-versus-set semantic at the surface; the runtime stores collections without duplicates by identity.
Cardinality constraints
A bare [T] admits any count, including zero. To pin the count, add a cardinality clause:
pub relator Lease {
tenants: [Tenant; >= 1],
landlord: Landlord,
property: Property,
start_date: Date,
end_date: Date,
monthly_rent: Money,
}
The >= 1 constraint says “at least one tenant.” Argon admits three operators in this position:
| Operator | Meaning |
|---|---|
>= N | At least N |
== N | Exactly N |
<= N | At most N |
There is no < or >. If you find yourself wanting strict inequality, use >= N + 1 or <= N - 1 and pre-compute the bound, or model the constraint as a refinement type instead (see Chapter 2.6).
Tip: put the cardinality constraint on the field, not in a separate assert. The compiler can use the field-level cardinality during type-checking and reasoning; an assert that re-states the same constraint is redundant and will be flagged as such.
Examples
pub kind Square {
sides: [LineSegment; == 4],
}
pub kind Committee {
chair: Person,
members: [Person; >= 3],
}
pub kind OptionalAddress {
line_one: String,
line_two: [String; <= 1],
}
Relations
Argon has a dedicated item kind, rel, for binary relations whose presence at the call site you want to be explicit. There are two shapes.
Binary form
pub rel HasResidence: Person -> Residence
A binary relation between two concepts. The arrow is ->. The relation name reads as a predicate at rule positions:
pub derive ActiveResident(p: Person, r: Residence) :-
HasResidence(p, r),
r.is_occupied
Binary relations can also carry attributes via a body:
pub rel Owns: Person -> Asset {
acquired_on: Date,
purchase_price: Money,
}
Treat the attached attributes as fields on the relation instance.
A worked example with decorator characteristics, from argon/examples/pizza/src/prelude.ar (the Manchester pizza ontology, ported faithfully):
// Transitive ingredient inclusion: every Margherita is a Pizza,
// every Pizza is a Food, every Food has ingredients, transitively.
@[transitive]
pub rel hasIngredient: Food -> Food
// Functional + asymmetric base: every Pizza has exactly one PizzaBase,
// and a base never "has" a pizza.
@[functional]
@[asymmetric]
pub rel hasBase: Pizza -> PizzaBase
// Functional spiciness: a topping has at most one Spiciness value.
@[functional]
pub rel hasSpiciness: PizzaTopping -> Spiciness
The pizza workspace exercises five relation characteristics — @[transitive], @[symmetric], @[asymmetric], @[functional], @[inverse-functional] — across class lattices with multiple inheritance. Read it end-to-end as a 200-line tour of relation-shape modeling without a foundational-ontology dependency. The argon/examples/wine/, argon/examples/skos/, and argon/examples/foaf/ workspaces show the same relation patterns at different domains.
Reified form
When a relation is rich enough that “binary plus attributes” feels strained — for instance, when the relation has its own subtypes, or participates in further relations — promote it to a concept of its own:
pub relator ResidentialLease : Lease {
bedrooms: Int,
}
This is precisely the relator pattern from Chapter 2.2. The relator is a concept; its participants are fields on it; its identity is the tuple of its participants over a time interval.
The choice between rel and relator is mostly pragmatic: use rel when the relation is genuinely binary and lightweight; use a relator concept when it has substantial structure of its own. The lease in our running example is a relator because it carries dates, rent, subtypes, a lifecycle, and so on.
Composition
Binary relations can declare a composition chain over other roles:
pub rel composes_to: Agent -> Resource = role1.role2
The = a.b clause says composes_to is the composition of a and b — a derived relation, not a primitive one. We use this rarely in the tutorial; it is most useful when integrating with a foundational ontology that ships role compositions.
Inverses
If you want both directions of a relation explicitly, declare them both:
pub rel TenantOf: Tenant -> Lease
pub rel HasTenant: Lease -> Tenant
A derive rule can keep them synchronized:
pub derive HasTenant(l: Lease, t: Tenant) :- TenantOf(t, l)
Argon does not have a built-in inverse keyword. The pattern above is the idiom; it is explicit, which means the type-checker and reasoner both see the bidirectional structure.
Refinement types and field invariants
Argon admits refinement types — sub-types carved by a predicate over the metatype calculus. They live on a concept-decl’s where { ... } clause and are used at field positions like any other type:
pub subkind RigidLease : Lease where { A.rigidity == rigid }
pub kind LeaseAccount {
lease: RigidLease,
balance: Money,
}
The body of a refinement type is a conjunction of axis-predicates — A.rigidity == rigid, A.sortality == sortal, etc. — over the meta-property calculus, not instance-level field comparisons. The compiler proves this fragment decidable; Chapter 2.6 covers the surface in full.
Instance-level invariants — “monthly rent must be positive,” “grace days must be between zero and fourteen” — live on assert items in Chapter 2.4, not on field declarations. The two mechanisms compose: a refinement type narrows by metatype; an assert narrows by instance value.
Note: the refinement fragment is decidable in PTIME with respect to the size of the concept hierarchy and axis count. Membership uses Kleene’s strong three-valued semantics () under the open-world assumption with the Pietz-Rivieccio “Exactly True” designation:
unknowndoes not satisfy refinement membership; onlytruedoes. We will revisit the OWA implications in Chapter 2.6.
The Top type
Sometimes a field can hold “anything.” Argon’s top type is ⊤ (the Unicode character; the ASCII alias Top parses too):
pub kind Annotation {
target: ⊤,
note: String,
}
A field of type ⊤ accepts any concept instance. The trade-off is the obvious one: refinement, dispatch, and reasoning on the field are limited because the compiler does not know what to assume about it. Use ⊤ sparingly — for genuinely heterogeneous data like comment annotations or metadata blobs.
Putting it in the running example
Update src/lease.ar to use cardinality and refinement:
use metatypes::*
use party::*
pub kind Property {
id: String,
address: String,
}
pub relator Lease {
id: String,
tenant: Tenant,
landlord: Landlord,
property: Property,
start_date: Date,
end_date: Date,
monthly_rent: Money,
}
pub subkind ResidentialLease : Lease {
bedrooms: Int,
}
pub subkind CommercialLease : Lease {
permitted_use: String,
}
For the tutorial we keep the lease single-tenanted (one Tenant, not [Tenant; >= 1]); a richer model would relax that. The refinement on monthly_rent will land in Chapter 2.6, where we introduce the dedicated refinement-type item.
$ ox check
Checking lease-tutorial v0.1.0
Finished in 0.10s
The model still compiles. We have a small but well-typed structural skeleton. Next chapter: rules.
Edge cases worth knowing
- Cardinality versus comparison. The
>=/==/<=inside[T; …]are cardinality operators, separate from value-comparison operators of the same spelling. The compiler tracks them in different enums precisely so this distinction is visible. You will not accidentally cross them. - Underscore separators in counts.
[String; >= 1_000_000]works; underscores are stripped before the integer parses. - Multi-supertype fields. A field whose type is
LawyerTenant(a concept with multiple supertypes) carries the closure of all its supertypes’ fields. The compiler enforces field-name uniqueness across the closure; if two parents declare a field with the same name and different types, you get a type error and must either rename or override. - Cyclic field types are fine.
Person { friends: [Person] }works. The compiler builds a graph, not a tree. - No anonymous concepts in field positions. A field of type
{ name: String }(an inline record) is not admitted; declare a named concept instead.
Summary
Fields attach data to concepts; multi-valued fields use [T] with optional cardinality clauses; relations come in binary (rel name: A -> B) and reified (relator) shapes; refinement-typed fields sharpen the field’s type with a boolean predicate. The lease tutorial now has properties and relations enough to model a typical residential agreement. What it lacks is rules: the engine that turns this static structure into derived facts. That is the next chapter.