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
INameable
that 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 ofINameable
and MPS' ownINamedConcept
. Use this interface instead ofINamedConcept
.- Structure
-
-
Extend
INameable
. -
Extend
INamedConcept
.
-
- Editor
-
Consider overriding the editor component
INameable
withINamed
. Add property cell forname
. - Behavior
-
-
Implement method
string getName() overrides INameable.getName
with bodythis.name;
. -
Implement method
SProperty getNameProperty() overrides INameable.getNameProperty
with 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 limitFoo
s 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
IFoo
or extendAFoo
(depending on the choice above).
-
- Constraints
-
-
If we made
AFoo
an abstract concept, openAFoo
constraints and addFoo
as default concrete concept.
-
-
Add concept
FooReference
: The thing that has only a referenced name.- Structure
-
-
Implement
IFoo
or extendAFoo
(depending on the choice above). -
Add reference
fooRef: Foo [1]
.
-
- Editor
-
-
Consider overriding the editor component
INameable
withFooReferenceName
. Add ref. presentation cell forfooRef
. -
Add concept editor: Either use editor component
FooReferenceName
or create the same implementation directly.
-
- Behavior
-
-
Implement method
string getName() overrides INameable.getName
with bodythis.fooRef.name;
. -
Implement method
SProperty getNameProperty() overrides INameable.getNameProperty
with 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.