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 version}That type declares a version 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
Section titled “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 version, don’t read “the field version has a required flag attached.” Read “the field version 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
Section titled “Fulfilling”The most common case. Provide a value:
type module service { required cascade version}
service Payments { version: "2.1" // 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 db_read read db_write 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
Section titled “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 version}
service StatelessRouter { version: "1.0" 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
Section titled “The forbidden third option”What you can’t do:
// In a type body:type module service { required cascade version}
// In an instance:service Bad { // Idea: "I want the version 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
Section titled “What required can apply to”Every part of a type body can be required:
type module service { // Required field required cascade version
// Required aspect required aspect domain
// Required sub-declaration (interface) required kafka heartbeat
// Required sub-declaration (sub-module) required database PrimaryStore}Each one demands its own fulfill-or-drop decision from the instance.
For nested aspects, the dotted form and the block form are equivalent:
type module service { required aspect domain // is the same as: aspect { required domain }}Move-set against an inherited blank
Section titled “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 type, keep blank | required <subtype-type> Name |
| Refine to a subtype type, fulfill it | <subtype-type> Name { ... } |
| Switch to a non-subtype type, keep blank | override required <new-type> Name |
| Switch to a non-subtype type, fulfill it | override <new-type> Name { ... } |
| Fulfill, keeping the inherited type | 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 type that isn’t a subtype of the original.
All six moves on one blank:
type module service { required database PrimaryStore}
// 1. Refine to subtype type, keep blank — relational_db extends databasetype service transactional_service { required relational_db PrimaryStore}
// 2. Refine to subtype type, fulfilltype service fulfilled_relational { relational_db PrimaryStore { db_read read }}
// 3. Switch to non-subtype type, keep blanktype service cache_still_blank { override required cache PrimaryStore}
// 4. Switch to non-subtype type, fulfilltype service cache_fulfilled { override cache PrimaryStore { db_read lookup }}
// 5. Fulfill keeping inherited typetype service ordinary { database PrimaryStore { db_read read; db_write 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
Section titled “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 version}
type service payments_service { version: "2.1" // fulfills parent's blank required ext.processor_vendor // adds a new blank}A payments_service instance no longer faces version (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
Section titled “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 version // cascades through nested modules}
service Payments { version: "2.1"
component MetricsExporter { // No 'version' set here; the cascade means resolved version is "2.1" }}Chapter 18 is the deep dive on cascade.
Errors you’ll see
Section titled “Errors you’ll see”The validator produces specific diagnostics:
- “Required field
versionis not fulfilled and not dropped” — you forgot to fill or drop a blank. - “Cannot use
requiredtogether with content” —required component logs { rest_create 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
Section titled “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
Section titled “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.