Implementing Domain-Specific Languages with Xtext and Xtend(Second Edition)
上QQ阅读APP看书,第一时间看更新

Xtend – a better Java with less "noise"

Xtend is a statically typed language and it uses the Java type system, including Java generics and Java annotations. Thus, Xtend and Java are completely interoperable.

Most of the linguistic concepts of Xtend are very similar to Java, that is, classes, interfaces, and methods. One of the goals of Xtend is to have a less "noisy" version of Java. Indeed, in Java, some linguistic features are redundant and only make programs more verbose.

The Xtend Eclipse editor supports the typical features of the Eclipse Java editor, including templates. Thus, we can create a main method inside the previously created Xtend class as shown in the following screenshot, using the content assist template proposal:

Let's write the "Hello World" print statement in Xtend:

package org.example.xtend.examples

class XtendHelloWorld {
 def static void main(String[] args) {
    println("Hello World")
  }
}

You can see that it is similar to Java, though the removal of syntactic noise is already evident by the fact that terminating semicolons (;) are optional in Xtend. All method declarations start with either def or override (explained later in the chapter). Methods are public by default.

Note that the editor works almost the same as the one provided by JDT. You may also want to take a look at the generated Java class in the xtend-gen folder corresponding to the Xtend class.

Although it is usually not required to see the generated Java code, it might be helpful, especially when starting to use Xtend, to see what is generated in order to learn Xtend's new constructs. Instead of manually opening the generated Java file, you can open the Xtend Generated Code view. The contents of this view will show the generated Java code, in particular, the code corresponding to the section of the Xtend file you are editing. This view will be updated when the Xtend file is saved.

Since the Xtend class we have just written contains a main method, we can execute it. Instead of running the generated Java class, we can directly execute the generated Java class by right-clicking on the Xtend file and navigate to Run As | Java Application. The output will be shown, as usual, in the Console view.

Types

Class and interface declarations in Xtend are similar to Java syntax, but all Xtend types are public by default. Moreover, in Xtend, you can declare multiple public top-level types per file, and they will be compiled into separate Java files. Inheritance and interface implementation are just the same as in Java. All Xtend classes implicitly extend java.lang.Object.

Package declarations in Xtend work as in Java.

In an Xtend class you can define any number of constructors, using the keyword new, without repeating the name of the class. Constructors support the same accessibility modifiers as in Java, but they are public by default:

class MyFirstXtendClass {
 new () {
 ...
  }
 new (String s) {
 ...
  }
}

Xtend supports also static nested classes and anonymous inner classes. Moreover, it supports annotation (see https://eclipse.org/xtend/documentation/204_activeannotations.html) and enum declarations. We will not use such declarations in this book though.

Methods

Method declarations start with either def or override and are public by default. The usual method Java modifiers are available in Xtend.

Xtend is stricter concerning method overriding: if a subclass overrides a method, it must explicitly define that method with override instead of def, otherwise a compilation error is raised. This will avoid accidental method overrides; that is, you did not intend to provide an overridden version of a method of a superclass. Even more importantly, if the method that is being overridden later is removed.

For example, the following method definition will raise a compiler error, since we should have used override instead of def:

class MyFirstXtendClass {
 def String toString() { // toString() is defined in java.lang.Object
 return ""
  }
}

In Xtend there are no statements, since everything is an expression. In a method body, the last expression is the return expression, and an explicit return statement is optional.

The return type of a method can be omitted if it can be inferred from the method body. Of course, if the return type is specified, then the method body return expression must be compliant with the specified return type.

For example, all the following method definitions are valid in Xtend:

class MyFirstXtendClass {
 def m1() {
 ""
  }
 def String m2() {
 ""
  }
 def m3() {
 return ""
  }
 def String m4() {
 return ""
  }
}

Tip

If your Xtend code is meant to be used as API, it is advisable to always specify the return type of the public methods explicitly. This will make your intentions clearer to your users and will avoid inadvertently breaking the API if you change the method body.

The type of method parameters must always be specified, since it cannot be inferred automatically.

Method parameters are always implicitly final in Xtend, and there is no way of specifying a method parameter as non-final.

Fields and Variables

Fields and local variables are declared using val (for final fields and variables) and var (for non-final fields and variables). Fields are private by default. Standard Java accessibility modifiers are available for fields. The type of fields and variables can be omitted and it is inferred from the context, not only from the initialization expression. Of course, final fields and variables must be initialized in the declaration. The standard Java syntax for field definition is also supported, but in this case the type must be declared explicitly.

Here are some examples of Xtend variable declarations:

val s = 'my variable' // final variable
var myList = new LinkedList<Integer> // non final variable, type inferred
val aList = newArrayList
aList += "" // now the type of aList is inferred as ArrayList<String>

Note that, in the preceding example, aList is inferred as ArrayList<String> from its usage, not from the initialization expression.

Tip

Text hovering the Xtend elements in the editor will give you information about the inferred types.

Operators

The semantics of Xtend operators differ slightly from Java. In particular == actually compares the values of the objects by mapping the operator to the method equals. To achieve the Java semantics of object identifier equality, you must use the triple equality operator: ===.

Standard arithmetic operators are extended to lists with the expected meaning. For example, when executing the following code:

val l1 = newArrayList("a")
l1 += "b"
val l2 = newArrayList("c")
val l3 = l1 + l2
println(l3)

The string [a, b, c] will be printed.

Syntactic sugar

Xtend provides some syntactic sugar, that is, syntax that is designed to write code which is easier to read, for getter and setter methods. For example, instead of writing, o.getName(), you can simply write o.name. Similarly, instead of writing o.setName("..."), you can simply write o.name = "...". The same convention applies for boolean fields according to JavaBeans conventions, where the getter method starts with is instead of get. Similar syntactic sugar is available for method invocations so that, when a method has no parameter, the parenthesis can be avoided.

Static members and inner types

Access to static members (fields and methods) of types is specified in Xtend, just like in Java, using the dot (.). Refer to the following code snippet:

import java.util.Collections

class StaticMethods {
 def static void main(String[] args) {
 val list = Collections.emptyList
    System.out.println(list)
  }
}

The same holds for inner types (classes and interfaces). For example, given this Xtend class, with an inner interface:

class MyXtendClass {
 interface MyInnerInterface {
 public static String s = "s";
  }
}

We can access the inner interface in Xtend using the following syntax:

MyXtendClass.MyInnerInterface

Note

In older versions of Xtend, access to static members had to be specified using the operator ::, for example, System::out.println(). Access to inner types had to be specified using the operator $, for example, MyXtendClass$MyInnerInterface. Since this syntax is still valid in Xtend, you may happen to see that in old Xtend programs.

Literals

Literals are specified in Xtend as in Java but for a few exceptions.

References to a type, that is, a type literal, is expressed in Xtend simply with the type name (instead, in Java, you need to use the class name followed by .class). Type literals can also be specified using the keyword typeof, with the type name as an argument, for example, typeof(String). For references to array types, you must use the latter syntax, for example, typeof(String[]).

In Xtend (and in general, by default, in any DSL implemented with Xtext using the default terminals grammar), strings can be specified both with single and double quotes. This allows the programmer to choose the preferred format depending on the string contents so that quotes inside the string do not have to be escaped, for example:

val s1 = "my 'string'"
val s2 = 'my "string"'

Escaping is still possible using the backslash character \ as in Java.

Xtend supports collection literals to create immutable collections and arrays. List or array literals are specified using the syntax #[ … ]. Whether a list or an array is created depends on the target type:

val aList = #["a", "b"] // creates a list of strings
val String[] anArray = #["a", "b"] // creates an array of strings

Immutable sets are created with the syntax #{…}. Finally, an immutable map is created like this:

val aMap = #{"a" -> 0, "b" -> 1} // creates a Map<String, Integer>

Extension methods

Extension methods is a syntactic sugar mechanism that allows you to add new methods to existing types without modifying them. Instead of passing the first argument inside the parentheses of a method invocation, the method can be called with the first argument as its receiver. It is as if the method was one of the argument type's members.

For example, if m(Entity) is an extension method, and e is of type Entity, you can write e.m() instead of m(e), even though m is not a method defined in Entity.

Using extension methods often results in a more readable code, since method calls are chained; for example, o.foo().bar() rather than nested, for example, bar(foo(o)).

Xtend provides several ways to make methods available as extension methods, as described in this section.

Xtend provides a rich runtime library with several utility classes and static methods. These static methods are automatically available in Xtend code so that you can use all of them as extension methods. They aim at enhancing the functionality of standard types and collections.

Of course, the editor also provides code completion for extension methods so that you can experiment with the code assistant. These utility classes aim at enhancing the functionality of standard types and collections.

Tip

Extension methods are highlighted in orange in the Xtend editor.

"my string".toFirstUpper

For example, the above code is equivalent to this code:

StringExtensions.toFirstUpper("my string")

Similarly, you can use some utility methods for collections, for example, head and last as in the following code:

val list =
newArrayList("a", "b", "c")
println(list.head) // prints a
println(list.last) // prints b

You can also use static methods from existing Java utility classes (for example, java.util.Collections) as extension methods using a static extension import in an Xtend source file, for example:

import static extension java.util.Collections.*

In that Xtend file, all the static methods of java.util.Collections will then be available as extension methods.

Methods defined in an Xtend class can automatically be used as extension methods in that class, for example:

class ExtensionMethods {
 def myListMethod(List<?> list) {
    // some implementation
  }
  
 def m() {
 val list = new ArrayList<String>
    list.myListMethod
  }

Finally, by adding the extension keyword to a field, a local variable, or a parameter declaration, its instance methods become extension methods in that class, code block, or method body, respectively. For example, assume you have this class:

class MyListExtensions {
  
 def aListMethod(List<?> list) {
    // some implementation
  }

 def anotherListMethod(List<?> list) {
    // some implementation
  }
}

Here you want to use its methods as extension methods in another class, C. Then, in C, you can declare an extension field (that is, a field declaration with the extension keyword) of type MyListExtensions, and in the methods of C, you can use the methods declared in MyListExtensions as extension methods:

class C {
  
 
extension MyListExtensions e = new MyListExtensions
  
 def m() {
 val list = new ArrayList<String>
    list.aListMethod // equivalent to e.aListMethod(list)
    list.anotherListMethod // equivalent to e.anotherListMethod(list)
  }

As mentioned earlier, you can achieve the same goal by adding the keyword extension to a local variable:

 
 def m() {
 val extension MyListExtensions e = new MyListExtensions
 val list = new ArrayList<String>
    list.aListMethod
    list.anotherListMethod
  }

Alternatively, you can add it to a parameter declaration:

 def m(extension MyListExtensions e) {
 val list = new ArrayList<String>
    list.aListMethod
    list.anotherListMethod
  }

When declaring a field with the keyword extension, the name of the field is optional. The same holds true when declaring a local variable with the keyword extension.

The implicit variable – it

You know that, in Java, the special variable this is implicitly bound in a method to the object on which the method was invoked. The same holds true in Xtend. However, Xtend introduces another special variable it. While you cannot declare a variable or parameter with name this, you are allowed to do so using the name it. If in the current program context a declaration for it is available, then all the members of that variable are implicitly available, just like all the members of this are implicitly available in an instance method, for example:

class ItExamples {
 def trans1(String it) {
    toLowerCase // it.toLowerCase
  }
  
 def trans2(String s) {
 var it = s
    toLowerCase // it.toLowerCase
  }
}

This allows you to write a much more compact code.

Lambda expressions

A lambda expression (or lambda for short) defines an anonymous function. Lambda expressions are first class objects that can be passed to methods or stored in a variable for later evaluation.

Lambda expressions are typical of functional languages that existed long before object-oriented languages were designed. Therefore, as a linguistic mechanism, they are so old that it is quite strange that Java has provided them only since version 8. Java 8 has been available for some time now, so we assume that you are familiar with Java 8 lambdas. Xtend has been supporting lambda expressions since the very beginning, and Xtend lambdas have a more compact form as we will show in the rest of this section.

Note

When using Java 8, Xtend translates its lambda expressions into Java lambda expressions. When using previous versions of Java, Xtend translates its lambda expressions into Java anonymous inner classes. This is all transparent for the end user.

Xtend lambda expressions are declared using square brackets []; parameters and the actual body are separated by a pipe symbol,|. The body of the lambda is executed by calling its apply method and passing the needed arguments.

The following code defines a lambda expression that is assigned to a local variable, taking a string and an integer as parameters and returning the string concatenation of the two. It then evaluates the lambda expression passing the two arguments:

val l = [ String s, int i | s + i ]
println(l.apply("s", 10))

Xtend also introduces types for lambda expressions (function types). Parameter types (enclosed in parentheses) are separated from the return type by the symbol =>. Generic types can be fully exploited when defining function types. For example, the preceding declaration could have been written with an explicit type as follows:

val (String, int)=>String l = [ String s, int i | s + i ]

Recall that Xtend has powerful type inference mechanisms—variable type declarations can be omitted when the context provides enough information. In the preceding declaration, we made the type of the lambda expression explicit, thus the types of parameters of the lambda expression are redundant since they can be inferred:

val (String, int)=>String l = [ s, i | s + i ]

Function types are useful when declaring methods that take a lambda expression as a parameter (remember that the types of parameters must always be specified), for example:

def execute((String, int)=>String f) {
  f.apply("s", 10)
}

We can then pass a lambda expression as an argument to this method. When we pass a lambda as an argument to this method, there is enough information to fully infer the types of its parameters, which allows us to omit these declarations:

execute([s, i | s + i])

A lambda expression also captures the local variables and parameters defined in the current program context. All such variables and parameters can be used within the lambda expression's body. Recall that in Xtend all method parameters are automatically final.

Thus, when evaluated, a lambda expression is closed over the environment in which it was defined: the referenced variables and parameters of the enclosing context are captured by the lambda expression. For this reason, lambda expressions are often referred to as closures.

For example, consider the following code:

package org.example.xtend.examples
class LambdaExamples {
 def static execute((String,int)=>String f) {
    f.apply("s", 10)
  }
 def static void main(String[] args) {
 val c = "aaa"
    println(execute([ s, i | s + i + c ])) // prints s10aaa
  }
}

You can see that the lambda expression uses the local variable c when it is defined, but the value of that variable is available even when it is evaluated.

Note

Formally, lambda expressions are a linguistic construct, while closures are an implementation technique. From another point of view, a lambda expression is a function literal definition while a closure is a function value. However, in most programming languages and in the literature, the two terms are often used interchangeably.

Although function types are not available in Java, Xtend can automatically perform the required conversions in the presence of Java SAM (Single Abstract Method) types, also known as Java functional interfaces. If a Java or Xtend method expects an instance of a SAM type, in Xtend, you can call that method by passing a lambda. Xtend will perform all the type checking and conversions.

For example, java.util.Collections.sort expects a List<T> and a Comparator<T>, which is a functional interface in Java 8, with the abstract method int compare(T o1, T o2). In Xtend, we can pass a lambda that is compliant with such method, just like in Java:

val list = newArrayList("Second", "First", "Third")
Collections.sort(list,
  [ arg0, arg1 | arg0.compareToIgnoreCase(arg1) ])

Xtend infers the types of the parameters of the lambda automatically.

Xtend provides some additional syntactic sugar for lambdas to make code even more readable.

First of all, when a lambda is the last argument of a method invocation, it can be put outside the (...) parentheses (and if the invocation only requires one argument, the () can be omitted):

Collections.sort(list)[arg0, arg1 |
  arg0.compareToIgnoreCase(arg1)]
strings.findFirst[ s | s.startsWith("F") ]

Furthermore, the special symbol it we introduced earlier is also the default parameter name in a lambda expression. Thus, if the lambda has only one parameter, you can avoid specifying it and instead use it as the implicit parameter:

strings.findFirst[ it.startsWith("F") ]

Since all the members of it are implicitly available without using ".", you can simply write the following:

strings.findFirst[startsWith("F")]

This is even more readable.

If the parameters of a lambda can be inferred from the context, you can avoid specifying the parameter names. In that case, the lambda parameters are automatically available in the shape of $0, $1, and so on. For example, the code invoking Collections.sort can also be written as follows:

Collections.sort(list)[ $0.compareToIgnoreCase($1)]

Sophisticated data processing queries are easier to express in Xtend than in Java, since there is no need to use Java 8 Streams. For example, suppose you have the following list of Person (where Person is a class with string fields firstname, surname, and an integer field age):

personList = newArrayList(
 new Person("James", "Smith", 50),
 new Person("John", "Smith", 40),
 new Person("James", "Anderson", 40),
 new Person("John", "Anderson", 30),
 new Person("Paul", "Anderson", 30))

Here, you want to find the first three younger persons whose first name starts with J, and we want to print them as surname, firstname on the same line separated by "; ", thus, the resulting output should be (note: ; must be a separator):

Anderson, John; Smith, John; Anderson, James

In Xtend, with lambdas and extension methods, it is as simple as follows:

val result = personList.filter[firstname.startsWith("J")].
  sortBy[age].
  take(3).
  map[surname + ", " + firstname].
  join("; ")
println(result)

Multi-line template expressions

Besides traversing models, when writing a code generator, most of the time you will write strings that represent the generated code. Unfortunately, in Java, you cannot write multi-line string literals.

This actually results in two main issues: if the string must contain a newline character, you have to use the special character \n; if, for readability, you want to break the string literal in several lines, you have to concatenate the string parts with +. If you want to indent the generated code nicely, then things become even harder.

If you have to generate only a few lines, this might not be a big problem. However, a generator of a DSL usually needs to generate lots of lines.

Xtend provides multi-line template expressions to address all of the preceding issues (indeed, all strings in Xtend are multi-line).

For example, let's assume that you want to write a generator for generating some Java method definitions. The corresponding code generator written in Xtend using multi-line template expressions is shown in the following screenshot:

Before explaining the code, we must first mention that the final output is nicely formatted as it was meant to be, including indentation:

public void m() {
  /* body of m */
  System.out.println("Hello");
  return;
}

Template expressions are defined using triple single quotes ('''). This allows us to use double quotes directly without escaping them. Template expressions can span multiple lines, and a newline in the expression will correspond to a newline in the final output. Variable parts can be directly inserted in the expression using guillemets («», also known as angle quotes or French quotation marks). Note that between the guillemets, you can specify any expression and even invoke methods. You can also use conditional expressions and loops (we will see an example later in this book; you can refer to the documentation mentioned earlier in the introduction for all the details).

Note

Curly brackets {} are optional for Xtend method bodies that only contain template expressions.

Another important feature of template expressions is that indentation is handled automatically and in a smart way. As you can see from the previous screenshot, the Xtend editor uses a specifi c syntax coloring strategy for multi-line template strings, in order to give you an idea of what the indentations will look like in the fi nal output.

Tip

To insert the guillemets in the Xtend Eclipse editor, you can use the keyboard shortcuts Ctrl + Shift + < and Ctrl + Shift + > for « and » respectively. On a Mac operating system, they are also available with Alt + q («) and Alt + Q (»). Alternatively, you can use content assist inside a template expression to insert a pair of them.

The drawback of guillemets is that you will have to have a consistent encoding, especially if you work in a team using different operating systems. You should always use UTF-8 encoding for all the projects that use Xtend to make sure that the right encoding is stored in your project preferences (which is in turn saved on your versioning system, such as Git). You should right-click on the project and then select Properties, and in the Resource property, set the encoding explicitly. You must set this property before writing any Xtend code (changing the encoding later will change all the guillemets characters, and you will have to fix them all by hand). Systems such as Windows use a default encoding that is not available in other systems, such as Linux, while UTF-8 is available everywhere. Refer to the following screenshot:

Note

All the projects generated by the Xtext wizard are already created with the UTF-8 encoding. The encoding can also be changed by changing the corresponding property in the MWE2 file.