opel — asynchronous expression language


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:


compile group: 'pl.allegro.tech', name: 'opel', version: '1.0.1'



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
. The main difference is that opel
always returns CompletableFuture:

String expression = "2 * 3 + 4";
OpelEngine engine = OpelEngineBuilder.create().build()
	.whenComplete((result, error) -> System.out.println(result));

this expression is transformed to equivalent code:

    .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
interface has to be implemented. It is an implementation of the well known adapter design pattern.

public class ExampleTemperatureFunction implements OpelAsyncFunction<BigDecimal> {
    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())

Then you can the evaluate expressions using the function:

String expression = "if (temperature('warsaw') > 25) 'Stay at home' else 'Go for a jog' ";
	.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.

returns the result of expressions parsing allowing re-use of it:

OpelEngine engine = OpelEngineBuilder.create().build();
OpelParsingResult parsingResult = engine.parse(expression);

	.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())
OpelParsingResult parsingResult = engine.parse(expression);

EvalContext context1 = EvalContext.Builder.create()
	.withValue("currentLocation", CompletableFuture.supplyAsync(() -> "Warsaw"))

parsingResult.eval(expression, context1)
	.whenComplete((result, error) -> System.out.println(result));

EvalContext context2 = EvalContext.Builder.create()
	.withCompletedValue("currentLocation", "London")

parsingResult.eval(expression, context2)
	.whenComplete((result, error) -> System.out.println(result));


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

Webmaster Forums Top AMP Questions


原型与你 - Weekly NO.38



opel — asynchronous expression language