Apama EPL Classes for functional operations
This works with Apama 10.5 or later, and probably most of the earlier ones as well, although only later versions support lambdas. With previous versions you need to define explicit actions to use instead.
This 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 any action or action variable with the signature action<integer> returns boolean
, or (in later versions of Apama) a matching lambda. 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.
Here is a list of the operators provided on Fn
and Functional
.
Operator | Arguments | Return | Description |
---|---|---|---|
filter | sequence, dictionary or generatoraction<TYPE> returns boolean or action<KEY, VALUE> returns boolean |
Fn: A new container of the same type as the input container Functional: a new Functional |
Filters the container to only have the elements where the provided predicate is true. |
map | sequence, dictionary or generatoraction<TYPE> returns NEWTYPE or action<KEY, VALUE> returns NEWTYPE |
Fn: A new container of the same sort as the input container, but the type returned from the functor Functional: a new Functional |
Uses the functor to replace all the values in the container with new values. Keys in dictionaries are unchanged. |
reduce | sequence, or dictionaryaction<RESULT, TYPE> returns RESULT or action<RESULT, KEY, VALUE> returns RESULT |
The result of calling the functor across all of the values | Repeatedly calls a functor on each value, using the output of functor to update an accumulator passed to the next call and returning the final result. The first call will be passed a default-initialized RESULT type. |
reduceFrom | sequence, or dictionary Initial value for the reduction action<RESULT, TYPE> returns RESULT or action<RESULT, KEY, VALUE> returns RESULT |
The result of calling the functor across all of the values | Repeatedly calls a functor on each value, using the output of functor to update an accumulator passed to the next call and returning the final result. The first call will be passed the initial value. |
accumulate | sequence, dictionary or generatoraction<RESULT, TYPE> returns RESULT or action<RESULT, KEY, VALUE> returns RESULT |
Fn:A generator which will iterate over the results Functional:A new Functional |
Repeatedly calls a functor on each value, using the output of functor to update an accumulator passed to the next call and returning each result in turn. The first call will be passed a default-initialized RESULT type. |
accumulateFrom | sequence, dictionary or generator Initial value for the accumulation action<RESULT, TYPE> returns RESULT or action<RESULT, KEY, VALUE> returns RESULT |
Fn:A generator which will iterate over the result Functional:A new Functional |
Repeatedly calls a functor on each value, using the output of functor to update an accumulator passed to the next call and returning each result in turn. The first call will be passed a default-initialized RESULT type. The first call will be passed the initial value. |
argmap | sequence, or generatoraction<TYPE...> returns NEWTYPE |
Fn: A new container of the same sort as the input container, but the type returned from the functor Functional: a new Functional |
Each item in the input is treated as an argument, or sequence or arguments, for the functor. The result will be a container with the results of calling the functor on those arguments |
slice | sequence, or generator Start offset (0+) End offset(0+, or -1 for the whole sequence) Distance to increment each time(1+) |
Fn:A sequence containing the selected elements Functional: a new Functional |
Selects a subset of a sequence, or generator. Immediately consumes enough of the generator to create a concrete sequence |
Fn
also provides some predicates to use with filter
:
Predicate | Description |
---|---|
_not | Inverts another predicate. eg: Fn.filter(numbers, Fn._not(Fn.even)) |
istrue | True if a boolean is true |
even | True if an integer is even |
odd | True if an integer is odd |
negative | True if an integer, float or decimal is less than 0 |
positive | True if an integer, float or decimal is greater than 0 For including 0 use Fn._not(Fn.negative) |
whole | True if a float or decimal does not have a fractional part. Always true for integers |
_any | True if a container of booleans contains at least one True False for the empty container |
_all | True if a container of booleans contains no False True for the empty container |
Fn
also provides some functors to use with map
, reduce
and accumulate
:
Functor | Operand type(s) | Description |
---|---|---|
increment | integer | Increments to the next integer |
sum | integer, float or decimal | Add up all the values |
mul | integer, float or decimal | Calculate the product of the values |
concat | string | Concatenates all the strings |
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
Here is the list of all the generator functions on Fn
and Functional
:
Generator | Arguments | Returns |
---|---|---|
generator | action<TYPE> returns TYPE |
Returns the result of calling the functor on the previous value, starting from a default-initialized TYPE. |
generatorFrom | Initial valueaction<TYPE> returns TYPE |
Returns the result of calling the functor on the previous value, starting from the initial value. |
count | None | A sequence of increasing integers starting at 1. |
repeat | A value | The given value repeated infinitely. |
cycle | A sequence | Generates each value in the sequence in turn, going back to the first element after completing the sequence. |
range | Start integer End integer (End>start) Number to skip each time (1+) |
Returns a finite sequence, not a generator, of the numbers in the given range. |
sequenceOf | A value The number of times to repeat the value |
Returns a finite sequence, not a generator, containing the given value a given number of times. |
Generators can be wrapped in a Functional
and the fluent methods called on it directly. If you have a generator in a Functional
, the resulting object has a generate
method which can be called to step the underlying generator and return the value, without having to call get
first.
Although there exists a Generator
type for generators returned from built-in functions, any event which has an action generate() returns TYPE
can be used in any context you can use a generator.
If you are using a version of Apama without lambdas, then you can use action variables instead. However, if you want to capture local variables, while that's simple in a lambda:
integer factor := 5;
Fn.map(container, (integer i)->i*factor);
To use an action you would need to write an event type to wrap the local you wanted to capture and provide the function as an action on it:
event Multiplier { integer factor; action func(integer i) returns integer { return factor * i; } }
...
integer factor := 5;
Fn.map(container, Multiplier(5).func);
As an alternative, Fn
provides a function to partially satisfy function arguments and return an object which can be used in the place of a functor to later evaluate with the full arguments. For example:
action mul(integer factor, integer i) returns integer { return factor * i; }
...
Fn.map(container, Fn.partial(mul, 5));
The partial stores the first argument and then map is called with the second argument, evaluating the function once all arguments are available.
You can also store and directly execute a partially evaluated function with the exec
function:
Partial p := Fn.partial(mul, 5);
p.exec(3);
You can also chain partials and stash multiple arguments using a sequence:
Fn.partial(fn, [1,2,3]).partial(4).exec(5); // executes fn(1,2,3,4,5)
The library is provided as a single EPL file. To use this library from your EPL code, simply import the file Functional.mon
into your project, then add the types to your EPL file with:
using com.apamax.functional.Fn;
using com.apamax.functional.Functional;
using com.apamax.functional.Generator;
using com.apamax.functional.Partial;
Then you can just start using functional operations in your code.
Several tests for the functional operators are included with this repository. To run the tests you will need to use an Apama command prompt to run the tests from within the tests
directory:
pysys run
API documentation can be found here: API documentation