While Software AG is busy adding features to Apama, the community is busy creating and publishing things as well. We maintain an index of the various community-created projects here. We hope that these community libraries and plug-ins can be useful for others and share them for people to use. Improvements and contributions are also very welcome.
This is part three of this series of community round-up blogs. You can find the previous blog post here, which looked at some EPL plug-ins. In this blog post we have a deeper dive into another EPL library providing a functional API to working with containers in EPL. It also gives a sneak peek into the upcoming lambdas feature in EPL.
Functional EPL
Today’s library is brought to you by Matthew Johnson and David Boyce and can be found, as always, on GitHub.
The library provides a selection of functional operations modelled similar to Python’s functools
or itertools
libraries, for example filter, map and reduce. These operate on EPL container types ( sequence
and dictionary
) and on generators provided by this library (see below). To help using these functional operators, there are also several functors and predicates provided within the library.
There are two APIs for accessing the functional operators. Firstly, all of the operators are provided as static functions on the com.apamax.functional.Fn
type. Each of these functions takes a container as its first argument and returns a new container with the new contents as the result, in each case using an any
as the type. For example, to filter a sequence of numbers for just even numbers:
sequence<integer> evens := <sequence<integer>> Fn.filter(numbers, Fn.even);
This example also shows the use of one of the functors, also provided on the Fn
event. You can use an action or action variable with the signature action<integer> returns boolean
, or (in later versions of Apama) a matching lambda. Some of these are provided within the library, but you can also write your own. You can combine several of these operations into a pipeline:
integer evenSum := <integer> Fn.reduce(Fn.filter(numbers, Fn.even), Fn.sum);
This will return the sum of all the even numbers within the numbers
container. The reduce function takes an additional first argument of the current value of the accumulator and returns the new value of the accumulator, so in this case the signature would be action<integer, integer> returns integer
.
If you are operating on a dictionary instead of a sequence, then you can use functions with one of two signature types. action<VALUETYPE> returns RETURNTYPE
signatures will be invoked with each value in turn (ignoring the keys). action<KEYTYPE, VALUETYPE> returns RETURNTYPE
signatures will be passed the key and the value in turn.
The second API is using the com.apamax.functional.Functional
type. This wraps your container and then provides the functional operators as instance methods, each one returning a new Functional
object. At the end of the chain you can either use an operator which directly returns a value, like reduce
, or you can call get
to return the underlying result object. For example:
sequence<integer> evens := <sequence<integer>> Functional(numbers).filter(Fn.even).get();
integer evenSum := <integer> Functional(numbers).filter(Fn.even).reduce(Fn.sum);
Functional
wraps all of the operators provided as static functions on Fn
. As you can see, you still use Fn
to access the predicates and functors to use with the operators.
A full list of all the operators and functors is available on GitHub.
Generators
The functional library also provides a concept of generators. Generators are objects which lazily calculate an infinite list. The simplest form of a generator is a current value and a functor which takes the previous value and calculates the next value. To get the next value you call the generate
function, which steps the generator and returns the next value. You can use most of the functional operators above and they will return another generator which lazily evaluates the function each time you step the resulting generator. To create a generator you can use the generator
static function on Fn
:
Generator g := Fn.generate((integer i) -> i+1);
print g.generate().toString(); // returns 1
print g.generate().toString(); // returns 2
g := <Generator> Fn.filter(g, Fn.even);
print g.generate().toString(); // returns 4
print g.generate().toString(); // returns 6
There are also several static functions which create pre-defined generators on Fn
. For example:
Generator g := Fn.count(); // increments from 0
Generator g := Fn.repeat("A"); // an infinite series of "A"
Each of these static functions also exist as a static function on Functional
which return a Functional
object which can have operators called on them fluently:
integer evenSum := <integer> Functional.count().filter(Fn.even).slice(0,10,1).reduce(Fn.sum); // sum of the first 10 even numbers
Lambdas
The library is fully compatible with the planned EPL lambdas feature, set to be released with Apama 10.7.2, so you can easily write your own functors and operators:
integer factor := 5;
Fn.map(container, (integer i)->i*factor);
EPL lambdas give you the ability to write anonymous functions, binding in local variables, and use them in any context you could use an action variable or a reference to an action on an event or monitor. The above is an example of a lambda which is a simple expression, where the return type is automatically inferred. You can also write lambdas as a full block of code:
(integer x, integer y) returns integer -> {
returns integer.max(x, y);
};
Lambdas are a powerful tool for writing clean and maintainable code and avoid the need to break flow to define small helper actions somewhere else. Look out for more details in an upcoming blog post.
Partial evaluation
While lambdas work very well with the functional style of writing code, it can be awkward to write generic functors for the functional API if you don’t have access to them. For people using Apama today, without lambda support, the functional API also provides an API for doing partial evaluation of functions. This allows you to take a function with multiple parameters, bind some of them and then return a new action which can be used as a functor. The above lambda argument to the map function could instead be written using partial evaluation as:
action mul(integer factor, integer i) returns integer { return factor * i; }
// ...
integer factor := 5;
Fn.map(container, Fn.partial(mul, factor));
Summary
The EPL Functional API provides several extremely useful tools for dealing with containers in a functional and a fluent way. More details are provided on GitHub and in the API documentation, including complete lists of all the provided functors, operators, functional APIs and generators. We hope that you check it out.
These are just a couple of the community activities going on outside of the official Software AG ecosystem. We hope they may prove useful. Please contact the authors via GitHub if you have any useful comments and as always feel free to ask questions via the apama tag on Stack Overflow.
Look out for the next blog post in this series,
Matt