opel — asynchronous expression language
In everyday work programmers are facing various problems. We would like to focus on two of them: big systems with non-blocking API and specific business needs that can be solved using Expression Language. Hold tight!
How to use a non-blocking API in an expression language? The answer is the new expression language — opel
. It was designed
to enable writing simple, single line asynchronous expressions. It uses
Parboiled as a language grammar engine and Java 8
CompletableFuture.
Getting started #
To use opel
, just add this dependency to your project:
gradle #
compile group: 'pl.allegro.tech', name: 'opel', version: '1.0.1'
maven #
<dependency>
<groupId>pl.allegro.tech</groupId>
<artifactId>opel</artifactId>
<version>1.0.1</version>
</dependency>
And you’re ready to go!
Simple use case #
You can use opel
just as you would use any other existing expression language such as
SpEL or
JEXL. The main difference is that opel
always returns
CompletableFuture:
String expression = "2 * 3 + 4";
OpelEngine engine = OpelEngineBuilder.create().build()
engine.eval(expression)
.whenComplete((result, error) -> System.out.println(result));
this expression is transformed to equivalent code:
CompletableFuture.completedFuture(2)
.thenCombine(CompletableFuture.completedFuture(3), (l, r) -> l * r)
.thenCombine(CompletableFuture.completedFuture(4), (l, r) -> l + r)
.whenComplete((result, error) -> System.out.println(result));
As you see, opel
efficiently hides boilerplate of the CompletableFuture API.
Taste the asynchronous #
Typically, operations that use external systems through the REST API or database queries are the main source of Future
objects in the system. This type of calls can be easily integrated with opel
.
To achieve this, OpelAsyncFunction<T>
interface has to be implemented. It is an implementation of the well known
adapter design pattern.
public class ExampleTemperatureFunction implements OpelAsyncFunction<BigDecimal> {
@Override
public CompletableFuture<BigDecimal> apply(List<CompletableFuture<?>> args) {
return args.get(0).thenApply(city -> {
// add code to call external service about temperature in given city
return BigDecimal.valueOf(22);
});
}
}
That function can be added to OpelEngine
:
OpelEngine engine = OpelEngineBuilder.create()
.withFunction("temperature", new ExampleTemperatureFunction())
.build()
Then you can the evaluate expressions using the function:
String expression = "if (temperature('warsaw') > 25) 'Stay at home' else 'Go for a jog' ";
engine.eval(expression)
.whenComplete((result, error) -> System.out.println(result));
Get rid of parsing overhead #
Using Future API is the first step towards improving performance. Next step is reducing the frequency of expression parsing. Expression parsing is an expensive operation and should be avoided.
OpelEngine
returns the result of expressions parsing allowing re-use of it:
OpelEngine engine = OpelEngineBuilder.create().build();
OpelParsingResult parsingResult = engine.parse(expression);
parsingResult.evaluate()
.whenComplete((result, error) -> System.out.println(result));
parsingResult.evaluate() // no parsing, only evaluation
.whenComplete((result, error) -> System.out.println(result));
It is also worth mentioning that parsing result
object is stateless and thread-safe, and can be used
to evaluate expressions by many threads (as long as functions and values registered in the engine are thread-safe).
Evaluation context #
In all the above examples evaluation of the expression depends only on the expression itself and on registered functions and values.
Sometimes we may want to achieve a behaviour where every evaluation of parsed expression depends on current context.
Although it is not possible to pass arguments to opel expressions, it is possible to define a local evaluation context
object
and pass it to eval
method. All functions and values registered in evaluation context
will overwrite those defined in the engine.
This situation is common for web applications, where context contains functions and values related to current request
(eg. function or values returning information about signed in user).
String expression = "if (temperature(currentLocation) > 25) 'Stay at home' else 'Go for a jog' ";
OpelEngine engine = OpelEngineBuilder.create()
.withFunction("temperature", new ExampleTemperatureFunction())
.build();
OpelParsingResult parsingResult = engine.parse(expression);
EvalContext context1 = EvalContext.Builder.create()
.withValue("currentLocation", CompletableFuture.supplyAsync(() -> "Warsaw"))
.build()
parsingResult.eval(expression, context1)
.whenComplete((result, error) -> System.out.println(result));
EvalContext context2 = EvalContext.Builder.create()
.withCompletedValue("currentLocation", "London")
.build()
parsingResult.eval(expression, context2)
.whenComplete((result, error) -> System.out.println(result));
Conclusion #
In these few examples we present a vision of opel
and a way to write non-blocking expressions without boilerplate of Java 8
CompletableFuture API. But it is not the end. Opel
has many more features to write sexy expressions such as implicit
conversion, map and list concise notation, support for own values etc.
Since September 1st, opel
is open source and you can find more information about its features at
github.