16. Defining Kinds
You’ve been consuming kinds from the stdlib since Chapter 4. Now you write your own. This chapter introduces three: a custom module kind, a custom facet kind, and a custom interface kind. Each one shows a different shape of type body.
A custom module kind
Suppose your team has a notion of an internal service — a service that’s only callable from inside your VPC, owned by your team, and tagged security.zone: Internal. Every instance is a service with those three things baked in.
The naive way: copy-paste the three lines into every internal service. The better way: define a kind.
type module internal_service { required cascade team labels { security.zone: Internal }}Now an instance:
internal_service Inventory { team: Commerce "Tracks stock counts in real time."
command CheckAvailability command Reserve}Inventory gets a security.zone: Internal label automatically. It still has to fill in team because the type marked it required — that’s the next chapter.
Note that internal_service is declared with module as its parent kind. That makes it a generic module subtype — you could also subtype service if you wanted the visual treatment of a service plus the constraints of internal_service:
type service internal_service { required cascade team labels { security.zone: Internal }}Now internal_service instances render with the service widget by default (inherited from the stdlib service type), in addition to having the labels and required team.
The shape of a type declaration
type <stable_id?> <parent_kind> <name> { <body> }<stable_id?>— optional#xyzslot. Formatter mints on save.<parent_kind>— the type this one extends. Any registered kind.module,facet,interfaceare the three base kinds; everything else is a user-defined subtype somewhere up the chain.<name>— the name your instances will use.<body>— defaults, blanks, sub-declarations, and modifiers.
There’s no extends keyword. The parent is encoded by position. type service paymentsService { ... } means “paymentsService extends service.”
A custom facet kind
A facet for “an HTTP resource”:
type facet resource { "A facet representing an HTTP resource. Path composes via append." append base}append base says: instances of resource carry a base field that composes with descendants’ values (a parent’s /orders and a child’s /items resolve to /orders/items). The append modifier is the subject of Chapter 18 — for now, take it as “the way path components stack.”
Used as:
service Orders { resource OrdersResource { base: "/orders"
command Post query Get
resource Items { base: "/items" // appends to /orders → /orders/items query List } }}A custom interface kind
For a webhook interface kind that takes a default protocol: webhook field:
type interface webhook { protocol: webhook "An HTTP webhook callback delivered by an external system."}Used as:
service OrderWebhooks { webhook OrderCreated webhook OrderCancelled}Every webhook instance inherits protocol: webhook. The instance can override or drop it.
What a type body can contain
Every shape we’ve already met as a body member can appear in a type body, with the addition of required markers and the cascade / append modifiers on fields:
| In a type body | Meaning |
|---|---|
field: value | Default value for instances |
required field | Mandatory blank — instance must fill |
cascade field: value | Default value that cascades to descendants |
append field | Field that composes with descendants’ values |
required cascade field | Mandatory blank that, once filled, cascades |
labels { x: y } | Default label values |
required labels.x | Mandatory blank label |
command X { ... } | Pre-filled interface — instance inherits |
required command X | Mandatory blank interface — instance must refine |
component Y { ... } | Pre-filled sub-module |
required component Y | Mandatory blank sub-module |
The combinations of required, cascade, and append are the language for designing your metamodel.
Mistake to avoid.
required component logs { command Send }is a contradiction.requiredmeans “no value”; the brace block means “here’s a value.” The validator rejects this. Either mark itrequired(blank, to be filled) or provide content (norequired).
Where types go
Types live in .arch files. Convention: keep them in a file named kinds.arch at the package root for visibility.
acme.shop/├── package.archspace├── kinds.arch # type module internal_service { ... }├── payments.arch # internal_service Payments { ... }└── orders.arch # internal_service Orders { ... }To make a type visible to other packages, mark it export:
export type module internal_service { required cascade team labels { security.zone: Internal }}Without export, the type is internal to its package. Importers use the use mechanism from Chapter 12.
What can’t be a parent kind
Two restrictions:
processandviewcan’t be parent kinds. Their bodies aren’t stampable templates (they’re sequences and projections respectively), so subtyping them has no useful semantics yet. Reserved for future iterations.- Reserved keywords can’t be kind names. You can’t
type module process { ... }becauseprocessis a keyword. Appendix B: Keywords lists the full set.
A worked metamodel slice
Here’s a small set of related types defining a payments-domain vocabulary:
// A service that operates in PCI scope.export type service pci_service { required cascade team labels { security.zone: PCI } required ext.runbook_url}
// A service that processes external card transactions.export type pci_service card_processor { required ext.processor_vendor required ext.api_docs_url "A card processor — talks to an external vendor and is in PCI scope."}
// An interface for a callback delivered by an external system.export type interface webhook { protocol: webhook "An HTTP webhook callback."}Then in instances:
pci_service Authorize { team: Payments ext.runbook_url: "https://wiki/auth-runbook"
command Authorize}
card_processor StripeIntegration { team: Payments ext.runbook_url: "https://wiki/stripe-runbook" ext.processor_vendor: "Stripe" ext.api_docs_url: "https://stripe.com/docs"
command Charge webhook PaymentSucceeded webhook PaymentFailed}The metamodel encodes domain knowledge: every PCI service has a runbook URL; every card processor names its vendor and API docs. The validator enforces these requirements at parse time. The vocabulary (pci_service, card_processor, webhook) reads naturally in source.
Summary
- A type extends a parent kind by position:
type <parent> <name>. Noextendskeyword. - Type bodies contain defaults, mandatory blanks, pre-filled sub-declarations, and propagation modifiers.
module,facet,interfaceare the three base kinds; everything else is a subtype.processandviewcan’t be parent kinds (yet).- Export types with
export type ...to make them visible to importing packages. - Convention: keep types in
kinds.archat the package root.
What’s next
Chapter 17: Required Blanks → — the mandatory-decision mechanism that makes types more than syntactic sugar.