Monday, June 18, 2012

Vert.x and Xtend

Vert.x is a framework for asynchonous, scalable, concurrent applications. It’s conceptually very similar to node.js but runs on the JVM leveraging its native support for multiple threads.

Its programming model is based on a tell don't ask paradigm, that is you never wait for responses (and thereby block the current thread), but tell what to do as soon as a response is available by passing a handler. It also isolates state (even static one) so no locking is required.

Vert.x comes with special APIs for Java, JavaScript, Ruby and Groovy. They also plan to support Scala and Python.

So what about Eclipse's Xtend?


Xtend is not just another JVM language but an alternative way to write Java applications. It translates to comprehensible Java source code and unlike other JVM languages Xtend is 100% interoperable with Java and is designed to work great with existing Java APIs. That’s why no special API for Xtend is required.

The language is statically typed and features very advanced IDE support which tightly integrates with Java projects. A release of Xtend with lots of cool new features (e.g. debugging support) is just a couple of days away (June 27).

Some Examples


I have converted the Java examples shipped with vert.x to Xtend. The code is on github and is a fully working Eclipse project including working launch configurations to run and debug all the examples. If you are used to working in Eclipse, this is an easy way to try Vert.x : Just clone & import (using the "Import existing Projects into workspace" wizard) and you are done (Thanks to Doug for explaining the setup).

In the following I want to compare a couple of Java code snippets with their equivalent Xtend code, to demonstrate the expressiveness.

Https Example


The https example consists of two classes one for the client and one for the server. The client simple executed a get request on port 4443 and the server 'localhost' and prints the received data to System.out. Here's the Java code:
// Java
@Override
public void start() {
  vertx.createHttpClient()
    .setSSL(true)
    .setTrustAll(true)
    .setPort(4443)
    .setHost("localhost")
    .getNow("/", new Handler<HttpClientResponse>() {
      public void handle(HttpClientResponse response) {
        response.dataHandler(new Handler<Buffer>() {
          public void handle(Buffer data) {
            System.out.println(data);
          }
        });
      }
    });
  }
The Java API allows to configure the HttpClient using chained calls to setter methods. The last call is the get-request (getNow) where the path as well as a response handler is provided. Instead of blocking the current thread and waiting for the response you just tell what to do when the response is available. The Java API expects an instance of Handler<HttpClientResponse> for that.

Xtend allows to use the very same API with the following code:
// Xtend
override start() {
  vertx.createHttpClient => [
    SSL = true
    trustAll = true
    port = 4443
    host = "localhost"
    getNow("/") [
      dataHandler [ data |
        println( data )
      ]
    ]
  ]
}
First an http client is created, instead of using the chained setters (we could), we use the with-operator (=>) which allows us to use and initialize the http client from the left hand side within a lambda expression (the block in squared brackets). Within the lambda the HttpClient is bound to the implicit variable 'it', which like the self reference 'this' can be omitted when used as a receiver.
Next we call the setters using assignments, that is the expression
    SSL = true
is translated and equivalent to
    it.setSSL(true)
The interesting part is how we pass the response handler in Xtend :
    getNow("/") [
      dataHandler [ data |
        println( data )
      ]
    ]
The call to getNow(String, Handler<HttpClientResponse>) is done by passing just the string in the parentheses and a lambda expression right after the call. You need to understand two things here:
  1. If the last argument of a feature call is a lambda expression it can be passed after the method call.
  2. A lambda expression automatically coerces to the expected target type if it's an interface with just one method (which is a common idiom not only in vert.x).
Within the response handling a data handler is registered by calling setDataHandler(Handler<Buffer>) on the response object. Note that since this method gets just one lambda expression passed you don't have to write the parenthesis (in Xtend empty parenthesis are optional). Also note, that this time we gave the parameter a name (data) since I found it sightly more readable than :
    getNow("/") [
      dataHandler [
        println( it )
      ]
    ]
The Java implementation of the server prints out the header keys to the console and returns a tiny document:
// JAVA
public void start() {
  vertx.createHttpServer()
    .setSSL(true)
    .setKeyStorePath("server-keystore.jks")
    .setKeyStorePassword("wibble")
    .requestHandler(new Handler<HttpServerRequest>() {
      public void handle(HttpServerRequest req) {
        System.out.println("Got request: " + req.uri);
        System.out.println("Headers are: ");
        for (String key : req.headers().keySet()) {
          System.out.println(key + ":" + req.headers().get(key));
        }
        req.response.headers().put("Content-Type", "text/html; charset=UTF-8");
        req.response.setChunked(true);
        req.response.write("<html><body><h1>Hello from vert.x!</h1></body></html>", "UTF-8").end();
      }
    }).listen(4443);
  }
Translated to Xtend and applying the same patterns we used for the client this looks like the following:
// Xtend
override start() {
  vertx.createHttpServer => [
    SSL = true
    keyStorePath = "server-keystore.jks"
    keyStorePassword = "wibble"
    requestHandler [
      println("Got request: " + uri)
      println("Headers are: ")
      for (it : headers.entrySet) {
        println(key + ":" + value)
      }
      response.headers.put("Content-Type", "text/html; charset=UTF-8")
      response.chunked = true
      response.write('''
        <html>
          <body>
            <h1>Hello from vert.x!</h1>
          </body>
        </html>
        ''', "UTF-8").end
      ]
    listen(4443) 
  ]
}
There are two things worth mentioning in addition to what we've discussed in the first example:
  • You can use the variable name it every where. For instance in a for loop like in the code snippet above.
  • Xtend supports multiline string literals. The common literals using single quote or double quote can be multiline as well, but by using triple single quotes you get smart whitespace handling. That is the indentation before the tag will be pruned for all lines, i.e. the result will be well formatted. Also the triple quotes allow for having interpolation expressions, which are not used in this example.

Can we do even more?


Although the standard Java API already works very well there's room for special Xtend API. Vert.x for instance communicates with JSON objects a lot, which you might want to declare easily. Xtend doesn't support native JSON syntax, but you can design powerful APIs using a combination of extension methods and operator overloading. I added just three methods to be able to construct instances of vert.x's JsonObjects like this:
class JsonExample extends Verticle {
  
  override start() {
    val eb = vertx.eventBus
    val pa = 'vertx.mongopersistor'
    val albums = _(
      _(
        'artist'- 'The Wurzels',
        'genre'- 'Scrumpy and Western',
        'title'- 'I Am A Cider Drinker',
        'price'- 0.99,
        'categories'- _('action', 'comedy')
      ),
      _(
        'artist'- 'Vanilla Ice',
        'genre'- 'Hip Hop',
        'title'- 'Ice Ice Baby',
        'price'- 0.01
      ),
      _(
        'artist'- 'Ena Baga',
        'genre'- 'Easy Listening',
        'title'- 'The Happy Hammond',
        'price'- 0.50
      ),
      _(
        'artist'- 'The Tweets',
        'genre'- 'Bird related songs',
        'title'- 'The Birdy Song',
        'price'- 1.20
      )
    )
    
    // First delete everything
    eb.send(pa, _('action'- 'delete', 'collection'- 'albums', 'matcher'- _()))
    eb.send(pa, _('action'- 'delete', 'collection'- 'users', 'matcher'- _()))
    
    // Insert albums - in real life price would probably be 
    // stored in a different collection, but, hey, this is a demo.
    
    for (album : albums) {
      eb.send(pa, _(
        'action'- 'save',
        'collection'- 'albums',
        'document'- album
      ))
    }
    
    // And a user
    eb.send(pa, _(
      'action'- 'save',
      'collection'- 'users',
      'document'- _(
        'firstname'- 'Tim',
        'lastname'- 'Fox',
        'email'- 'tim@localhost.com',
        'username'- 'tim',
        'password'- 'password'
      )
    ))
  }  
}
These are the three extension methods:
  def static <T> Pair<String,T> operator_minus(String key, T value) {
    new Pair(key, value)
  }
  
  def static JsonArray _(Object ... entries) {
    val result = new JsonArray
    for (e : entries) {
      switch e {
        String : result.addString(e)
        Number : result.addNumber(e)
        Boolean : result.addBoolean(e)
        JsonObject : result.addObject(e)
        JsonArray : result.addArray(e)
      }
    }
    return result
  }
  
  def static JsonObject _(Pair<String, ?> ... entries) {
    val result = new JsonObject
    for (e : entries) {
      switch value : e.value {
        String : result.putString(e.key, value)
        Number : result.putNumber(e.key, value)
        Boolean : result.putBoolean(e.key, value)
        JsonObject : result.putObject(e.key, value)
        JsonArray : result.putArray(e.key, value)
      }
    }
    return result
  }

2 comments:

  1. Please explain how to actually import. I have never used Xtend before and it looks interesting but all I'm presented with is a bunch of errors sayin `EchoClient`, `EchoServer` .. is already defined. Seems like the xtend-gen folder is causing these errors.

    ReplyDelete
  2. Do you have the latest build of Xtend installed?

    Use the latest from the Juno updatesite : http://download.eclipse.org/releases/juno/

    If you have further questions please ask on the google group:
    https://groups.google.com/forum/?hl=de&fromgroups#!forum/xtend-lang

    ReplyDelete