Enums
An enum declaration introduces a type whose values are a finite, named set of tags. Use it when you need a type-level enumeration of mutually-exclusive cases — a status, a mode, a severity, a response category — that are not themselves concepts in your ontology and don’t carry their own data.
Enums sit alongside the other type-introducing forms in Chapter 2.6: like primitives and concepts, an enum is a thing a field can hold. Unlike concepts, enums do not participate in the metatype calculus, do not specialise other types, and do not anchor relations. They are deliberately small.
Declaring an enum
Top-level form:
enum Severity {
Info,
Warning,
Error,
}
Variants are bare identifiers separated by commas. A trailing comma is permitted. A pub keyword exports the enum from its module:
pub enum LeaseStatus {
Pending,
Active,
Terminated,
}
Variants may carry nested sub-variants using a brace-enclosed body. Nesting expresses a tagged hierarchy on top of the outer enumeration:
pub enum HttpResponse {
Ok,
Error {
NotFound,
ServerError,
Timeout,
},
}
Argon enums do not carry payload data on their variants. A variant is a name; if you need a name plus associated structure, the right form is a concept hierarchy (covered below).
Doc comments attach to the enum and to each variant:
/// The lifecycle of a residential lease.
pub enum LeaseStatus {
/// The contract has been signed but the term has not begun.
Pending,
/// The term is in effect.
Active,
/// The term has ended; deposits and reconciliations remain.
Terminated,
}
Using an enum value
An enum is a type. It can appear anywhere a type can — as a field type on a concept, as a parameter type on a compute, as a filler in a relation’s range:
use std::math::String
pub enum LeaseStatus { Pending, Active, Terminated }
pub kind Lease {
id: String,
state: LeaseStatus,
}
Variants are referenced by name. When the enum is unambiguously in scope, the bare name resolves:
test "newly-signed lease starts pending" {
let l: Lease = {
id: "lease-001",
state: Pending,
}
}
When two enums in scope expose a same-named variant, qualify with the enum’s name and a dot:
use std::math::String
pub enum LeaseStatus { Active, Inactive }
pub enum AccountStatus { Active, Frozen }
pub kind Account {
id: String,
lease_state: LeaseStatus,
acct_state: AccountStatus,
}
test "fully qualified variants" {
let a: Account = {
id: "a-001",
lease_state: LeaseStatus.Active,
acct_state: AccountStatus.Active,
}
}
The qualified form is <EnumName>.<VariantName>. Nested variants extend the path: a HttpResponse.Error.NotFound expression names the deepest tag.
When to reach for an enum
Argon offers three forms that all enumerate something. They serve distinct purposes; mixing them up produces models that are harder to reason about than they need to be.
| Form | Use when |
|---|---|
enum E { A, B, C } | The cases are tags. They don’t have their own roles, fields, or relations. You’ll branch on them in a compute or read them out of a field. |
| Concept hierarchy (subtypes of a parent metatype) | Each case is a concept that lives in the lattice. It can specialise further, anchor relations, refine its members, fire rules, and participate in structural reasoning. (Covered in Chapter 2.2.) |
Typed-domain metaxis | The values feed the metatype calculus — they’re values of a meta-axis bound to a primitive type, not values of a domain field. (Covered in Chapter 2.6.) |
A useful test: can a variant own a relation? If yes, it’s a concept and you want a hierarchy of subtypes. If no, it’s a tag and you want an enum.
// Tag: the response category doesn't have its own structure.
pub enum HttpStatus { Ok, Created, NotFound, ServerError }
// Concept: each lease phase carries its own roles, rules,
// and may refine further — each phase is a concept in the lattice.
pub kind Lease {}
pub phase SignedPending : Lease {}
pub phase Active : Lease {}
pub phase Terminating : Lease {}
pub phase Settled : Lease {}
Edge cases worth knowing
Empty enums are accepted. pub enum Marker {} elaborates today. There is no diagnostic for it. Empty enums are rarely useful and a future cut may flag them; don’t rely on the silence as endorsement.
Variant-validity at assignment time is not yet checked. A field declared as state: LeaseStatus will currently accept any identifier in its position — including identifiers that aren’t variants of LeaseStatus. The check is queued for a future cut; today the elaborator records the variant reference but does not yet emit a diagnostic when the reference doesn’t resolve to one of the enum’s declared variants. Treat the type annotation as documentation until the check lands.
Duplicate variant names within one enum elaborate today. A future cut will reject them; in the meantime keep variants unique by hand.
Cross-package referencing. A pub enum in package A is reachable from package B via the standard use path:
use lease_legal::LeaseStatus
Bare-name resolution applies once the use is in scope; otherwise qualify with the package-prefixed path.
Enums and the metatype calculus. Enum types are not metatypes. They cannot appear in metaxis declarations as the carrier of an axis (use a typed metaxis : <Primitive> for that), and they do not specialise via <:. They are leaves in the type system, not nodes in the lattice.
Putting it in the running example
The running lease model in Chapter 2.2 treats each lifecycle phase — SignedPending, Active, Terminating, Settled — as a concept in its own right, declared as phase subtypes of Lease. That is the right call when each phase carries its own roles, refinements, and per-phase rules. Phases are concepts.
A small enum sits alongside the phase hierarchy for an orthogonal concern — the disposition of an emitted notice:
use std::math::String
pub enum NoticeDisposition {
Pending,
Acknowledged,
Disputed,
}
pub kind RentNotice {
notice_id: String,
disposition: NoticeDisposition,
}
The disposition is a tag attached to the notice. It doesn’t itself anchor roles or rules — those live on RentNotice — and a Disputed notice doesn’t structurally specialise into a sub-concept the lattice cares about. Enum is the lighter form, and it’s the right one here.
Worked examples in the codebase
Two of the foundational example workspaces use enums idiomatically:
argon/examples/pizza/src/prelude.ardeclarespub enum Spiciness { Hot, Medium, Mild }and attaches it toPizzaToppingviahasSpiciness: PizzaTopping -> Spiciness. The variants are tags — aHottopping does not own its own relations, fields, or further sub-spicinesses; the enum is the right form.argon/examples/time/src/prelude.ardeclarespub enum DayOfWeek { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }andpub enum MonthOfYear { January, …, December }. These are textbook enums: the values are bare tags drawn from a finite, ordered set, and OWL-Time uses them indayOfWeekandmonthOfYeardatatype properties without anchoring relations on the variants.
If you want a contrast — a case where what looks like an enum should actually be a concept hierarchy — read argon/examples/pizza/src/prelude.ar’s PizzaTopping lattice. There are 30+ tagged topping concepts (Mozzarella, Pepperoni, JalapenoPepperTopping, …), each capable of further specialisation, each anchoring hasSpiciness and hasCountryOfOrigin relations. Concept hierarchy is right; enum would be wrong.
Summary
[pub] enum Name { Variant, ... }introduces a type whose values are a finite set of named tags.- Variants are bare identifiers; nested variants extend the path (
Outer.Inner.Leaf). - Doc comments attach to the enum and to each variant.
- An enum is a type — fields and parameters can hold its values; variants are referenced bare or
EnumName.Variant. - Reach for
enumwhen the cases are tags. Reach for a concept hierarchy when the cases are themselves ontology objects with structure. Reach for a typedmetaxiswhen the values drive the metatype calculus.