on
MPS language design patterns: Externalized names (and other common attributes)
We often want to intermix referred names and defined names. This works best if we plan ahead for this situation, and have a shared abstraction for both of them from the beginning.
| This article follows Sergej Koščejev’s excellent idea on language design patterns. |
Rationale
As an example, think of a document with named sections.
The root concept would be Document, containing Sections (details of how to handle the contents are beyond the scope of this article):
Document {
children:
sections: Section [0..*]
references:
-
}
Section implements INamedConcept {
children:
contents: Word [0..*]
references:
-
}
We’re building our language around that structure, including checks on uniqueness of Section.name, generators that iterate over all Document.sections and their contents, and so on.
This is a reasonable structure … until our document gets too big, or we want to reuse a section in different documents.
We want to introduce a concept SectionReference:
SectionReference extends Section {
children:
-
references:
originalSection: Section [1]
}
It has to extend Section, otherwise we cannot use it inside our document.
This means we also inherit the name property and contents child.
But they should not be contained here — they should come from the referenced originalSection!
For properties, we could write custom getters and setters that forward to the referenced section. However, each instance of SectionReference would still store their name on their own. This will bite us, and our users, on the first complicated model merge.
For children, we would need to use even more advanced trickery, if we managed at all.
More likely, we have to touch all places in all of our language aspects that access contents and handle them appropriately.
Then we have to do the same thing three more times because we didn’t find all the places in one try.
In my experience, this situation arises in almost all languages sooner or later. Names are the prime example; other candidates are all highly common, shared, and uniformly processed attributes like comments, visibility, or recursively contained structures — think of sections inside sections.
Externalized names: a checklist
-
Add interface
INameable: The base of all things that have their own or a referenced name.- Structure
-
nothing
- Editor
-
Consider having an editor component
INameablethat can be used everywhere you want to display a thing with name. - Behavior
-
-
Add abstract method
string getName(): Retrieves the own or referenced name. -
Add abstract method
SProperty getNameProperty(): We commonly want to mark an error (or warning) on a named thing. The red squiggly line should only appear on the name, not the whole (potentially huge) block. For this, we need to supply the property to the error check.
-
-
Add interface
INamed: The combination ofINameableand MPS' ownINamedConcept. Use this interface instead ofINamedConcept.- Structure
-
-
Extend
INameable. -
Extend
INamedConcept.
-
- Editor
-
Consider overriding the editor component
INameablewithINamed. Add property cell forname. - Behavior
-
-
Implement method
string getName() overrides INameable.getNamewith bodythis.name;. -
Implement method
SProperty getNameProperty() overrides INameable.getNamePropertywith bodyproperty/INamedConcept : name/;.
-
-
Add interface
IFoo: Commonality between a first-class thing and a referenced thing.We might use an abstract concept instead of an interface, and use the name AFoo. Advantage: Foo could be declared AFoo's default concrete concept. Disadvantage: We limitFoos to one single inheritance linage.- Structure
-
-
Extend
INameable: Every Foo has a name, either their own or referenced.
-
-
Add concept
Foo: The thing that has its own name and can be referenced.- Structure
-
-
Implement
INamed. -
Implement
IFooor extendAFoo(depending on the choice above).
-
- Constraints
-
-
If we made
AFooan abstract concept, openAFooconstraints and addFooas default concrete concept.
-
-
Add concept
FooReference: The thing that has only a referenced name.- Structure
-
-
Implement
IFooor extendAFoo(depending on the choice above). -
Add reference
fooRef: Foo [1].
-
- Editor
-
-
Consider overriding the editor component
INameablewithFooReferenceName. Add ref. presentation cell forfooRef. -
Add concept editor: Either use editor component
FooReferenceNameor create the same implementation directly.
-
- Behavior
-
-
Implement method
string getName() overrides INameable.getNamewith bodythis.fooRef.name;. -
Implement method
SProperty getNameProperty() overrides INameable.getNamePropertywith bodynull;. MPS' error checking code will handle this gracefully, and mark the whole concept with a red squiggly line; that’s completely fine in this case, as our concept renders only as the referenced name.
-
The hardest part is the required discipline never to access INamedConcept.name directly.
Instead, use INameable.getName() (and INameable.getNameProperty() in constraint checks).
Every now and then, search for all references to INamedConcept.name and replace them appropriately.
We can use the same pattern (modulo the getNameProperty() method) for other highly common, shared, and uniformly processed attributes.
About Niko Stotz
I head the Model Driven Engineering team at F1RE, and support our colleagues and customers with language engineering and DSL expertise.
You can contact me at niko@f1re.io.