Concepts and Hierarchies
A concept is the unit of meaning in an Argon ontology. Concepts are the things in your domain — Person, Lease, Property. They have names, fields, and supertypes. Most everything else in the language hangs off them.
This chapter teaches concept declarations from one-line forms up to multi-level hierarchies, and starts the lease tutorial in earnest. The metatype keywords used below — kind, subkind, role, phase, relator — come from the UFO package, declared via the substrate forms covered in Chapter 2.1; they are not language built-ins.
The smallest concept
pub kind Person {
id: String,
name: String,
}
Three things to notice.
First, kind is not a keyword — it’s an identifier that resolves to an imported metatype declaration. We met that mechanism in Chapter 1.2. For the rest of this chapter we will assume metatypes::* is in scope, which is the case in our running tutorial because prelude.ar re-exports it.
Second, the body is a comma-separated list of name: Type field declarations. Trailing comma is allowed; use it.
Third, pub is opt-in. Without it, Person is visible only inside the file. With it, Person is part of the package’s exported surface (assuming prelude.ar re-exports the module, as ours does).
Subtypes
A concept declares a supertype with <: (the specializes operator), or : as a documented sugar:
pub subkind ResidentialLease <: Lease {
bedrooms: Int,
}
Read in English: ResidentialLease specializes Lease. It inherits all of Lease’s fields and adds one more, bedrooms. The typeset alias ⊑ is also accepted (pub subkind ResidentialLease ⊑ Lease) — useful when rendering ontology source for academic publication.
Sugar:
:in supertype slots. Argon also accepts:here:pub subkind ResidentialLease : Lease { bedrooms: Int }The two forms produce identical compilation. Why both? Argon’s two primitive relations are
instantiates(:at the atom level —alice : Tenant) andspecializes(<:at the atom level —Tenant <: Person). In a supertype slot the only coherent reading isspecializes(you cannot instantiate a type from a type at the same metalevel), so:is admitted as a sugar for the<:of that position. New code is encouraged to use<:for clarity; existing:-form code remains valid indefinitely.
The metatype you use carries semantic weight:
- A
kindis rigid (instances belong to it for as long as they exist) and sortal (it supplies its own identity criteria).Personis akind. - A
subkindis a proper subtype within a hierarchy that still supplies identity.ResidentialLeaseis asubkindofLease. - A
roleis anti-rigid — a thing plays it temporarily, not permanently.Tenantis arolesomeone plays for a while. - A
phasepartitions the lifecycle of its parent kind. We meet phases in Chapter 3.2. - A
relatoris a concept whose whole purpose is to mediate other concepts.Leaseis arelator— it connects aTenant, aLandlord, and aProperty. - A
categoryis rigid but non-sortal — a cross-cutting umbrella.LegalEntitymight be one.
The metatype of a concept is its kind in the type-theoretic sense: it tells you what kinds of statements about identity, persistence, and instantiation hold for instances. We get to the formal calculus in Chapter 5.1; here we use the metatypes idiomatically.
Migration note: the team is exploring moving sibling-disjointness — the rule that says distinct
subkindsiblings cannot share an instance — from explicitpartitionaxioms onto thesubkindmetatype itself, viaaxioms { siblings_disjoint }blocks in metatype declarations. Today’s code does not writepartitionaxioms in this book’s tutorial because we do not need them; the constraint will become implicit when the redesign lands. See Appendix D.
Multi-level hierarchies
Concepts compose through inheritance. The lease model has two levels:
pub kind Person {
id: String,
name: String,
}
pub role Tenant : Person {
contact_email: String,
}
pub role Landlord : Person {
payout_account: String,
}
A Tenant is a Person who plays the tenant role. A Landlord is a Person who plays the landlord role. Both inherit id and name; each adds its own role-specific fields.
In a richer foundational ontology you might add a layer in between — say, LegalSubject : Person and have Tenant : LegalSubject — but the tutorial keeps this flat. The tradeoff is the usual one: more layers, more semantic precision, more to maintain.
The relator pattern
Some concepts exist precisely to connect other concepts. The lease itself is the canonical example:
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,
}
A Lease does not exist independently of the parties it connects. Its identity comes from the Tenant, Landlord, and Property it relates, plus the time interval. The relator metatype marks this kind of “this-is-a-connection” concept distinctly from kind, which marks an independent existent.
You will recognise the pattern from foundational ontologies (UFO calls these relators; OntoUML uses the same name). Argon does not bake UFO into the language, but the pattern is naturally expressible because we have the metatype mechanism.
Note: the
Moneyfield type lives instd::mathalongside the other primitives. Ause std::math::Moneyat the top of the module brings it into scope; bareMoneywithout an import producesOE0101.
Specializing the relator
Just as Tenant : Person specializes Person, leases specialize:
pub subkind ResidentialLease : Lease {
bedrooms: Int,
}
pub subkind CommercialLease : Lease {
permitted_use: String,
}
A ResidentialLease is a Lease (so it carries all the lease fields plus its own). The two siblings — ResidentialLease and CommercialLease — are intended to be disjoint: a single lease is residential or commercial, not both. Today that disjointness is not enforced by the metatype itself, so a partition assertion would express it explicitly. The redesign captured in Appendix D folds the disjointness into subkind’s sibling-axiom, which is what the rest of the book treats as the default reading.
Variations
A few common shapes worth knowing.
Concept without a body
The body is optional. A concept with no fields declares a name and a metatype:
pub kind LegalEntity { }
Often you do this when the concept is a parent type whose instances always pick a more specific subtype.
Concept with multiple supertypes
: admits a comma-separated list:
pub kind LawyerTenant : Tenant, Lawyer { }
Use this when a concept is genuinely both — a thing that plays the tenant role and the lawyer role. The compiler builds the closure over the supertype set.
Concept body extension
You can add fields, derives, and asserts to an existing concept from a different module via extend:
extend Person {
email: String,
derive HasEmail(?p: Person) :- ?p.email != ""
}
extend blocks admit only fields, derives, and asserts — not mutations, queries, or computes. The intent is structural extension (data shape, derived facts, invariants), not behavioural extension. We will use extend sparingly in the tutorial; it is mostly useful when adding a small amount of structure to a concept defined in a dependency.
Cross-package extend is a common shape: a downstream package adds optional fields to an upstream concept without forking it. The _catalog/concept-extend/cross-package/ workspace ships a two-package example — base-ontology exports Person; contact-extension extends it with phone-number fields and a derive rule classifying which contacts are reachable. Variants: _catalog/concept-extend/{minimal,composition,cross-package,idiom,anti-pattern,negative-orphan,negative-bad-member}/.
The cross-package variant looks like this in source:
// In base-ontology/src/prelude.ar:
pub kind Person {
id: String,
name: String,
}
// In contact-extension/src/prelude.ar:
use base_ontology::*
extend Person {
primary_phone: String,
secondary_phones: [String; >= 0],
derive Reachable(?p: Person) :- ?p.primary_phone != ""
}
A downstream consumer that uses both packages sees a single Person concept with the union of declared fields and rules. The negative-orphan/ variant attempts to extend a Person the package does not have in scope — OE0805 OrphanExtend fires.
Edge cases worth knowing
- Trailing comma in field lists. Allowed everywhere a comma-separated list lives. Use it; it makes diffs cleaner.
pubis per-item, not per-module. Each concept declares its own visibility.- No anonymous concepts. Every concept has a name. Inline structural types like
{ name: String, age: Int }are not concept declarations — those are structural records, used only in narrow positions. - Field types can be other concepts.
tenant: Tenant(above) types the field as aTenantinstance. Argon handles the reference automatically; you do not write&TenantorBox<Tenant>. There is no notion of borrowing in the language.
Putting it in the running example
Two new files in the lease tutorial:
src/party.ar:
use metatypes::*
pub kind Person {
id: String,
name: String,
}
pub role Tenant : Person {
contact_email: String,
}
pub role Landlord : Person {
payout_account: String,
}
src/lease.ar:
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,
}
Update src/prelude.ar to re-export both:
pub use metatypes::*
pub use party::*
pub use lease::*
Then:
$ ox check
Checking lease-tutorial v0.1.0
Finished in 0.10s
The hierarchy compiles. We have a structural skeleton — concepts, supertype relations, fields. What we do not yet have is constraints over those structures (ranges, multiplicities, refinement predicates) or rules that derive new facts. Those come in the next two chapters.
Summary
Concepts are the primary unit of meaning in Argon. They have a name, a metatype, an optional supertype list, and a field block. Hierarchies form via the supertype relation. Different metatypes — kind, subkind, role, phase, relator, category — mark different ontological commitments. The lease tutorial now has a Person/Tenant/Landlord hierarchy and a Lease relator with two subtypes. Next we attach properties and relations to it.