Lambda: the new generation!


Java 8 brings lambdas. If you missed it here few samples:

// a method taking an interface with a single method
public void doRun(Runnable run) {
  // ....do something with run
}

// somewhere in code
doRun(() -> {
    // some java code
});
// or
doRun(this::methodWithNoParameter);

Of course you can also use parameters while they match. In 1 sentence we could say lambda are functions you can handle in java directly like class instances.

The most known use case for lambda is the new Java 8 stream integration:

List<String> list = asList("search string", "searched string", "searched string 2");
List<String result = list.stream()
  .filter(s -> s.contains("searched string")) // lambda to filter the stream
  .map(s -> new StringBuilder(s).reverse().toString()) // lambda to work on each item
  .collect(Collectors.toList());
// here result = ["gnirts dehcraes", "2 gnirts dehcraes"]

However lambda enables you to go further in term of API!

Now let suppose you create a framework using a fluent API based on lambda supporting some injections based on a registry. Here what it could look like:

public Demo {
    public void doTheDemo() {
        FlowBuilder.newFlow()
            .start((TransactionHandler tx) -> tx.start())
            .then((AService service, CurrentTaskDescriptor descriptor) -> service.checkpoint(descriptor.getCurrentTaskIndex()))
            .finallyDo((StatusHandler sh) -> sh.setStatus(SUCCESS));
    }
}

Just reading this example you can think it is easy when you know lambda parameters are just the single method parameters. However this second example should be valid as well for the case this post cares about:

public Demo2 {
    public void doTheDemo() {
        FlowBuilder.newFlow()
            .start((CurrentTaskDescriptor d) -> System.out.println(d.getCurrentTaskIndex()))
            .finallyDo((StatusHandler sh) -> sh.setStatus(PAUSE));
    }
}

So each step can get N (N between 0 and X) parameters and each parameter is looked up from the flow framework registry. In real world application it can be CDI, Spring, a custom one, or even a merge of multiple ones. Typically in previous examples AService would come from CDI or Spring and CurrentTaskDescriptor/StatusHandler/TransactionHandler from the flow framework.

The logic to lookup the data is very close to the one I described in a previous post for CDI case, just add few lines in the lookup implementation to handle built in types before CDI/Spring lookup and you are done. However the still open question is: how to get this API built?

What do we need?

  • functional interfaces with N parameters
  • add as many method as functional interfaces in the “service/builder” API

The first point is easy to solve. First create a template (I’ll use groovy simple templates in this post):

package io.github.rmannibucau.blog.lambda.api;

@FunctionalInterface
public interface ${className}<${generics}> {
    void apply(${params});
}

Then write few groovy to use this template to generate as many interfaces as needed. For it we’ll suppose we have a maven project and we put our previous template in ‘src/main/template/task.template’. Of course to stay maven friendly we’ll generate our API in target/generated-sources/api.

import groovy.text.SimpleTemplateEngine

def maxParams = 10 // our max parameter number
def engine = new SimpleTemplateEngine()

// tasks
def template = new File(project.basedir, 'src/main/template/task.template').text
def outputDir = new File(project.build.directory, 'generated-sources/api/io/github/rmannibucau/blog/lambda/api/')
outputDir.mkdirs()
(1..maxParams).each { i ->
  def plural = (i > 1 ? 's' : '')
  def className = "Task${i}Parameter${plural}"

  def generics = (1..i).collect({ idx -> 'T' + idx }).join(',')
  def params = (1..i).collect({ idx -> 'T' + idx + ' param' + idx }).join(',')
  def binding = [ "generics": generics, "params": params, "className": className ]
  new File(outputDir, "${className}.java").write(engine.createTemplate(template).make(binding).toString(), "UTF-8")
}

To execute this groovy script we can use groovy maven plugin:

<plugin>
  <groupId>org.codehaus.groovy.maven</groupId>
  <artifactId>gmaven-plugin</artifactId>
  <version>1.0</version>
  <executions>
    <execution>
      <id>create-functional-interfaces</id>
      <phase>generate-sources</phase>
      <goals>
        <goal>execute</goal>
      </goals>
      <configuration>
        <source><![CDATA[
          // the script
        ]]></source>
      </configuration>
    </execution>
  </executions>
</plugin>

And to attach generated sources to the build we use build-helper-maven-plugin:

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>build-helper-maven-plugin</artifactId>
  <version>1.9.1</version>
  <executions>
    <execution>
      <id>add-source</id>
      <phase>generate-sources</phase>
      <goals>
        <goal>add-source</goal>
      </goals>
      <configuration>
        <sources>
          <source>${project.build.directory}/generated-sources/api</source>
        </sources>
      </configuration>
    </execution>
  </executions>
</plugin>

Now we have all our functional interfaces but how to wire it to the “service/builder” without having a code too hard to maintain?

Personally I create a BuilderBase with a package scope where I put all my “unique” logic (ie all but the lambda methods) if relevant. Then we’ll do exactly the same as for the functional interfaces: we’ll generate the builder. For it we’ll modify a bit previous script to track class names and their associated generics (to avoid to recompute them):

// same as before
def names = []
(1..maxParams).each { i ->
  // same as before
  names[i] = [ "name": className, "generics": '<' + generics + '>' ]
}

Now we just need to create a single class with as much methods as needed. In our case we’ll simplify the FlowBuilder to have only a then() method for each functional interface (I don’t want to dig into the builder handling to forbid then/finally as starting task etc…):

So we create a ‘src/main/template/builder.template’:

package io.github.rmannibucau.blog.lambda.api.builder;

<% (1..max).each{ i -> %>import io.github.rmannibucau.blog.lambda.api.${names[i]["name"]};
<% } %>

public abstract class FlowBuilder extends FlowBuilderBase {
    <% (1..max).each{ i -> %>
    public ${names[i]["generics"]} void then(${names[i]["name"]}${names[i]["generics"]} task) {
        baseExecute(() -> invoke(task));
    }
    <% } %>
}

Side note: this suppose FlowBuilderBase has a then(Runnable) method and an invoke(lambda) method which handles the lambda invocation with parameters binding (see previous article on lambda and CDI for details).

Then we add to our generation script this code to create our builder:

def builderTemplate = new File(project.basedir, 'src/main/template/builder.template').text
def outputFile = new File(project.build.directory, 'generated-sources/api/io/github/rmannibucau/blog/lambda/api/builder/FlowBuilder.java')
outputFile.parentFile.mkdirs()
outputFile.write(engine.createTemplate(builderTemplate).make([ "max": maxParams, "names": names ]).toString(), "UTF-8")

And here we are. Just a “mvn compile” or “mvn package” and your API is ready to test!

What is nice about this solution is:

  • it is easy to make evolving
  • it generates sources as well (opposed to ASM based solution) which is important for an API, you can even generate some javadoc!
  • keep consistency between tasks (cause it is generated)

What is not that nice:

  • it is static (ie if my max parameter number is 10 then I can’t support 11 parameters) but this is by design
  • some part of the code (tasks and builder) is not in the project and needs generation which means you need to configure your IDE to generate them if not already done
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s