• Keine Ergebnisse gefunden

Getting more Declarative: Case Classes & Implicits

In this section, we show how case classes together with implicit conversions can be used to achieve a more declarative syntax. Therefore, in the next subsection, we first demonstrate how a more declarative element creation can be achieved. In the subsequent subsection, we demonstrate how powerful pattern matching abilities can be integrated in the MTL by the use of case classes. Finally, after having demonstrated why it is so useful to implicitly convert between case class objects and EMF model elements, we discuss how those conversions can be generated automatically from analyzing a metamodel.

4.3.1 Declarative Element Creation Using Case Classes

Many transformation rules only create a target model element and set its attributes.

Therefore, some MTLs provide features to create objects and immediately pass their attribute values along, instead of imperatively using according setter methods (as shown in listing 4.7, lines 6–8). ATL provides a declarative ‘to’ section (refer to listing 4.5 lines 8–10) as an alternative to the more imperative ‘do’ section, which we mimic with the

‘perform’ section in our Scala DSL. In Scala, a similar way for a more declarative object creation is the use ofcase classes (see Sec. 2.4.5). Instances of case classes can be created without newand their attribute values can be passed right along.

However, the classes that EMF generates from a metamodel are Java classes and Java does not have the concept of case classes. Furthermore, because EMF does not expose concrete metamodel class implementations directly, but only provides interfaces and factories for element creation, we cannot create model elements directly.

For being able to take advantage of Scala’s case classes, we generate a corresponding case class for each class defined in the target metamodel. In addition, we generate an implicit conversion for each case class, so that instances of these generated case classes are converted implicitly to their corresponding target model objects, using EMF’s factories in the conversion. The required code can be generated explicitly with a Scala script or with an Eclipse plug-in – we show this after the next subsection. By default, we name a case class like its corresponding metamodel class but with a ‘CC’ postfix.

Listing 4.8 shows a version of the Member2Female rule using a case class for simpler target object creation. Note the use of FemaleCCinstead of Femalein line 2. We further

4.3. Getting more Declarative: Case Classes & Implicits 91 shortened this syntax by overloading the performmethod with a variant which expects a function with just a single parameter.

Listing 4.8: Object creation using case classes

1 new Rule[Member, Female]

2 perform ((s) => FemaleCC(s.getFirstName() + " " + getFamilyName(s)))

Here, it is particularly helpful that Scala supports named and default parameters: In the example above, the target element creation in line 2 could alternatively be written as FemaleCC(fullName = s.getFirstName() + ...). This makes the instantiation of classes with many constructor parameters more manageable.

4.3.2 Pattern Matching

Pattern matching is a powerful Scala feature which is particularly useful for transfor-mation code with a lot of alternatives or null checks. To illustrate that, we first show a rule defined in ATL (Listing 4.9). It uses the Relations metamodel (Fig. 4.4). The rule is intended to extract the type of a column whose table’s name starts with"Customer". First, null checks for the involved attributes are required (line 7), and then the name of the owning table is tested. However, readability suffers from several nestedIfstatements, and a complex pattern structures can easily lead to missing cases. The presented ATL rule, for example, does not cover the case where type and owner are not null but the owner nameis incorrect.

Listing 4.9: An ATL rule to select the type of columns in tables called ‘Customer...’

1 rule ColumnTypeSelect {

2 from

3 c : Relational!Column

4 to

5 type : Relational!Type

6 do {

7 if(not c.type.oclIsUndefined() and not c.owner.oclIsUndefined()) {

8 if(c.owner.name.startsWith(’Customer’)) {

9 type.name <- c.type.name;

10 }

11 } else {

12 type.name <- ’unknown’;

13 }

14 }

15 }

With pattern matching, tasks like this are better manageable. However, pattern match-ing in Scala is only available on instances of case classes because here, the Scala com-piler automatically provides the required instance methods. To integrate Scala’s pattern matching into our MTL, we generate case classes and corresponding implicit conver-sions not only for the target metamodel but also for the source metamodel. This way, source model elements are implicitly converted into case class instances, and then pat-terns can be matched on those instances. Such match relies on the order of construc-tor parameters of the case class. We implemented this order to be alphabetic by the

92 Chapter 4. A Rule-Based Language for Unidirectional Model Transformation attributes’ name. For the example above, this results in the following case class con-structors: ColumnCC(keyOf, name, owner, type), TypeCC(name), and TableCC(cols, keys, name). Listing 4.10 shows a rule in our Scala MTL which is similar to the ATL rule above and which uses those case class constructors for deep pattern matching. This means that the pattern does not only specify attribute values of matched element itself but also of the attribute values of the elements contained in it. In our MTL, pattern matching is made available by using ‘use.matching’ instead of the perform keyword.

This way, the implicit conversion into case class instances is automatically triggered.

Listing 4.10: Pattern matching using generated case classes

1 new Rule[Column, Type].use.matching {

2 case ColumnCC(_, _, TableCC(_, _, name), t@TypeCC(_))

3 if name.startsWith("Customer") => t

4 case _ => TypeCC("unknown") // the default case

5 }

Scala’s pattern matching renders null checks on attributes unnecessary. For instance, the rule shown in Listing 4.10 will not fail if the column’stypeattribute is null. Instead, the default case (line 4) is triggered because a column with a null-valued typeattribute does not match the pattern specified in line 2; null is only matched by null (explicit null check) or by ‘_’ which matches on everything. Pattern matching therefore enables fine grained error handling. Each unsatisfying attribute occurrence can be addressed explicitly with a case statement. This allows the effective separation of error handling code and actual logic triggered by the desired input pattern.

Because of the way pattern matching is currently implemented in Scala, pattern match-ing code can become cluttered with occurrences of ‘_’ when a class has many attributes but only some of them are matched for. This is going to be improved when Scala’s sup-port for named parameters is not limited to case class creation anymore but is extended to case class matching. This is currently scheduled12 for Scala version 2.12.

4.3.3 Case Class Generation and Conversion

In the previous two subsections, we demonstrated that it is useful to implicitly convert between EMF model elements and case class objects, in order to achieve a higher ex-pressiveness of our Scala MTL. In this section, we first explain how the required case class definitions as well as the corresponding conversion functions can be generated au-tomatically from metamodels. Afterwards, we explain in more detail how the runtime conversion between model elements and case class objects is performed.

Generating Case Class Definitions and Conversions

To convert an EMF model element into a Scala case class instance, a corresponding case class definition has to exist. One could provide those definitions manually but this would be tedious and error-prone. We therefore generate case class definitions automatically from the metamodel. For each metamodel class, one case class definition and two implicit

12https://issues.scala-lang.org/browse/SI-5323

4.3. Getting more Declarative: Case Classes & Implicits 93 conversions need to be provided: one function which converts an EMF model element (an instance of a metamodel class) into a corresponding case class instance, and one function which converts a case class object into an EMF model element. In contrast to their corresponding metamodel classes, the case classes we generate do not define any methods, but only define public attributes – only those are needed for pattern matching and element creation. Because case classes cannot be inherited, we need to implement inheritance between metamodel classes by defining inherited attributes explicitly in every case class. In other words, the generated case classes merely define the data structures of their corresponding metamodel classes. Every generated case class implements the interface IGeneratedCC which is used to tag a class as being generated. This way, in the implementation of our MTL, methods working with case class instances can require this type and can abstract over concrete case class types, whose definitions may not be generated yet. Figure 4.5 shows how an inheritance hierarchy in the metamodel is translated into corresponding case class definitions.

Figure 4.5: Generating case class definitions from a metamodel

Because an EMF metamodel is an EMF model, too, we implement the case class generation and the implicit conversion generation as a model-to-text transformation by using our MTL itself. This way, the case class generation is a standard part of our MTL so that the MTL is self-contained, and only a Java runtime is needed to generate case classes and conversions. The case class generation transformation also allows for circular dependencies between metamodel classes. In addition to the transformation itself, we also provide a plug-in forEclipse, where a metamodel can be selected and the corresponding case class generation can be triggered. However, using this plug-in is optional as it creates a dependency of the internal DSL to one specific Scala tooling. Figure 4.6 shows how case class generation can be selected as a run configuration in Eclipse, if the plug-in is installed.

Converting Models to Case Class Objects

When a corresponding case class is defined for each metamodel class, converting from a model element to the corresponding case class object seems straight forward: A con-version of an object of type Female from the Families2Persons example could be de-fined as:implicit def female2femaleCC(f:Female) = FemaleCC(f.fullName). How-ever, as models are graphs which may have cross-references, circular dependencies can

94 Chapter 4. A Rule-Based Language for Unidirectional Model Transformation

Figure 4.6: An Eclipse plug-in provides a run configuration for case class generation occur. Implementing those conversions naively can therefore easily lead to the following problems: Either the compile-process will not terminate because it does not stop to in-sert calls to conversion functions, or the conversion-process at runtime will not terminate.

Therefore, we apply the following approach to implement the implicit conversions: The first time an implicit conversion is requested, the whole model – that is, the in-memory graph of EMF-based class instances – is converted to a graph of corresponding case class objects, which is then made available globally, so that subsequent conversion requests can simply return an already existing case class instance of that graph.

The algorithm, illustrated in Fig. 4.7, is as follows: (1) A reference to the model element whose conversion is requested is saved in the type-specific conversion function. (2) A general conversion function is called, which first uses EMF’s containment hierarchy to identify the root of that containment tree by walking up the hierarchy from the given element. (3) The containment tree is traversed for a first time and – by using the type-specific implicit conversion functions that we generated beforehand from the metamodel – for each model element a corresponding case class object is created and its attribute values are set accordingly. However, fields holding a cross-reference are only set with a type-safe placeholder because the corresponding case class instance of the referenced model element might not exist yet. Importantly, forward traces are kept globally, documenting which case class instance was created from which model element. (4) The model containment tree is traversed for a second time and – by looking up already created case class instances in the traces – placeholders in case class instances are resolved and cross-references in the case class graph are set. (5) The general conversion functions returns to the type-specific conversion function, which uses the created traces to return the corresponding case class instance of the originally passed model element.

After this conversion process was triggered once, every time an implicit conversion of a model element into a case class instance is requested, the conversion function only looks up the corresponding – already created – case class instance and returns it (i.e., only step 5). This way, circular dependencies can be handled. Furthermore, repeatedly con-verting the same model element in different transformation rules is prevented effectively.