Static Groovy and Concurrency: Type Inference in Action
Join the DZone community and get the full member experience.
Join For FreeMy previous article on static compilation of Groovy describes the idea of combining the expressiveness of Groovy with static type-checking to achieve the improved performance needed for writing concurrent applications, as well as catching more errors at compile time. So far the experiment of implementing this approach has made good progress. Very soon we hope to release a version available for community experiments.
What I want to talk about today is how Groovy helps to get rid of a lot of boilerplate code imposed by the traditional java.util.concurrent API
As a showcase I'll choose a very standard scenario, which I am sure a lot of people have met in practice
OK, let's start with the Java code that solves the problem. We define an abstract class representing our function and method future returning a FutureTask with 3 parameters - argument, executor and continuation. Because we are lazy and want to be able to omit irrelevant parameters, we also define two convenient overloads of futureTask:
We have a function with one parameter, which from time to time should be submitted for concurrent execution using java.util.concurrent.Executor
Sometimes we want to wait for result of the execution (java.util.concurrent.FutureTask is the perfect API for that and other tasks)
Sometimes we want just to be notified when the result is ready (or computation cancelled or crashed).We don't have it directly from FutureTask but can easily achieve it by overiding the FutureTask.done () method
public abstract class F<T,R> {
public abstract R apply (T arg);
public FutureTask<R> future (T arg) {
return future(arg, null, null);
}
public FutureTask<R> future (T arg, Executor executor) {
return future(arg, executor, null);
}
public FutureTask<R> future (final T arg, Executor executor, final F<FutureTask<R>,Object> continuation) {
final FutureTask<R> futureTask = new FutureTask<R> ( new Callable<R> () {
public R call() throws Exception {
return apply(arg);
}
}) {
protected void done() {
if (continuation != null)
continuation.apply(this);
}
};
if (executor != null)
executor.execute(futureTask);
return futureTask;
}
}
Truly speaking, it is not bad at all. In 28 lines of code we created a method with default parameters that does exactly what we need.
Let's now try to achieve the same with statically typed Groovy
abstract class F<T,R> {
abstract R apply (T param)
FutureTask<R> future (T arg, Executor executor = null, F<FutureTask<R>,Object> continuation = null) {
FutureTask<R> futureTask = [ 'super': { -> apply(arg) }, done: { continuation?.apply(this) } ]
executor?.execute(futureTask)
futureTask
}
}
And here we are - 9 lines of Groovy do the job.
I can imagine that someone will start arguing that I can use different formatting in Java code and compress the code into less lines. You know what - I will use my Java formatting settings from IntelliJ. We can compress these 28 lines to 18 lines below by paying price of making code totally unreadable.
public abstract class F<T,R> {
public abstract R apply (T arg);
public FutureTask<R> future (T arg) { return future(arg, null, null); }
public FutureTask<R> future (T arg, Executor executor) { return future(arg, executor, null); }
public FutureTask<R> future (final T arg, Executor executor, final F<FutureTask<R>,Object> continuation) {
final FutureTask<R> futureTask = new FutureTask<R> ( new Callable<R> () {
public R call() throws Exception { return apply(arg); }
}) {
protected void done() { if (continuation != null) continuation.apply(this); }
};
if (executor != null) executor.execute(futureTask);
return futureTask;
}
}
Anyway, I believe that the number of lines is not the most important parameter. What is really important is that we replaced huge amount of boilerplate code by concentrated minds - I mean code that says exactly what we want and nothing more.
But let me try to explain what's going on here. I assume that the meaning of default parameters is obvious, so I'll concentrate on the following 3 lines of Groovy code
FutureTask<R> futureTask = [ 'super': { -> apply(arg) }, done: { continuation?.apply(this) } ]
executor?.execute(futureTask)
futureTask
The last one is a very standard technique in Groovy - you don't need to specify return keyword on last statement of a method.
The line just before it is also absolutely standard - it is a famous Groovy safe call. Very roughly speaking object?.method(args) is shortcut for object != null ? object.method(args) : null
The statement above is not 100% true. In reality it involves the so called Groovy Truth, which is much more then just != null And
for statically typed Groovy default value is not necessary null, but in
case of primitive types it is the zero of relevant type. For example,
if a method returns double the default value will be 0.0d
The first line of the method future is the most interesting one
FutureTask<R> futureTask = [ 'super': { -> apply(arg) }, done: { continuation?.apply(this) } ]
It is the inference in action. We define a local variable and initialize it with Groovy map syntax. Let us try to follow the transformations done by compiler step by step and see that there is no magic involved
Because the variable is typed the compiler assumes an impilicit (FutureTask<R>) cast expression before map expression [..:..]
If vice versa we have an untyped variable initialized by a typed expression and the compiler will deduce the type of the variable. We will see an example of this later.
- Now the compiler has to create the new instance of FutureTask according to the definition given in the map expression. There are several decisions to take (and it is really impressive that the map syntax allows us to cover all posibilities below)
- Should FutureTask be created or some new subclass of it (like anonymous inner class)?
- What methods are overriden, what new properties or methods are defined and which constructor of the super class to use in case of subclass?
- What values should be set for old and/or new properties (if any) in both cases?
- The 'super' key immediately tells the compiler that we want to create subclass and call the superconstructor with arguments specified by the expression corresponding to super
We use small syntactic sugar here. The more formal way to specify parameters of superconstructor is
'super' : [ { -> apply(arg) } ] - notice the additional []. But because there is only one parameter we can omit the brackets.
- Now the compiler has to deduce what we mean by the closure expression { -> apply(arg) }
- As there is only one constructor of FutureTask with one parameter (of type java.util.concurrent.Callable) the compiler immediately understands that the closure expression should be compiled to Callable
Notice that the apply method is used inside our Callable. It is exactly the same thing that happens with anonymous inner classes. We just don't need to declare it as final (the compiler knows that itself) - a bit less noise in the code again.
- Callable is the 'one method' interface and parameters of the closure (no parameters) match the undefined method, so there is nothing easier than create anonymous inner class representing Callable.
- Now, after finishing with super, the compiler has to deal with done key/value pair. There are important decisions to be taken
- Does it define property or method?
- Is the property or method new or does it already exist?
- The logic is very natural as there is no existing property named done we are dealing with something new. It is an additional hint to compiler that we create a subclass instance and not FutureTask itself
If FutureTask had a property named done, the compiler would implicitly cast the value expression to the type of the property and set this property after an instance created
- Now when the compiler knows that it is dealing with either a new property or a new method, it takes a very simple decision - the closure expression defines the method and anything else defines the new property. So in our case done defines the method.
Imagine that we want to have additional property named uuid of type UUID as member of our subclass. All what we need to do is to add uuid : UUID.randomUUID() inside our map - the compiler will do the rest. uuid : (UUID)null would create uninitialized property (notice that cast is needed to define type of the property)
- Now compiler has a last but extremely important question - Do we want a new method or do we want to override the existing one? Of course, we want to overide existing one. And compiler has an easy way to realize that by parameters of closure expression { continuation.apply(this) }, which match with the existing method of FutureTask
- And now as it is an overriden method, it should have the same return type void as method of super class.
It is interesting to notice that informally { continuation.apply(this) } means { def it = null -> continuation.apply(this) }, if FutureTask had another method void done (SomeType x ) we would have potential collision (but we can always specify parameters of closure expression explicitly). Fortunately in our case everything works OK.
So we are done with our main example.
Before finishing the article I want to provide several more examples
Imagine that we decided to simplify our task and don't need to deal with Executor. Then the code becomes even more elegant (thanks to implicit return and automatic cast of the return value to the return type of the method)
FutureTask<R> future (T arg,compiler F<FutureTask<R>,Object> continuation = null) {
[ 'super': { -> apply(arg) }, done: { continuation?.apply(this) } ]
}
Another interesting way of using Groovy type inference is the following
void method (def param) {
if (param instanceof List && param.size() > 0 ) {
def sum = 0
for (element in (List<Integer>)param)
sum += element
}
else
return 0;
}
Three things happen here:
- The variable sum is untyped but as we assign it to int the compiler knows that it is an int when it used next time
- The condition param instanceof List allows us to use && param.size() > 0 without doing an explicit cast of param to List as we would do it in Java
- We don't need to define the type of for-loop for variable element because the compiler is able to deduct it from type of List
Opinions expressed by DZone contributors are their own.
Trending
-
[DZone Survey] Share Your Expertise for Our Database Research, 2023 Edition
-
What Is TTS and How Is It Implemented in Apps?
-
A Data-Driven Approach to Application Modernization
-
What Is JHipster?
Comments