Additional operators

Besides standard operators, Xtend has additional operators that help to keep the code compact.

Quite often, you will have to check whether an object is not null before invoking a method on it; otherwise, you may want to return null or simply perform no operation. As you will see in DSL development, this is quite a recurrent situation. Xtend provides the operator "?.", which is the null-safe version of the standard selection operator (the dot .). Writing o?.m corresponds to if (o != null) o.m. This is particularly useful when you have cascade selections, for example, o?.f?.m.

The Elvis operator ("?:") is another convenient operator for dealing with default values in case of null instances. It has the following semantics: x ?: y returns x if it is not null and y otherwise.

Combining the two operators allows you to set up default values easily, for example:

// equivalent to: if (o != null) o.toString else 'default'
result = o?.toString ?: 'default'

The with operator (or double arrow operator), =>, binds an object to the scope of a lambda expression in order to do something on it. The result of this operator is the object itself. Formally, the operator => is a binary operator that takes an expression on the left-hand side and a lambda expression with a single parameter on the right-hand side: the operator executes the lambda expression with the left-hand side as the argument. The result is the left operand after applying the lambda expression.

For example, see the following code:

return eINSTANCE.createEntity => [ name = "MyEntity"]

It is equivalent to:

val entity = eINSTANCE.createEntity
entity.name = "MyEntity"
return entity

This operator is extremely useful in combination with the implicit parameter it and the syntactic sugar for getter and setter methods to initialize a newly created object to be used in a further assignment without using temporary variables. As a demonstration, consider the Java code snippet we saw in Chapter 2, Creating Your First Xtext Language, that we used to build an Entity with an Attribute (with its type) that we will report here for convenience:

Entity entity = eINSTANCE.createEntity();
entity.setName("MyEntity");
entity.setSuperType(superEntity);
Attribute attribute = eINSTANCE.createAttribute();
attribute.setName("myattribute");
AttributeType attributeType = eINSTANCE.createAttributeType();
attributeType.setArray(false);
attributeType.setLength (10);
EntityType entityType = eINSTANCE.createEntityType();
entityType.setEntity(superEntity);
attributeType.setElementType(entityType);
attribute.setType(attributeType);
entity.getAttributes().add(attribute);

This requires many variables that are a huge distraction (are you able to get a quick idea of what the code does?). In Xtend, we can simply write the following:

eINSTANCE.createEntity => [
  name = "MyEntity"
  superType = superEntity
  attributes += eINSTANCE.createAttribute => [
    name = "myattribute"
    type = eINSTANCE.createAttributeType => [
      array = false
      length = 10
      elementType =  eINSTANCE.createEntityType => [
        entity = superEntity
      ]
    ]
  ]
]

Note

If you want to try the preceding code in the Xtend example project, you need to add as dependencies the bundles org.example.entities and org.eclipse.emf.ecore.

Polymorphic method invocation

Method overloading resolution in Java and in Xtend is a static mechanism, meaning that the selection of the specific method takes place according to the static type of the arguments. When you deal with objects belonging to a class hierarchy, this mechanism soon shows its limitation you will probably write methods that manipulate multiple polymorphic objects through references to their base classes, but since static overloading only uses the static type of those references, having multiple variants of those methods will not suffice. With polymorphic method invocation (also known as multiple dispatch or dynamic overloading), the method selection takes place according to the runtime type of the arguments.

Xtend provides Dispatch Methods for polymorphic method invocation; upon invocation, overloaded methods marked as dispatch are selected according to the runtime type of the arguments.

Going back to our Entities DSL of can write two dispatch methods as in the following example:

def dispatch typeToString(BasicType type) {
  type.typeName
}
def dispatch typeToString(EntityType type) {
  type.entity.name
}

Now, when we invoke typeToString on the reference elementType, the selection will use the runtime type of that reference:

def toString(AttributeType attributeType) {
  attributeType.elementType.typeToString
}

With this mechanism, you can get rid of all the ugly instanceof cascades and explicit class casts that have cluttered many Java programs.

Note that Xtend will automatically infer an entry point for dispatch methods with a parameter representing the base class of all the parameters used in the dispatch methods. In the preceding example, it will generate the Java method typeToString(ElementType) since ElementType is the base class of BasicType and EntityType. This generated Java method entry point will throw IllegalArgumentException if we pass an ElementType (that is, an ElementType object which is neither a BasicType nor an EntityType). For this reason, when writing dispatch methods, you may want to provide yourself a dispatch method for the base type and handle the base case manually.

Enhanced switch expressions

Xtend provides a more powerful version of Java switch statements. First of all, only the selected case is executed, in comparison to Java that falls through from one case to the next. For this reason, you do not have to insert an explicit break instruction to avoid subsequent case block execution. Indeed, Xtend does not support break statements at all. Furthermore, a switch can be used with any object reference.

Xtend switch expressions allow you to write involved case expressions, as shown in the following example:

def String switchExample(Entity e, Entity specialEntity) {
 switch e {
 case e.name.length > 0 : "has a name"
 case e.superType != null : "has a super type"
 case specialEntity : "special entity"
 default: ""
  }
}

If the case expression is a boolean expression (like the first two cases in the preceding example), then the case matches if the case expression evaluates to true. If the case expression is not of type boolean, it is compared to the value of the main expression using the equals method (the third case in the preceding example). The expression after the colon of the matched case is then evaluated, and this evaluation is the result of the whole switch expression.

Another interesting feature of Xtend switch expressions is type guards. With this functionality, you can specify a type as the case condition and the case matches only if the switch value is an instance of that type (formally, if it conforms to that type). In particular, if the switch value is a variable, that variable is automatically casted to the matched type within the case body. This allows you to implement a cleaner version of the typical Java cascades of instanceof and explicit casts. Although we could use dispatch methods to achieve the same goal, switch expressions with type guards can be a valid and more compact alternative.

For example, the code in the previous section using dispatch methods can be rewritten as follows:

def toString(AttributeType attributeType) {
 val elementType = attributeType.elementType
 switch elementType {
    BasicType: // elementType is a BasicType here
      elementType.typeName
    EntityType: // elementType is an EntityType here
      elementType.entity.name
  }
}

Note how entityType is automatically casted to the matched type in the case body.

Tip

Depending on your programming scenario, you may want to choose between dispatch methods and type-based switch expressions. Keep in mind that, while dispatch methods can be overridden and extended (that is, in a derived class, you can provide an additional dispatch method for a combination of parameters that was not handled in the base class), switch expressions are inside a method, and thus they do not allow for the same extensibility features. Moreover, dispatch cases are automatically reordered with respect to type hierarchy (most concrete types first), while switch cases are evaluated in the specified order.

Other Xtend expressions

Xtend also provides all the typical Java constructs such as for loops, if else, synchronized blocks, try, catch, and finally expressions and instanceof expressions. Recall that, although these have the same syntax as in Java, in Xtend these are considered expressions and not statements. Each of the preceding expressions evaluate to the last expression and can be returned just like any other expression.

Moreover, Xtend type inference is used also for the preceding expressions, thus, for instance, the type of the variable in a for loop can be omitted.

Casts in Xtend are specified using the infix operator as. Thus, the Xtend expression e as T corresponds to the Java cast expression (T) e.

When using instanceof as a condition of an if expression, Xtend automatically casts to the matched type within the body of the if branch. This is shown in the following example, where the casts are implicit and are not needed (Xtend will issue a warning about a useless cast if you insert the cast explicitly):

def toString(AttributeType attributeType) {
 val elementType = attributeType.elementType
 if (elementType instanceof BasicType)
    elementType.typeName // elementType is a BasicType here
 else 
if (elementType instanceof EntityType)
    elementType.entity.name // elementType is an EntityType here
}

Xtend introduces Active Annotations, which is a mechanism allowing the developer to hook in the translation process of Xtend source code to Java code through library. This is useful to have a lot of boilerplate automatically generated. We will not use active annotations in this book, but we invite you to have a look at the Xtend documentation about that.

The Xtend library provides some ready to use active annotations. For example, annotating an Xtend class with @Data will automatically add in the generated Java class getter methods for the fields, a constructor with parameters and other methods from java.lang.Object, such as equals, hashCode, and toString.

The Person class we used in Section Lambda expressions, can be implemented in Xtend as follows:

import org.eclipse.xtend.lib.annotations.Data

@Data class Person {
    String firstname
    String surname
 
int age
}

Xtend IDE

Xtend is obviously implemented with Xtext, thus its integration into Eclipse provides rich tooling features. In particular, Xtend provides the same IDE tooling of the Eclipse JDT. Here, we just mention a few features, and we encourage you to experiment further:

  • Rich content assist for all the existing Java libraries types and methods
  • Automatic import statement insertion during the content assistant
  • Organize Imports menu and keyboard shortcut (Ctrl + Shift + O)
  • Call Hierarchy view
  • Refactoring mechanisms, including Rename, Extract Variable, and Extract Method refactoring

Finally, you can debug Xtend code directly, as we will show in the next section.

Debugging Xtend code

The Java code generated by Xtend is clean and easy to debug. However, it is also possible to debug Xtend code directly (instead of the generated Java), thanks to the complete integration of Xtend with the Eclipse JDT debugger.

This means that you can debug Java code that has been generated by Xtend and, stepping through that, automatically brings you to debugging the original Xtend source. You can also debug an Xtend file containing a main method directly, since all the Run and Debug configuration launches are available for Xtend files as well. Breakpoints can be inserted in an Xtend file by double-clicking on the breakpoint ruler in the editor. The Debug context menu is available for Xtend files as well.

The next screenshot shows a debugging session of Xtend code. We have set a breakpoint on the Xtend file, which is also shown in the Breakpoints view. Note that all the JDT debugger views are available. Implicit variables such as it can be inspected in the Variables view:

If, for any reason, while debugging Xtend code you need to debug the generated Java code, you can do so by right-clicking on the Debug view on an element corresponding to an Xtend file line and selecting Show Source. Refer to the following screenshot:

Xtend expressions are indeed Xbase expressions. We will describe Xbase in Chapter 12, Xbase.