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

concept hierarchy
Figure 1. Externalized names concept hierarchy
  • 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 of INameable and MPS' own INamedConcept. Use this interface instead of INamedConcept.

    Structure
    • Extend INameable.

    • Extend INamedConcept.

    Editor

    Consider overriding the editor component INameable with INamed. Add property cell for name.

    Behavior
    • 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 Foos 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 extend AFoo (depending on the choice above).

    Constraints
    • 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.

    Structure
    • Implement IFoo or extend AFoo (depending on the choice above).

    • Add reference fooRef: Foo [1].

    Editor
    • 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.

    Behavior
    • 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.

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.