Getting Started

The first thing you want to see in any language is a "Hello World" example. In Xtend, that reads as

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

You see that Xtend looks a lot like Java. At a first glance the main difference seems to be the def keyword to declare a method. Also like in Java it is mandatory to define a class and a main method as the entry point for an application. Admittedly 'hello world' programs are not a particular strength of Xtend. The real expressiveness is unleashed when you do real stuff as you will learn in a second.

An Xtend class resides in a plain Java project. As soon as the SDK is installed, Eclipse will automatically translate it to Java code. By default you will find it in a source folder xtend-gen, which is of course configurable. The hello world example is translated to the following Java code:

// Generated Java Source Code
import org.eclipse.xtext.xbase.lib.InputOutput;

public class HelloWorld {
  public static void main(final String[] args) {
    InputOutput.<String>println("Hello World");
  }
}

The only surprising fact in the generated Java code may be the referenced library class InputOutput (src). It is provided by the runtime library and only one nice utility that is handy when using expressions.

You can put an Xtend class into a source folder of any Java project within Eclipse (or any Maven project). Eclipse will complain about the missing library if it is not on the classpath and provide a quick fix to add it.

The next thing you might want to do is materializing one of the example projects into your workspace. To do so right click anywhere in the Navigator view in Eclipse and select New -> Example....

In the upcoming dialog you will find two examples for Xtend:

The Movies Example

The movies example is included in the example project Xtend Introductory Examples (src/examples6/Movies.xtend) and is about reading a file with data about movies and doing some analysis on it.

The Data

The movie database is a plain text file (data.csv) with data sets describing movies. Here is an example data set:

Naked Lunch  1991  6.9  16578  Biography  Comedy  Drama  Fantasy

The values are separated by two spaces. The columns are :

  1. title
  2. year
  3. rating
  4. numberOfVotes
  5. categories
Let us define a data type Movie representing a data set:

@Data class Movie {
  String title
  int year
  double rating
  long numberOfVotes
  Set<String> categories 
}

It is a plain class with a typed field for each column in the data sets. The @Data annotation will turn this class into a value object, that is it will get

Parsing The Data

Let us now add another class to the same file (any number of classes per file is allowed) and initialize a field called movies with a list of movies. For the initialization we read in the text file and turn the data sets into Movies:

import java.io.FileReader
import java.util.Set
import static extension com.google.common.io.CharStreams.*

class Movies {
  
  val movies = new FileReader('data.csv').readLines.map [ line |
    val segments = line.split('  ').iterator
    return new Movie(
      segments.next, 
      Integer::parseInt(segments.next), 
      Double::parseDouble(segments.next), 
      Long::parseLong(segments.next), 
      segments.toSet
    )
  ]
}

A field's type can be inferred from the expression on the right hand-side. That is called local type inference and is supported everyhwere in Xtend. We want the field to be final, so we declare it as a value using the keyword val.

The initialization on the right hand side first creates a fresh instance of FileReader. Then the method readLines() is invoked on that instance. But if you have a look at FileReader you will not find such a method, because readLines() is in fact a static method from Google Guava's CharStreams which was imported as an extension which allows us to use this readable syntax.

import static extension com.google.common.io.CharStreams.*

CharStreams.readLines(Reader) returns a List<String> on which we call another extension method called map. That one is defined in the runtime library (ListExtensions.map(...) (src)) and is always imported and therefore automatically available on all lists. The map extension expects a function as a parameter. It basically invokes that function for each value in the list and returns another list containing the results of the function invocations.

Function objects are created using lambda expressions (the code in squared brackets). Within the lambda we process a single line from the text file and turn it into a movie by splitting the string using the separator (two whitespaces) and calling iterator on the result. As you might know String.split(String) returns a string array (String[]), which Xtend auto-convertes to a list when we call Iterable.iterator() on it.

val segments = line.split('  ').iterator

Now we use the iterator to create an instance of Movie. The data type conversion (e.g. String to int) is done by calling static methods from the wrapper types. The rest of the iterable is turned into a set using the extension method IteratorExtensions.toSet(Iterator<T>) (src) and contains all the categories the movie is associated with.

return new Movie (
  segments.next, 
  Integer::parseInt(segments.next), 
  Double::parseDouble(segments.next), 
  Long::parseLong(segments.next), 
  segments.toSet
)

Answering Some Questions

Now that we have turned the text file into a List<Movie>, we are ready to execute some queries against it. We use JUnit to make the individual analysis executable.

Question 1 : What Is The Number Of Action Movies?

@Test def numberOfActionMovies() {
  assertEquals(828, 
    movies.filter[ categories.contains('Action') ].size)
}

First the movies are filtered. The lambda expression checks whether the current movie's categories contain the entry 'Action'. Note that unlike the lambda we used to turn the lines in the file into movies, we have not declared a parameter name this time. We could have written

movies.filter[ movie | movie.categories.contains('Action') ].size

but since we left out the name and the vertical bar the variable is automatically named it which (like this) is an implicit variable. That is why we can write either

movies.filter[ it.categories.contains('Action') ].size

or

movies.filter[ categories.contains('Action') ].size

Eventually we call size on the resulting iterable which also is an extension method defined in IterableExtensions (src).

Question 2 : What Is The Year The Best Movie From The 80ies Was Released?

@Test def void yearOfBestMovieFrom80ies() {
  assertEquals(1989, 
    movies.filter[ (1980..1989).contains(year) ].sortBy[ rating ].last.year)
}

Here we filter for all movies whose year is included in the range from 1980 to 1989 (the 80ies). The .. operator is again an extension defined in IntegerExtensions (src) and returns an instance of IntegerRange (src). Operator overloading is explained in section.

The resulting iterable is sorted (IterableExtensions.sortBy (src)) by the rating of the movies. Since it is sorted in ascending order, we take the last movie from the list and return its year.

We could have sorted descending and take the head of the list as well:

movies.filter[ (1980..1989).contains(year) ].sortBy[ -rating ].head.year

Note that first sorting and then taking the last or first is slightly more expensive than needed. We could have used the method reduce instead to find the best movie which would be more efficient. Maybe you want to try it on your own?

The calls to movie.year as well as movie.categories in the previous example in fact access the corresponding getter methods.

Question 3 : What Is The The Sum Of All Votes Of The Top Two Movies?

@Test def void sumOfVotesOfTop2() {
  val long sum = movies.sortBy[ -rating ].take(2).map[ numberOfVotes ].reduce[ a, b | a + b ]
  assertEquals(47_229L, sum)
}

First the movies are sorted by rating, then we take the best two. Next the list of movies is turned into a list of their numberOfVotes using the map function. Now we have a List<Long> which can be reduced to a single Integer by adding the values.

You could also use reduce instead of map and reduce. Do you know how?