Language users will often want to write certain definitions that are later reused elsewhere in their models. For example, a user might define an entity, listing its attributes and references to other entities, or a UI component, such as an address form or a search form.

Many of these definitions are best written separately from their usages. You first define an entity and then have other entities link to it. Or you create a form first and then refer to it from some action code. In other cases, however, the user might want to define a certain thing inline, right where it is used.

We know this pattern from programming languages, of course:

  • A class in Java can be defined at the top level, or as an anonymous inner class, used at the point of definition.
  • A lambda expression is an anonymous inline function.

It turns out that this pattern is useful for domain-specific languages, too. For example, when describing tax forms, the user might want to include a separately defined block with common fields, such as the taxpayer’s name and address. Other fields may form conceptual blocks too, but they would never be reused in a different form, so creating them separately would mean unnecessary friction and an anonymous, inline block would be better.

As language designers we can give our users the same flexibility we enjoy in our programming languages. This pattern came up several times in one of my projects and I wanted to implement it consistently so I have created a checklist to help me do that. After seeing this pattern in another project, I realized that it is probably quite common and perhaps the checklist could help other language designers. Or perhaps seeing the checklist approach in action could inspire you to come up with checklists for the patterns you see frequently in the languages that you develop.

Inline definitions: a checklist

Inline definitions concept hierarchy

  • Add IFooDefinition interface: the base interface for definitions

    • TIP: Use an interface instead of an abstract concept as the inline version may need to extend a concept.
    • Structure:
      • Add common properties and links for both concepts
        • TIP: Think about which features the inline definitions will need: it will probably have fewer features than the separate definition, as it may get some information out of its context.
    • Editor:
      • Consider defining an editor component for the common parts.
    • Behavior:
      • Add abstract methods for information that’s not available in the structure (the information that the inline definition might get from its context)
    • Type system:
      • Add checking rules common to both definition kinds.
  • Add NamedFooDefinition: the separate, named, definition

    • Structure:
      • implements: INamedConcept, IFooDefinition
      • can be root: true
      • alias: Foo Definition
      • Add the fields necessary to implement the inherited behavior methods.
    • Editor: as required, may want to use the editor component for the common parts, if available.
    • Behavior:
      • Implement the methods inherited from IFooDefinition.
  • Add IFooDefinitionUse interface/concept: to be used where the definition is supposed to be used

    • Only necessary if there is no base concept for uses, such as Expression.
    • editor: use a single error cell
  • Add NamedFooDefinitionReference: represents the use of the named definition

    • Create it by invoking Create Reference Concept intention on NamedFooDefinition
    • Constraints:
      • The default scope of the reference is current model and imported models. If needed, define a different scope explicitly.
  • Add InlineFooDefinition: the inline version (combines use and definition)

    • Structure:
      • implements: IFooDefinition, IFooDefinitionUse
      • alias: inline
      • short description: an inline foo
    • Behavior:
      • Implement the behavior methods inherited from IFooDefinition.
    • Editor:
      • Copy from NamedFooDefinition, adapt.
    • Constraints:
      • Should InlineFooDefinition only be allowed in some specific context? If yes, implement a can be child constraint.
  • Tests

    • Test the common checking/typing rules using the named definition (as it’s easier to instantiate).