Specialization and Redefinition
By now you have a complete coffee machine model: definitions, usages, ports, connections, actions, states, constraints, and requirements. Everything so far has been about building one machine. But what happens when you need a second kind of machine – one that shares most of the original design but changes a few things?
You could copy CoffeeMachine, rename it, and edit the copy. That works once. It becomes a maintenance problem the moment you fix a bug in the original and forget to propagate it to the copy. SysML v2 gives you better tools: specialization and redefinition. This chapter covers four related mechanisms for refining types without duplicating them.
Specialization
Specialization says: this new type is a kind of an existing type. It inherits everything from the general type and can add more.
The operator is :>.
package CoffeeMachineVariants {
import CoffeeMachineDomain::*;
part def EspressoMachine :> CoffeeMachine {
attribute pumpPressure : Real;
}
}
EspressoMachine :> CoffeeMachine means every EspressoMachine is a CoffeeMachine. It inherits all of CoffeeMachine’s features – the grinder, the brewer, the waterTank, the serialNumber. Then it adds pumpPressure, which only espresso machines have.
You have actually been using specialization implicitly since Chapter 2. When you write part def Grinder, it implicitly subclassifies the base library definition Parts::Part. When you write attribute waterTemp : Real, the attribute usage implicitly subsets the base library usage Attributes::attributeValues. Definitions get an implicit subclassification, usages an implicit subsetting, and writing your own explicit specialization suppresses the implicit one (SysML §7.6.8). The :> operator just makes the relationship explicit between your own types.
When to specialize
Specialize when the new type genuinely is a kind of the base type. Every instance of the specialized type should be substitutable wherever the general type is expected.
Good uses:
EspressoMachine :> CoffeeMachine– an espresso machine is a coffee machine.PressurizedBrewer :> Brewer– a pressurized brewer is a brewer.LargeWaterTank :> WaterTank– it is still a water tank, just bigger.
Bad uses:
Grinder :> CoffeeMachine– a grinder is not a coffee machine.BrewCycle :> CoffeeMachine– an action is not a structural type.
If the “is a kind of” sentence does not sound right in plain English, specialization is probably the wrong tool.
Multiple specialization
A definition can specialize more than one general type:
part def HeatedGrinder :> Grinder, HeatedComponent {
attribute grindTemp : Real;
}
HeatedGrinder inherits from both Grinder and HeatedComponent. This is useful when a concept belongs to two categories simultaneously. Use it sparingly – deep multiple-inheritance hierarchies become difficult to reason about.
Redefinition
Specialization adds new features. Redefinition replaces an inherited feature with a more specific version. The operator is :>>.
Suppose CoffeeMachine has a general target temperature attribute:
part def CoffeeMachine {
attribute targetTemp : Real;
// ... other features
}
An espresso machine needs that temperature locked to a specific value:
part def EspressoMachine :> CoffeeMachine {
attribute brewTemp :>> targetTemp = 94.0;
}
The line attribute brewTemp :>> targetTemp = 94.0 does three things at once:
- It says
brewTempredefines the inheritedtargetTemp. - It gives the feature a new name in this context:
brewTempinstead oftargetTemp. - It binds the value
94.0.
That third one is a binding, not a suggestion: a bare = asserts the value for every instance of the type. If you want a value a particular usage can still override, write default = 94.0 instead (SysML §7.6.3, and Chapter 5).
Inside an EspressoMachine, there is no separate targetTemp alongside brewTemp. The inherited feature is brewTemp. If you access targetTemp on an EspressoMachine instance, you get the same feature that brewTemp names.
Redefinition with tighter typing
You can also redefine a feature to narrow its type:
part def CoffeeMachine {
part brewer : Brewer;
}
part def EspressoMachine :> CoffeeMachine {
part espressoBrewer :>> brewer : PressurizedBrewer;
}
The inherited brewer is redefined to be typed by PressurizedBrewer (which itself specializes Brewer). The redefined type must be compatible with the original – you cannot redefine a Brewer as a WaterTank. Redefinition is a kind of subsetting, and a subsetting feature’s type has to specialize the type of what it subsets (KerML §8.3.3.3.10).
When to redefine
Redefine when an inherited feature needs to be more specific in the specialized context. Common cases:
- Binding a value:
:>> targetTemp = 94.0 - Narrowing a type:
:>> brewer : PressurizedBrewer - Tightening multiplicity:
:>> sensors [2]on a base that allows[1..*]
Do not redefine for cosmetic renaming alone. If the inherited name is fine and the type does not change, there is nothing to redefine.
Subsetting
Subsetting narrows a collection. Where redefinition replaces a feature, subsetting says: this feature is a subset of that inherited collection.
The operator is :> applied to a usage (not a definition). Context determines which relationship you get: on a definition, :> is subclassification; on a usage, it is subsetting (SysML §7.6.2, §7.6.3). The long spellings make it explicit – specializes on definitions, subsets on usages.
part def CoffeeMachine {
part sensors : Sensor[1..*];
}
part def EspressoMachine :> CoffeeMachine {
part tempSensors :> sensors : TemperatureSensor[1..2];
part pressureSensors :> sensors : PressureSensor[1];
}
tempSensors :> sensors says the temperature sensors are a subset of the total sensors collection. pressureSensors :> sensors is another subset. Both are parts of the same inherited sensors group, but they carve it into named subsets with more specific types.
The key difference from redefinition: subsetting does not replace the inherited feature. The sensors collection still exists. tempSensors and pressureSensors are portions of it.
When to subset vs. redefine
- Redefine when the specialized type has exactly one version of the inherited feature, and you want to replace it entirely.
- Subset when the inherited feature is a collection and you want to name specific portions of it without replacing the whole.
If the base type has part brewer : Brewer (a single part), redefine it. If the base type has part sensors : Sensor[1..*] (a collection), subset it.
Conjugation
Conjugation makes a type inherit another type’s features with their input and output directions reversed (SysML §7.12.3). The operator is ~ (tilde) placed before a type name.
You saw ports in Chapter 5. A port definition declares features with directions:
port def WaterSupplyPort {
out item water : Water;
}
When a water tank sends water out, it uses WaterSupplyPort directly. But the brewer that receives that water needs the same port shape with the direction flipped – out becomes in. Chapter 5 handled that by defining a second port type by hand: WaterPort, with in item water. Conjugation gets you the receiving side for free instead:
part def WaterTank {
port waterOut : WaterSupplyPort;
}
part def Brewer {
port waterInlet : ~WaterSupplyPort;
}
~WaterSupplyPort is WaterSupplyPort with all directions reversed. The out item water becomes in item water. The port shapes are structurally compatible – they carry the same items in opposite directions – which is exactly what you need for a connection.
When to conjugate
Conjugate when two components interact through complementary ports and the only difference is direction. This is the standard pattern for producer/consumer, sender/receiver, and request/response pairs.
Do not conjugate when the two sides of an interaction carry different items or have different structure. In that case, define two separate port types.
Putting It Together: The EspressoMachine
Here is a fuller example that uses specialization, redefinition, and conjugation together:
package CoffeeMachineVariants {
import ScalarValues::*;
import CoffeeMachineDomain::*;
part def PressurizedBrewer :> Brewer {
attribute boilerPressure : Real;
}
part def EspressoMachine :> CoffeeMachine {
// Redefine the brewer to a pressurized variant, and pin the
// water temperature it inherits from Brewer
part espressoBrewer :>> brewer : PressurizedBrewer {
attribute :>> waterTemp = 94.0;
}
// Add espresso-specific features
attribute shotVolume : Real;
}
}
Read this from top to bottom:
PressurizedBrewer :> Brewer– a new brewer type that addsboilerPressure.EspressoMachine :> CoffeeMachine– inherits grinder, brewer, water tank, serial number.espressoBrewer :>> brewer– the inheritedbrewerpart is redefined to usePressurizedBrewer.:>> waterTemp = 94.0– the brewer’s water temperature is bound to 94.0. Note where this line sits: insideespressoBrewer, becausewaterTempis a feature ofBrewer, not ofCoffeeMachine. A redefinition can only target a feature the enclosing type actually inherits, soattribute brewTemp :>> waterTempwritten directly inEspressoMachinewould fail to resolve.shotVolume– a new attribute that only espresso machines have.
That fourth line also shows a shortcut worth knowing: a redefining usage with no declared name takes the name of the feature it redefines (SysML §7.6.5), so this one is still called waterTemp.
An EspressoMachine instance has everything a CoffeeMachine has, plus the refinements. Any constraint, requirement, or verification case that references CoffeeMachine applies to EspressoMachine too, because specialization preserves that relationship.
Decision Guide
When you need to refine a type, choose the simplest mechanism that fits:
| Situation | Mechanism | Operator | Example |
|---|---|---|---|
| New type is a kind of existing type | Specialization | :> | EspressoMachine :> CoffeeMachine |
| Inherited feature needs a new default or narrower type | Redefinition | :>> | brewTemp :>> targetTemp = 94.0 |
| Inherited collection needs named subgroups | Subsetting | :> on usage | tempSensors :> sensors |
| Port directions need to be flipped | Conjugation | ~ | port waterInlet : ~WaterSupplyPort |
| Just adding new features to a type | Specialization alone | :> | Add pumpPressure in subtype |
| Replacing a part with a more specific variant | Redefinition | :>> | espressoBrewer :>> brewer |
| Documenting a family of named alternatives | Variation | variation / variant | see below |
Start at the top of this table. If specialization alone solves your problem, stop there. Reach for redefinition only when you need to change something inherited, not just add to it. Use subsetting only for genuine collections. Use conjugation only for direction inversion.
Variation
Sometimes a design has several named alternatives that share a common base by deliberate choice, rather than one refining the next. A coffee machine might offer two brew profiles – standard and intense – where neither is a refinement of the other; both are options the customer picks between.
variation <keyword> def <ChoiceName> :> <BaseName> {
variant <keyword> <name> : <AlternativeName>;
variant <keyword> <name> : <AlternativeName>;
}
part def BrewProfile {
attribute targetTemp : Real;
}
part def StandardBrew :> BrewProfile;
part def IntenseBrew :> BrewProfile;
variation part def BrewProfileChoice :> BrewProfile {
variant part standard : StandardBrew;
variant part intense : IntenseBrew;
}
part def CoffeeMachine {
part brewProfile : BrewProfileChoice;
}
BrewProfileChoice is a variation – a variation point, marked by the variation keyword before the kind keyword. Its members are its variants, each marked variant. A brewProfile typed by BrewProfileChoice resolves to exactly one of them per configuration.
The direction of those two keywords is what people get backwards. variation goes on the choice, not on the alternatives, and variant goes on the members inside it. Five rules follow, and the specification states all of them (SysML §7.6.7, with the formal constraints at §8.3.6.2):
- Variant usages may only be declared inside a variation.
- Every usage in a variation’s body is a variant, so a variation cannot carry ordinary features of its own. That is why
targetTemplives onBrewProfileand reachesBrewProfileChoiceby inheritance. - A variation is always abstract. You never write
abstracton it, and you never instantiate the choice – only a variant. - The kind of a variant must match the kind of its variation:
variant partinside avariation part def. - A variation may not specialize another variation. You can build a variation on an ordinary definition, as
BrewProfileChoicebuilds onBrewProfile, but you cannot chain choices together – a choice of choices is not a thing (validateDefinitionVariationSpecialization).
Usages can be variation points too, with the same shape – variation part transmission : Transmission { variant ...; } turns a single usage into a choice rather than a whole definition.
The point of variation is not refinement – it is alternation. Where EspressoMachine :> CoffeeMachine says every EspressoMachine is a CoffeeMachine, a variation says a given configuration picks one of several peers.
Use variation when the model genuinely enumerates alternatives the system can be configured into. When the relationship is “this is a more specific kind of that,” plain specialization is clearer; variation is the right tool only for the “choose one of these” pattern.
Elaboration: The Implicit Pass
Everything in this chapter – specialization, redefinition, subsetting, conjugation, variation – is something you write. But the model is also understood to gain relationships you did not write down. Weaving those in is called elaboration: the model is read as if a small set of implicit relationships were filled in for you.
Suppose a library declares:
part def Grinder {
attribute rpm : Real;
}
and your model specializes it:
part def HeatedGrinder :> Grinder {
attribute heatLevel : Real;
}
You wrote one :>, but HeatedGrinder is now understood to have both rpm (inherited from Grinder) and heatLevel. You did not redeclare rpm; elaboration supplies it. The same is true when specialization chains: declare BurrGrinder :> HeatedGrinder on top of HeatedGrinder :> Grinder, and the generalization from BurrGrinder all the way up to Grinder is implicit, so a constraint that references Grinder also sees every specialization of it as a valid operand.
The same idea fills in two more structures:
- Library-defined features. Standard-library supertypes (such as
Part) carry features the spec says every specialization gains; you don’t write them, but elaboration treats them as present. - Subsetting portions. When
tempSensors :> sensorsandpressureSensors :> sensorsboth subset the same collection, elaboration recognizes them as portions of the one inheritedsensorscollection, so they participate together in any query oversensors.
The point of elaboration is that you model in the direct relationships and the spec supplies the transitive ones. You don’t have to flatten the chain, redeclare inherited features, or enumerate library extensions – they are understood to exist because the spec says they do.
sysml-rs extends the spec here. The relationships elaboration adds – inherited features, transitive generalizations, library-provided extensions, subsetting-portion structure – are all spec. What sysml-rs chooses is when to realize them: it materializes them at elaboration time (eagerly, before the model is queried) rather than deriving them lazily on each request. The visible effect is the same; only the timing is the tool’s choice.
Common Mistakes
Specializing when composition is enough. If you just need a coffee machine with different parts inside, you do not need a new definition. Create a usage and configure it:
// Unnecessary specialization:
part def LargeCoffeeMachine :> CoffeeMachine {
part bigTank :>> waterTank : LargeWaterTank;
}
// Often sufficient: just a configured usage
part largeMachine : CoffeeMachine {
part bigTank :>> waterTank : LargeWaterTank;
}
The second form creates a specific usage of CoffeeMachine with a redefined water tank. No new definition needed. Specialize only when other parts of your model need to reference the new type by name – in constraints, requirements, or further specializations.
Redefining without a semantic reason. Renaming an inherited feature just because you prefer a different name adds noise to the model. Redefinition is a semantic statement: “this feature means something more specific here.” If the inherited name and type are fine, leave them alone.
Defining mirrored port types by hand. If you see two port definitions that are identical except that in and out are swapped, replace one with a conjugation. Two definitions means two places to update when the port structure changes. Conjugation keeps them automatically synchronized.
What You Have Built
The coffee machine now has a family, not just a shape. EspressoMachine
specializes CoffeeMachine, redefines the brewer to a pressurized one and pins
the temperature it inherits, and adds what only espresso machines have.
Four relationships did that work, and they are worth keeping straight:
- Specialization (
:>on a definition) – a more specific kind. - Redefinition (
:>>) – an inherited feature, made more specific here. - Subsetting (
:>on a usage) – this feature’s values are among those. - Conjugation (
~) – the same interface with its directions reversed.
Variation is the odd one out, and deliberately so: it is alternation rather
than refinement. EspressoMachine :> CoffeeMachine says every espresso machine
is a coffee machine. A variation says a configuration picks one of several peers.
In Chapter 13 the model starts carrying information about itself – maturity, ownership, safety classification – and you will use that to generate views for particular readers.