Skip to content

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 required blank 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 no required flag is the same thing as a field with required and 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:

  1. Fulfill it. Set a value. The blank is no longer blank.
  2. 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:

GoalSyntax
Refine to a subtype kind, keep blankrequired <subtype-kind> Name
Refine to a subtype kind, fulfill it<subtype-kind> Name { ... }
Switch to a non-subtype kind, keep blankoverride required <new-kind> Name
Switch to a non-subtype kind, fulfill itoverride <new-kind> Name { ... }
Fulfill, keeping the inherited kindName: value or Name { ... }
Remove entirelydrop 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 database
type service transactionalService {
required relational_db PrimaryStore
}
// 2. Refine to subtype kind, fulfill
type service fulfilledRelational {
relational_db PrimaryStore { command Read }
}
// 3. Switch to non-subtype kind, keep blank
type service cacheStillBlank {
override required cache PrimaryStore
}
// 4. Switch to non-subtype kind, fulfill
type service cacheFulfilled {
override cache PrimaryStore { command Lookup }
}
// 5. Fulfill keeping inherited kind
type service ordinary {
database PrimaryStore { command Read; command Write }
}
// 6. Remove entirely
type 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 team is not fulfilled and not dropped” — you forgot to fill or drop a blank.
  • “Cannot use required together with content”required component logs { command Send } is the contradiction.
  • “Cannot drop field x: not inherited” — you can only drop things that exist in scope.

Each one points at the source location and suggests the fix.

Summary

  • A required blank 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.