Document {
children:
sections: Section [0..*]
references:
-
}
Section implements INamedConcept {
children:
contents: Word [0..*]
references:
-
}
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. |
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.
Add interface INameable
: The base of all things that have their own or a referenced name.
nothing
Consider having an editor component INameable
that can be used everywhere you want to display a thing with name.
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 of INameable
and MPS' own INamedConcept
.
Use this interface instead of INamedConcept
.
Extend INameable
.
Extend INamedConcept
.
Consider overriding the editor component INameable
with INamed
.
Add property cell for name
.
Implement method string getName() overrides INameable.getName
with body this.name;
.
Implement method SProperty getNameProperty() overrides INameable.getNameProperty
with body property/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 limit Foo s to one single inheritance linage.
|
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.
Implement INamed
.
Implement IFoo
or extend AFoo
(depending on the choice above).
If we made AFoo
an abstract concept, open AFoo
constraints and add Foo
as default concrete concept.
Add concept FooReference
: The thing that has only a referenced name.
Implement IFoo
or extend AFoo
(depending on the choice above).
Add reference fooRef: Foo [1]
.
Consider overriding the editor component INameable
with FooReferenceName
.
Add ref. presentation cell for fooRef
.
Add concept editor: Either use editor component FooReferenceName
or create the same implementation directly.
Implement method string getName() overrides INameable.getName
with body this.fooRef.name;
.
Implement method SProperty getNameProperty() overrides INameable.getNameProperty
with body null;
.
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.