Ricardo Rocha's Website

Musings on Programming and Programming Languages

Email GitHub Bitbucket Twitter LinkedIn

URL Shortener #3: The Xtend Main Implementation

How do we go about implementing the service’s Main class in Xtend? This series of articles contrasts the Java and Xtend languages around a very simple URL shortening REST service. Xtend is a JVM language that compiles into readable Java and is fully compatible with all Java frameworks, libraries and tools. If you know Java you already know most of Xtend! A presentation is also available

The Main Application

In order to get a quick first glimpse of how Java and Xtend compare syntactically, let’s see the Main application logic for both versions of our URL shortener:

The Java version is:

public static void main(String... args) {
  final String configFilename =
    configFromArgs(args); 

  final InputStream in =
    openConfigFile(configFilename);
  
  final UrlShortener urlShortener =
    buildUrlShortener(in, configFilename);
  
  startUrlShortener(urlShortener);
}
static void exit(String message) {
  System.err.println(message);
  System.exit(1);
}

and the Xtend version is:

static def main(String... args) {
  val configFilename =
    configFromArgs(args)

  val in = openConfigFile(configFilename)
  
  val urlShortener =
    buildUrlShortener(in, configFilename)
  
  startUrlShortener(urlShortener)
}
static def exit(String message) {
  System.err.println(message)
  System.exit(1)
}

Outstanding bits:

  • Xtend methods are introduced by the def keyword. Method access default to public (as do classes)
  • Xtend method return types can be omitted as they’re inferred from the last expression in the method body (void above)
  • Semicolons are optional in Xtend
  • Xtend variable types don’t have to be declared as the compiler infers their types from the right-hand side of each assignment
  • Xtend uses val for final, immutable variables and var for mutable ones

Zooming into our Main Methods

Let’s rewrite the above to inline all methods called by Main so we can contrast the two languages in greater detail:

The Java version is:

// The default configuration
// file/resource name
public static final String
  DEFAULT_CONFIG_NAME = "url-shortener.yaml";

public static void main(String... args) {
  // Get configuration file name from args
  final String configFilename;
  if (args.length == 0) {
    configFilename = DEFAULT_CONFIG_NAME
  } else {
    configFilename = args[0];
  }

  final InputStream in;
  if (new File(configFilename).exists()) {
  // We try to read our configuration from
  // the filesystem first
    in = new FileInputStream(configFilename);
  } else {
  // Failing that we try to read it as a
  // classpath resource
    in = Main.class.getClassLoader().
        getResourceAsStream(configFilename);
  }
  if (in == null) {
    System.err.println("Can't open config file: " + configFilename);
    System.exit(1);
  }

  final Yaml yaml = new Yaml();
  // Set private field properties
  // reflectively; no bean accessors
  yaml.setBeanAccess(BeanAccess.FIELD);

  UrlShortener urlShortener;
  try {
    // Load a ready-made, fully injected
    // instance of UrlShortener
    urlShortener =
      yaml.loadAs(in, UrlShortener.class);
  } catch (Exception e) {
    exit("Error parsing Yaml config file: " + e);
  }

  try {
    // Try and start the REST server
    urlShortener.start();
  } catch (Exception e) {
    exit("Error starting URL shortener");
  }

  // Ensure graceful shutdown
  // on ctrl-c or kill -1 
  Runtime.getRuntime().
    addShutdownHook(urlShortener::close);
}

and the Xtend versions is:

// The default configuration file/resource name
static val DEFAULT_CONFIG_NAME =
  "url-shortener.yaml"

static def main(String... args) {
  // Assignment from if/else expression!
  val configFilename =
      if (args.length == 0)
        DEFAULT_CONFIG_NAME
      else
        // List-style array element access
        args.get(0) 

  // Assignment from if/else expression
  val in =
      // Np parens for no-arg method "exists"
      if (new File(configFilename).exists)
        new FileInputStream(configFilename)
      else
        // Main.class.getClassLoader()
        // becomes Main.classLoader
        Main.classLoader.
          getResourceAsStream(configFilename)
  // Three equals for comparison with null
  if (in === null) { 
    // String interpolation with triple
    // quotes and guillemets
    exit('''Can't open file: «configFilename»''')
  }

  // Parens omitted for no-arg constructor
  val yaml = new Yaml => [ // "with" operator: => [ ... ]
    // Inside this block properties
    // and methods point to "yaml".
    // The below compiles to:
    //   yaml.setBeanAccess(BeanAccess.FIELD)
    beanAccess = BeanAccess.FIELD
  ]

  // Assignment from try!
  val urlShortener =
      try {
        yaml.loadAs(in, UrlShortener)
      } catch (Exception e) {
        exit('''Error in file «configFilename»: «e»''')
        throw e
      }

  try {
    // No parens for no-arg method "start"
    urlShortener.start
  } catch (Exception e) {
    exit("Error starting URL shortener")
  }

  // Xtend's way of approaching a method
  // reference
  Runtime.runtime.addShutdownHook(
    [new Thread[urlShortener.close])
}

Let’s dissect this logic:

No Statements: in Xtend Everything is an Expression

There are no statements in Xtend: everything is an expression. Yes: this includes if ... else and try!

Where Java uses separate assignments like in:

final String configFilename;
if (args.length == 0) {
  configFilename = DEFAULT_CONFIG_NAME
} else {
  configFilename = args[0];
}

Xtends assigns the immutable variable from an if/then expression:

val configFilename =
  if (args.length == 0)
    DEFAULT_CONFIG_NAME
  else
    args.get(0)

Note, incidentally, that Xtends treats arrays and lists uniformly. Element reference, in particular, is implemented by the get method instead of using square brackets. Square brackets are reserved for lambdas as explained below.

try/catch is also an expression that can be used for variable assignment.

Let’s suppose we try and open a file but, failing that, we want return a default classpath resource known to exist. In Java we’d say something like:

final InputStream in;
try {
  in = new FileInputStream(filename);
} catch (IOException ioe) {
  in = getClass().getClassResource().
    getResourceAsStream("default-resource.txt");
}

In Xtend, we’d say:

val in =
  try {
    new FileInputStream(filename)
  } catch (IOException ioe) {
    class.classLoader.
      getResourceAsStream("default-resource.txt")
  }

Note, again incidentally, that where Java uses getClass().getClassLoader() Xtend can also use class.classLoader. In general, whenever there’s a method of the form object.getSomething() we can use object.something with a “true” property syntax Java currently lacks.

Equality Checking

Java uses equals to ascertain equality. Identity is established with the == and != operators.

In Xtend, equality is ascertained with == and != while identity is established with the (triple) === and !== operators.

Where in Java we’d write:

if ("John".equals(name)) {
  System.out.println("Hey Jack!")
}

in Xtend we’d write:

if (name == "John") {
  println("Hey Jack!") // No "System.out"
}

For comparisons involving null, where in Java we say:

if (in == null) {
  exit(
    "Can't open config file: " + configFilename);
}

in Xtend we say:

// Triple equals for comparison with null
if (in === null) {
  exit(
    '''Can't open file: «configFilename»''')
}

String Interpolation

Note above the string interpolation notation used to assemble the error message. This syntax goes beyond simple string substitution to support smart indentation of multi-line strings.

Where in Java we’d write:

final int status = 404;
final String message = "Not found";

String xml =
  "<response>" +
  " <status>" + status + "</status>" +
  " <message>" + message + "</message>" +
  "</response>";

in Xtend we can also write:

val status = 404
// Single quotes OK for strings too
val message = 'Not found'
val xml = '''
  <response>
    <status>«status»</status>
    <message>«message»</message>
  </response>
'''

String interpolation with «guillemets» require triple quote string literals.

From the above we get exactly:

<response>
  <status>404</status>
  <message>Not found</message>
</response>

with no leading blanks: they’re smartly removed based on the first non-blank line indentation. Very handy for HTML templates in web applications!

Optional Empty Parenthesis

In general, methods and constructors taking no arguments can be invoked without appending empty parenthesis.

Thus, where in Java we write:

urlShortener.start();

in Xtend we can also write:

urlShortener.start

Lambda Syntax

Xtend uses a Smalltalk-inspired syntax for lambdas.

Where in Java we write:

List<Integer> evenNumbers =
  IntStream.of(1, 2, 3).
    map(number -> number * 2).
    boxed().
    // Yields [2, 4, 6]
    collect(Collectors.toList());

in Xtend we can also write:

// No need to stream, no need to collect
val evenNumbers =
  #[1, 2, 3].map[number | number * 2]

or, more concisely:

// 'it' stands for the current element
val evenNumbers =
  #[1, 2, 3].map[it * 2]

Of course, Java Streams and Functions can be used as well, even if they look somewhat verbose:

val evenNumbers =
  IntStream.of(1, 2, 3).
  map[number | number * 2].
  boxed.
  collect(Collectors.toList)

Note above that Xtend uses the square bracket notation for both Java and Xtend-style lambdas.

Collection Literals

In the previous example we used the literal #[1, 2, 3] to populate a list of integers. Xtend has dedicated syntax for (immutable) lists, sets and map literals:

// List<Integer>: Dups ok
val ints = #[1, 2, 3, 2, 1]
// Set<Double>: Dups removed if present
val reals = #{1.4142, 2.7178, 3.1416 }
// Map<String, Double>: Dups keys removed if present
val transcendentals =
  #{'pi' -> 3.1416, 'e' -> 2.7178}

Enough about Main! What about the URL shortener Implementation?

Lots of information from a seemingly short Main method!

We can now move on to our next post and see how our URLShortener and its dependencies are implemented in Xtend.

comments powered by Disqus