123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- Metamodelling
- =============
- Defining languages in the Modelverse is done through metamodelling.
- With metamodelling, a language is itself a model, which can be created through the usual operations.
- This explains why language engineers need to have the same knowledge of the Modelverse as modellers: language engineering is modelling in a domain-specific language for languages.
- Creating a metamodel
- --------------------
- To create a metamodel, you have to instantiate the *SimpleClassDiagrams* metamodel.
- While there is no restriction that this has to be *SimpleClassDiagrams*, for now we assume that this is the only possible meta-language.
- Therefore, to create the PetriNets language, we execute::
- >>> model_add("formalisms/PetriNets", "models/SimpleClassDiagrams")
- From then on, we can simply use the PetriNets metamodel as if it were a model.
- Again, the *model_add* operation can take an optional third parameter, specifying the textual representation of the model.
- We will go deeper into this in one of the next sections.
- Instantiating a metamodel
- -------------------------
- To actually define a metamodel, we need to add elements to it.
- These elements are tightly related to the metamodel of the model, being most likely *SimpleClassDiagrams*.
- In this language, there exist four major components: *Class*, *Association*, *Inheritance*, and attributes.
- Class
- ^^^^^
- Creating a class is easy, and very similar to usual modelling.
- To define the *Place* concept, which can later on be used in models, we merely define this as a new class::
- >>> instantiate("formalisms/PetriNets", "Class", ID="Place")
- Note that here we must define an ID to get a sensible name to use.
- Of course, this ID does not need to be used, and the returned (randomized) ID can just as well be used.
- Nonetheless, if it is used, it becomes non-intuitive to use in future requests.
- In the instance of this newly created language, we can immediately use the new concept::
- >>> instantiate("formalisms/PetriNets", "Class", ID="Place")
- >>> instantiate("my_pn", "Place")
- If the ID was not used, we must remember the ID that was provided to us::
- >>> pn_place = instantiate("formalisms/PetriNets", "Class")
- >>> instantiate("models/my_pn", pn_place)
- While this is more reliable, it becomes difficult to refer to the concept in the future without any clue as to what it resembles.
- Later on, this can be solved with concrete syntax, in which case modellers can identify the concepts based on their visual representation.
- Association
- ^^^^^^^^^^^
- Creating a link between two concepts is very similar.
- For example, to create the *P2T* association, merely create an instance of association, using the same remarks as before::
- >>> instantiate("formalisms/PetriNets", "Association", edge=("Place", "Transition"), ID="P2T")
- Inheritance
- ^^^^^^^^^^^
- A useful concept is the use of inheritance, whereby the inheriting class can take on the responsibilities of the inherited class.
- To create this, simply instantiate the *Inheritance* association as usual.
- For example, it is possible to structure the *PetriNets* model (in an admittedly strange way) such that there is a generic concept of *NamedElement* and *Arc*::
- >>> instantiate("formalisms/PetriNets", "Class", ID="NamedElement")
- >>> instantiate("formalisms/PetriNets", "Class", ID="Place")
- >>> instantiate("formalisms/PetriNets", "Class", ID="Transition")
- >>> instantiate("formalisms/PetriNets", "Inheritance", edge=("Place", "NamedElement"))
- >>> instantiate("formalisms/PetriNets", "Inheritance", edge=("Transition", "NamedElement"))
- In the Modelverse, inheritance is possible between any two instances, including associations.
- To define the *Arc* relation, we act similar::
- >>> instantiate("formalisms/PetriNets", "Association", edge=("NamedElement", "NamedElement"), ID="Arc")
- >>> instantiate("formalisms/PetriNets", "Association", edge=("Place", "Transition"), ID="P2T")
- >>> instantiate("formalisms/PetriNets", "Association", edge=("Transition", "Place"), ID="T2P")
- >>> instantiate("formalisms/PetriNets", "Inheritance", edge=("P2T", "Arc"))
- >>> instantiate("formalisms/PetriNets", "Inheritance", edge=("T2P", "Arc"))
- Instantiating attributes
- ^^^^^^^^^^^^^^^^^^^^^^^^
- Instantiating attributes is similar as it was before::
- >>> attr_assign("formalisms/PetriNets", "NamedElement", "name", "NamedElement")
- >>> attr_assign("formalisms/PetriNets", "Transition", "name", "Transition")
- >>> attr_assign("formalisms/PetriNets", "Place", "name", "Place")
- Note that this *name* attribute is in no way related to the *name* attributes at the *my_pn* level.
- Indeed, the *name* in *PetriNets* specifies the name of the class.
- Other attributes that can be set on classes are:
- 1. *lower_cardinality* specifies the number (integer) of instances that must at least exist of this element.
- 2. *upper_cardinality* specifies the number (integer) of instances that must at most exist of this element.
- 3. *constraint* specifies additional constraints on the instances of this class.
- 4. *abstract* specifies whether the class is to be abstract or not.
- For associations, we have the same three attributes, augmented with:
- 5. *source_lower_cardinality* specifies the number (integer) of incoming edges that are at least required for the target.
- 6. *source_upper_cardinality* specifies the number (integer) of incoming edges that are at most required for the target.
- 7. *target_lower_cardinality* specifies the number (integer) of outgoing edges that are at least required for the source.
- 8. *target_upper_cardinality* specifies the number (integer) of outgoing edges that are at most required for the source.
- Constraints
- ^^^^^^^^^^^
- A constraint specifies additional constraints on the instances of this model, which could not be expressed before.
- We distinguish between two types of constraints: local and global constraints.
- Local constraints are defined at the level of a single class or association.
- For example, if the number of tokens must always be non-negative, this can be specified as follows::
- >>> attr_assign_code("formalisms/PetriNets", "Place", "constraint", \
- ... """
- ... String function constraint(model : Element, name : String):
- ... if (integer_gte(read_attribute(model, name, "tokens"), 0)):
- ... return "OK"!
- ... else:
- ... return "Negative number of tokens specified"!
- Global constraints operate over the complete model, and are therefore not attached to a single element.
- Instead, a specific *GlobalConstraint* element must be instantiated.
- This element has a *constraint* attribute, which specifies the constraint to evaluate in the global context.
- For example, when, for some reason, the number of places must always be larger than the number of transitions::
- >>> constraint = instantiate("formalisms/PetriNets", "GlobalConstraint")
- >>> attr_assign_code("formalisms/PetriNets", constraint, "constraint", \
- ... """
- ... String function constraint(model : Element):
- ... if (set_len(allInstances(model, "Place")) > set_len(allInstances(model, "Transition"))):
- ... return "OK"!
- ... else:
- ... return "Needs more places than transitions"!
- While all constraints can be specified as global constraints, using local constraints is preferred.
- .. note::
- Currently, all constraints are only evaluated upon explicit user request.
- More specifically: during the *verify* operation.
- Defining attributes
- ^^^^^^^^^^^^^^^^^^^
- Defining attributes is an important consideration in a metamodel.
- Up to now, our *Place* and *Transition* were just nodes without any attributes.
- A meaningful (marked) petrinet has tokens in places, and names for the places and transitions.
- To define these attributes, we must take two steps:
- 1. Define the type that will be used (e.g., String, Integer)
- 2. Define the name and type of the attribute (e.g., *tokens* is an Integer, *name* is a String)
- To define the type that we want to use, we must manually define it.
- While many other tools provide a set of primitives, which cannot be altered in any way, the Modelverse leaves all this up to the language engineer.
- If the language engineer wants the concepts of a *Natural*, for example, it is not required that the attribute is specified as an Integer, with an additional constraint on each and every attribute that is defined this way.
- It is possible to define a new attribute type as follows::
- >>> instantiate("formalisms/PetriNets", "SimpleAttribute", ID="Natural")
- But now, the attribute is constrained in no way: it is merely called Natural, but can even contain a string.
- The attribute type must be constrained::
- >>> attr_assign_code("formalisms/PetriNets", "Natural", "constraint", \
- ... """
- ... String function constraint(value : Element):
- ... if (is_physical_integer(value)):
- ... if (integer_gte(value, 0)):
- ... return "OK"!
- ... else:
- ... return "Not a positive value"!
- ... else:
- ... return "Not a numeric value!"!
- Now, the *Natural* is correctly specified and can be used throughout the model.
- All uses of *Natural* will now make sure that the value is positive.
- This offers additional possibilities, such as defining complex attributes as if they were primitives, for example complex numbers.
- The next step is to define the attribute itself.
- This can be easily done as follows::
- >>> define_attribute("formalisms/PetriNets", "Place", "tokens", "Natural")
- For the name, *String* can be defined similarly (using *is_physical_string* and omitting the check greater than 0).
|