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.