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 topublic
(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 andvar
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 useclass.classLoader
. In general, whenever there’s a method of the formobject.getSomething()
we can useobject.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 Stream
s and Function
s 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.