Frag Logo
  Frag Home | Frag SF Project   index | contents | previous | next

Creating DSLs with Frag: Implementing a Language Model

FMF Overview

As explained before, many (model-driven) DSLs are nowadays based on an explicit language model. FMF, explained in this section, is a Frag package to build a language model for a DSL. However, before explaining FMF, note that this is only a design option: Because Frag is a full-fledged programming language, a DSL design in Frag does not have to be based on a language model that is similar to a modeling language. We can also use a structure of ordinary programming language objects to represent the DSL language model - akin to many DSLs in other dynamic languages. These Frag objects can either be implemented in Frag or in Java.

As the language model is usually a central element of the DSL and often linked to some model, e.g., in the UML, in many cases it is advisable to base Frag DSLs on a language model that has a distinct representation in the language and is easily mappable to existing modeling languages. Frag implements a modeling framework called FMF that is similar to the modeling frameworks found in other model-driven language workbenches. Using this modeling framework, an explicit language model can be defined. FMF classes and objects are ordinary Frag objects. For this reason, deciding for the FMF option does not limit decision options with regard to other parts of the DSL. FMF classes can also be instantiated in Java, too.

FMF defines a meta-model that can be used to specify UML-style language models in Frag. The language model classes are defined using the FMF::Class meta-class. FMF introduces also a number of relationships between classes: Associations, Compositions, Aggregations, and Inheritance, as well as extensions using stereotypes, enumerations, and so on.

To work with FMF, you must first import the FMF package into Frag. This is done using:
import mdd.FMF

The following figure illustrates how a simple UML class model, containing two classes, a few attributes, and a composition relationship, is mapped to Frag code. The translation is in most cases a one-to-one mapping. Once we have defined a language model in Frag, we can use the language model to create instances using the same syntax.

./images/fmf-example.gif

The following figure illustrates how an instance of the UML model is mapped to Frag code.

./images/fmf-example-instances.gif

FMF Class

FMF::Class is used to define FMF classes that can be instantiated as Frag objects. FMF classes are Frag objects, too. Hence, all Frag features can be used for FMF classes and objects, as for all other Frag objects.

Attributes

Defining typed attributes

One of the basic features of FMF classes are attributes. They can be defined using the attributes method using the following syntax:
<fmf_object> attributes <attributes>
attributes allows you to define the attributes of a class. The attributes are given in a paired list of attribute names and types. Each attribute defined using attributes has a type parameter, which is checked, when the attribute is used. A number of attribute types have been predefined, but any other Frag object can also be used as an attribute type. The predefined attribute types are: You can use any capitalization to define these argument types (e.g., string and String are legal).

The following code defines a document class with a number of String attributes.
FMF::Class create Document -attributes {
    text String
    title String
    description String
    author String
    version String
}
The attributes can be used as methods on objects of the class. For instance in the following we define a document based on the FMF class defined above:
Document create d1 -text "..." -title "my document" \
    -description "..." -author "Uwe" -version "0.1"
You can change a particular attribute by invoking its method with one argument, the new value. The following changes the version attribute's value to 0.2:
d1 version "0.2"
You can read out an attribute by calling its method without arguments. For example, the following code will print Author = Uwe.
puts "Author = [d1 author]"

As a second example, consider the following class:
FMF::Class create DocumentAnnotation -attributes {
    doc Document
    pages int
    price float
}
We can create an instance of this class as follows:
DocumentAnnotation create da -doc d1 -pages 204 -price 3.75
Please note that FMF will generate a runtime error, if you would not put a document instance (or an instance of a subclass) into doc, or not an integer value into pages, or not a number that can be converted to float into price. For instance, the next invocation will raise the exception: value 'A 12' is not an integer.
da pages "A 12"

Defining untyped attributes

You can also define untyped attributes using the method untypedAttributes, which are not checked at runtime for type compliance. This method has the following syntax:
<fmf_object> untypedAttributes <attributes>
It only receives a list of attributes, without types. Untyped attributes behave like typed attributes, only they don't have a type check. For example the method can be used as follows:
FMF::Class create Document -untypedAttributes {
    text
    title 
    description
    author
    version
}

Introspecting attributes

You can introspect all arguments of a class using getAttributes. It has the following syntax:
<fmf_class> getAttributes
For example:
Document getAttributes
This invocation returns the list of all attributes of this class: version author description title text.

You can also query whether a class has an attribute using hasAttribute with the following syntax:
<fmf_class> hasAttribute <attribute>
This method returns true, if the class has the attribute, and false if not.

For example, Document hasAttribute text returns true, but Document hasAttribute t returns false. Finally, you can query for the type of an attribute using:
<fmf_class> getAttributeType <attribute>
For example, the following invocation returns int.
DocumentAnnotation getAttributeType pages

Untyped attributes return an empty string.

Relationships

The second basic features of FMF classes are relationships. They will be explained in this section.

Inheritance Relationships

As all Frag objects, FMF classes can use the basic Frag relationships, such as inheritance (using superclasses) or mixins. For example, a document can have document elements of various types:
FMF::Class create DocElement
FMF::Class create TOC -superclasses DocElement 
FMF::Class create DocElementContainer -superclasses DocElement
FMF::Class create SectionContainer -superclasses DocElementContainer

FMF::Class create Section -superclasses SectionContainer
FMF::Class create SubSection -superclasses SectionContainer
FMF::Class create SubSubSection -superclasses SectionContainer
FMF::Class create Paragraph -superclasses SectionContainer

FMF::Class create List -superclasses DocElement
FMF::Class create ListItem -superclasses DocElementContainer

FMF::Class create Text -superclasses DocElement

Using Associations

A special feature of FMF are associations, and their two specializations aggregation and composition. We want to describe them in more detail in this section.

In contrast to ordinary Frag object dependencies, i.e., an object ID is simply stored in another object's variable, associations are represented by an association object that links to the two FMF classes it should relate.

The easiest way to define an association is to instantiate an association object and use its method ends to define all association ends using a shortcut syntax. An example is the following association defined for the classes Document and DocElement:
FMF::Association create doc-elt -ends {
    {Document -roleName document -multiplicity 0..1 -navigable true}
    {DocElement -roleName elements -multiplicity * -navigable true}
}
This code creates an association object named doc-elt. This object has two association ends, defined through the two elements of the list given to ends. Both lists have the same syntax: They first provide the mandatory name of the class that is linked through the association, and then a number of optional arguments (indicated by - follow). The following arguments are possible for the definition of association ends: A side-effect of the ends method is that suitable methods for the association roles will be defined on the classes partaking in the association. That is, for each navigable association end that has a role name, a method named using the role name will be defined on the other end of the association. In the example above, a method document will be defined on DocElement and a method elements on Document. Both accept a list of arguments. The number of elements in this list must match the multiplicity of the corresponding association end.

For example, we can create a document and two elements associated with it, both being of subtypes of DocElement.
Document d1
Text create t1 -document d1
Section create s1 -document d1
We can query both classes for the relationship, as they both have navigable association roles:
puts "Doc Elements = [d1 elements]"
This will print:
Doc Elements = t1 s1
puts "Document = [t1 document]"
This will print:
Document = d1
As you can see, the consistency of the two ends of the association is automatically ensured. Hence, the same result as in the code above can be achieved by defining a number of document elements and adding the whole list to the document:
Text create t1
Section create s1
Document d1 -elements {t1 s1}

Aggregation and Composition

Association has two subclasses: Aggregation and Composition. They are used to implement the respective features of UML, i.e., containment relationships. At the moment they behave pretty much like associations; using them is just a means of documenting the design intent. They have one additional feature, though: the association ends of them have one additional attribute, called aggregatingEnd. Using this boolean feature, you can specify which of the two ends is the aggregating end.

For example, if you want to compose a book out of a number of documents, the book aggregates the documents, and hence it is the aggregating end:
FMF::Class create Book
    
FMF::Class create Document -attributes {
    text String
    title String
    description String
    author String
    version String
}

FMF::Composition create Book-Document -ends {
    {Book -roleName book -multiplicity 0..1 -navigable true -aggregatingEnd true}
    {Document -roleName document -multiplicity * -navigable true}
}
FMF::Aggregation is used in the same way.

Introspecting Association Roles

You can use navigable association roles to navigate from a class to its associated classes. However, not always you know the navigable association role names defined for a class. For this situation, FMF::Class offers the getNavigableAssociationRoles method to introspect them: It allows you to retrieve the list of all navigable association roles (defined as methods) on a class.

For example, in the above examples we have defined three classes. Introspecting their navigable association roles is done as follows:
puts "Book assoc roles = [Book getNavigableAssociationRoles]"
puts "Document assoc roles = [Document getNavigableAssociationRoles]"
puts "DocElement assoc roles = [DocElement getNavigableAssociationRoles]"

This yields the following results:
Book assoc roles = document
Document assoc roles = book elements
DocElement assoc roles = document

Introspecting and Defining Association Ends

For understanding how to introspect associations, it is important to know that each association instantiates two objects for its ends, having the type AssociationEnd. They contain the data of the ends. You can retrieve them using the getEnds methods defined on Association. For example, using doc-elt getEnds we can get the list of the two association ends of the doc-elt association defined above. AssociationEnd defines a number of introspection methods for the properties of associations explained above, as well as the class that partakes in the association. In addition, a setter method for each of these properties is defined. This way, all information defined for an end using the ends method can be queried and changed individually: For instance, the following prints all the information defined using ends above:
foreach ae [doc-elt getEnds] {
    puts "END:"
    puts "... Multiplicity = [$ae getMultiplicity]"
    puts "... Role Name = [$ae getRoleName]"
    puts "... Is Navigable = [$ae isNavigable]"
    puts "... Is Aggregating End = [$ae isAggregatingEnd]"
    puts "... Class = [$ae getClass]"
}
The result is:
END:
... Multiplicity = 0..1
... Role Name = document
... Is Navigable = true
... Is Aggregating End = false
... Class = Document
END:
... Multiplicity = *
... Role Name = elements
... Is Navigable = true
... Is Aggregating End = false
... Class = DocElement
Using AssociationEnd of course you can also define ends from scratch. You can put them onto an Association using the setEnds method. So instead of the short syntax used above, you could also use:
FMF::AssociationEnd create ae1 \
    -setMultiplicity 0..1 \
    -setRoleName document \
    -setNavigable true \
    -setAggregatingEnd false \
    -setClass Document
FMF::AssociationEnd create ae2 \
    -setMultiplicity * \
    -setRoleName elements \
    -setNavigable true \
    -setAggregatingEnd false \
    -setClass DocElement
FMF::Association create doc_elt2 -setEnds {ae1 ae2}

Introspecting Association Objects of a Class

In the previous discussion, we were assuming that you start from an association object to introspect its ends. Sometimes you don't have the association objects of a class at hand, but only the FMF class. For this situation, FMF::Class offers the getAssociationObject method. It allows you to query for the association object connected to a navigable association role (the role name is given as an argument to the getAssociationObject method).

For example, in the association defined above doc_elt2 is the association object. You can query the two classes involved for it as follows:
puts "[Document getAssociationObject elements]"
puts "[DocElement getAssociationObject document]"

Enumerations

Using FMF::Enum you can define enumeration types. Enumerations are pretty simple to use. The class offers two methods: getEnumValues and setEnumValues. You cannot instantiate an enumeration.

setEnumValues takes a list of enumeration values are argument. For example, to define the standard UML VisibilityKinds, you can define an Enum like this:
FMF::Enum create VisibilityKind -setEnumValues {
    PUBLIC PROTECTED PRIVATE PACKAGE
}

getEnumValues has no arguments and returns the list of enumeration values. For example, the following invocation returns the list of enum values defined above (i.e., PUBLIC PROTECTED PRIVATE PACKAGE):
VisibilityKind getEnumValues    

Enumeration types are typically used as attribute types. These attributes accept only the enum values as legal values. For example, in the following code we use a visibility kind attribute and set it on an instance to PRIVATE:
FMF::Class create C1 -attributes {
    id int
    visibility VisibilityKind
}
C1 create c1 -id 123 -visibility PRIVATE
Reading the attribute using c1 visibility will return the string PRIVATE.

Stereotypes

FMF classes can be extended using stereotypes, pretty much like stereotypes are used in UML. This is done using the FMF::Stereotype class. Stereotypes need to be created like classes. Using the extends method, we can let a stereotype extend a class. extends has the following syntax:
<fmf_stereotype> extends <fmf_class>
For example, if we want to be able to denote special kinds of ports that are able to receive inputs, we can define an input stereotype for ports:
FMF::Class create Port

FMF::Stereotype create Input
Input extends Port
Now, if we define a number of port instances, we can let some of them become input ports. To do so, we must use a new method defined automatically (via extends) for Input with the syntax: base<fmf_class>. With argument, this method allows you to specify a class that is extended by a stereotype:
Port create p1 
Port create p2

Input create i1 -basePort p1
Input create i2 -basePort p2
Input create i3 -basePort p1
Without argument, this method can also be used for querying the class that has been extended. For example the following will print p2:
puts [i2 basePort]        
Above we have defined two input ports. In addition to the basePort method, on the Port class (the stereotype class) another method has been defined automatically via extends: extension<fmf_stereotype>. This kind of method can be used for introspecting or changing the stereotype(s) that extend a class. For example, the following invocation will print the list "i1 i3":
puts [p1 extensionInput]
With argument, you can use this kind of method to define the extension performed by a stereotype.

Please note that the base<fmf_class> and extension<fmf_stereotype> syntaxes for method names are taken as naming conventions from UML2, where these names are used to denote the association ends of stereotypes in UML's extends relationship. The only difference is that in UML the extension<fmf_stereotype> path is not navigable, which is often awkward. Hence, we decided to define the method as a navigable relationship in FMF.

The Stereotype class allows you also to introspect which class has been extended by the stereotype, using getExtendedClass. This method takes no arguments. For example, the following invocation will print "Port".
puts [Input getExtendedClass]

In UML, profiles can also define tag values as extensions on classes. These can be implemented in FMF using attributes on stereotype classes. For example, we want to give each input's input source an identifier, we could define an "inputSource" attribute on the Input class:
FMF::Stereotype create Input -extends Port -attributes {
    inputSource String
}
Now we can use this attribute like a tag value, when using the stereotypes:
Input create i1 -basePort p1 -inputSource "RPC"

Example: Defining UML2 Activity Diagrams and extending them

As a small example of using FMF, let us define a model for an excerpt of the UML2 Activity Diagrams meta-model. This can be defined as follows:
Namespace create UML2

UML2 evalInScope {
    FMF::Class create Classifier
    
    # here we obmit a few classes in the UML2 inheritance hierarchy
    FMF::Class create TypedElement -attributes {
        type Classifier
    }   
    FMF::Class create Activity
    FMF::Class create ActivityNode
    
    FMF::Composition create ActivityNodes -ends {
        {Activity -roleName activity -multiplicity 0..1 -navigable true -aggregatingEnd true}
        {ActivityNode -roleName node -multiplicity * -navigable true}
    }
    
    FMF::Class create ActivityEdge 
    
    FMF::Composition create ActivityEdges -ends {
        {Activity -roleName activity -multiplicity 0..1 -navigable true -aggregatingEnd true}
        {ActivityEdge -roleName edge -multiplicity * -navigable true}
    }
    
    FMF::Association create ActivityEdgeIncoming -ends {
        {ActivityEdge -roleName incoming -multiplicity * -navigable true}
        {ActivityNode -roleName target -multiplicity 1 -navigable true}
    }
    
    FMF::Association create ActivityEdgeOutgoing -ends {
        {ActivityEdge -roleName outgoing -multiplicity * -navigable true}
        {ActivityNode -roleName source -multiplicity 1 -navigable true}
    }
    
    FMF::Class create ControlNode -superclasses ActivityNode
    FMF::Class create ObjectNode -superclasses ActivityNode TypedElement
    FMF::Class create FinalNode -superclasses ControlNode
    FMF::Class create ActivityFinalNode -superclasses FinalNode
    FMF::Class create FlowFinalNode -superclasses FinalNode
    FMF::Class create ForkNode -superclasses ControlNode
    FMF::Class create JoinNode -superclasses ControlNode
    FMF::Class create MergeNode -superclasses ControlNode
    FMF::Class create DecisionNode -superclasses ControlNode
    FMF::Class create InitialNode -superclasses ControlNode
}
We can now define an instance model. In the following code, we first define an activity diagram using an Activity SyncModel. Next we define an initial node, three activity nodes, and a final node. Then we define edges for connecting these nodes in a sequential order. Finally we use a loop to add all edges and nodes in the SyncModel namespace to the SyncModel Activity.
UML2::Activity create SyncModel

UML2::InitialNode create Initial
UML2::ActivityNode create Read
UML2::ActivityNode create Invoke
UML2::ActivityNode create Write
UML2::ActivityFinalNode create Final
SyncModel node {Initial Read Invoke Write Final}

UML2::ActivityEdge create ae1 -source Initial -target Read
UML2::ActivityEdge create ae2 -source Read -target Invoke      
UML2::ActivityEdge create ae3 -source Invoke -target Write  
UML2::ActivityEdge create ae4 -source Write -target Final
SyncModel edge {ae1 ae2 ae3 ae4}
Now consider we want to add a stereotype to the meta-model that allows us to denote service invocations in activity diagrams. This can be done as follows:
FMF::Stereotype create InvokeProcessFunction -extends UML2::ActivityNode
FMF::Stereotype create ServiceInvocation -superclasses InvokeProcessFunction
ServiceInvocation attributes {
    serviceName String
}
This code introduces a new stereotype ServiceInvocation, as a subclass of a general stereotype for invocations. It has a tag value attribute for denoting the service name that is to be invoked. We can use this stereotype to extend the invocation activity node in the model defined above:
ServiceInvocation create InvokeService1 -serviceName "Service 1" -baseActivityNode Invoke
The complete resulting model is depicted below.

./images/UML_AD_Frag_Doc.gif

  index | contents | previous | next