17. Required Blanks
A required blank is the language’s way of saying “this slot exists and must be filled before the model is valid.” It is how a type author tells every downstream instance: you need to decide this; I cannot decide it for you.
type module service { required cascade team}That type declares a team field. There is no default. Every service instance must either supply a value or explicitly drop the field. Anything else is a validation error.
This chapter is about the precise semantics of required and the only two ways an instance (or a downstream subtype) can address it.
What a blank actually is
The mental model that gets people in trouble: thinking of required as a marker that can be added or removed. It can’t.
Mindset shift. A
requiredblank is not a separable marker. It is the state of having no value. There is no “downgrade required to optional without filling it” operation, because that operation has no meaning — a declared field with no value and norequiredflag is the same thing as a field withrequiredand no value, which is the same thing as no field at all (at the type level).
So when you read required team, don’t read “the field team has a required flag attached.” Read “the field team exists with no value, and that’s not allowed in instances.” The flag and the blankness are the same fact.
The consequence: there are only two ways to deal with a required blank:
- Fulfill it. Set a value. The blank is no longer blank.
- Drop it. Remove the entire declaration. There is no blank because there is no field.
That’s it. No third option exists.
Fulfilling
The most common case. Provide a value:
type module service { required cascade team}
service Payments { team: Payments // fulfills the blank}Or provide content for a blank sub-declaration:
type module service { required database PrimaryStore}
service Orders { database PrimaryStore { // fulfills the blank with content command Read command Write }}After fulfillment, descendants and downstream subtypes no longer face the blank. It’s been answered. If you wanted them to also answer something, the parent type should have left a different blank.
Dropping
If you genuinely don’t have, and won’t have, a value for a required field — drop the declaration entirely:
type module service { required database PrimaryStore required cascade team}
service StatelessRouter { team: Platform drop PrimaryStore // explicit acknowledgement: this service has no store}Dropping is loud on purpose. It forces the author to write drop PrimaryStore rather than silently omitting the field. The next reviewer sees the drop and can ask “why doesn’t this service have a primary store?” — which is a useful question. Silent omission would hide it.
The forbidden third option
What you can’t do:
// In a type body:type module service { required cascade team}
// In an instance:service Bad { // Idea: "I want the team field to exist but not be required and not be set." // There's no syntax for this. There's no concept for this. The thing // you're describing isn't a thing.}If you reach for “downgrade to optional without filling,” step back and ask: what would the field’s value be? If the answer is “nothing yet, but it should exist,” you have a blank — fulfill it later. If the answer is “this service really doesn’t have one,” drop it.
What required can apply to
Every part of a type body can be required:
type module service { // Required field required cascade team
// Required label required labels.domain
// Required sub-declaration (interface) required event Heartbeat
// Required sub-declaration (sub-module) required database PrimaryStore}Each one demands its own fulfill-or-drop decision from the instance.
For nested labels, the dotted form and the block form are equivalent:
type module service { required labels.domain // is the same as: labels { required domain }}Move-set against an inherited blank
When a subtype or instance inherits a required blank, there are exactly six things it can do:
| Goal | Syntax |
|---|---|
| Refine to a subtype kind, keep blank | required <subtype-kind> Name |
| Refine to a subtype kind, fulfill it | <subtype-kind> Name { ... } |
| Switch to a non-subtype kind, keep blank | override required <new-kind> Name |
| Switch to a non-subtype kind, fulfill it | override <new-kind> Name { ... } |
| Fulfill, keeping the inherited kind | Name: value or Name { ... } |
| Remove entirely | drop Name |
override is the subject of Chapter 19. For now: it’s the keyword for changing an inherited declaration to a kind that isn’t a subtype of the original.
All six moves on one blank:
type module service { required database PrimaryStore}
// 1. Refine to subtype kind, keep blank — relational_db extends databasetype service transactionalService { required relational_db PrimaryStore}
// 2. Refine to subtype kind, fulfilltype service fulfilledRelational { relational_db PrimaryStore { command Read }}
// 3. Switch to non-subtype kind, keep blanktype service cacheStillBlank { override required cache PrimaryStore}
// 4. Switch to non-subtype kind, fulfilltype service cacheFulfilled { override cache PrimaryStore { command Lookup }}
// 5. Fulfill keeping inherited kindtype service ordinary { database PrimaryStore { command Read; command Write }}
// 6. Remove entirelytype service stateless { drop PrimaryStore}Every legal move against a required blank is one of these six. Anything else is an error.
Subtypes can add new blanks
A subtype can add required blanks of its own, asking its instances (and any further subtypes) for more information:
type module service { required cascade team}
type service payments_service { team: "Payments" // fulfills parent's blank required ext.processor_vendor // adds a new blank}A payments_service instance no longer faces team (the type fulfilled it) but does face ext.processor_vendor. The instance must fulfill or drop it.
This is how a metamodel evolves: a generic type provides the universal requirements; subtypes layer domain-specific requirements on top.
Cascade behavior of required blanks
The cascade modifier is part of how the field is declared at the type level. You can’t change it in a subtype or instance — the propagation mode is fixed at the type that introduces the field.
type module service { required cascade team // cascades through nested modules}
service Payments { team: Payments
component MetricsExporter { // No 'team' set here; the cascade means resolved team is "Payments" }}Chapter 18 is the deep dive on cascade.
Errors you’ll see
The validator produces specific diagnostics:
- “Required field
teamis not fulfilled and not dropped” — you forgot to fill or drop a blank. - “Cannot use
requiredtogether with content” —required component logs { command Send }is the contradiction. - “Cannot
dropfieldx: not inherited” — you can only drop things that exist in scope.
Each one points at the source location and suggests the fix.
Summary
- A
requiredblank is the absence of a value, not a separable marker. - Two ways to address it: fulfill (set a value or content) or drop (remove the declaration).
- “Downgrade required without filling” is not an operation; the state it would create has no meaning.
- The six-move table covers every legal action against an inherited required blank.
- Subtypes can add their own required blanks for further specialization.
- Cascade behavior is fixed at the type that introduces the field; subtypes can’t change it.
What’s next
Chapter 18: Propagation → — the longest chapter in the book, and the mindset shift that makes the metamodel cohere: the two compose-able propagation mechanisms.