Lambda functions are inline-defined anonymous functions that can be used as parameters in normal methods and class method calls.
Lambda functions with one parameter
Series and vectors, for instance, has an all method to check if all their numeric values satisfy an arbitrary condition. The condition is the only parameter of the method and must be passed as a lambda function. Let's say we have an aapl_prices persistent variable holding a series of prices. We can verify that all those prices are positive using this formula:
Austra
aapl_prices.all(x => x >= 0) -- It should return true.
The above formula checks whether all values in the price series are non-negative.
That's the role of the all method, which checks that all values in a series satisfies a
given predicate. The way we state the predicate to be satisfied is using this syntax:
Austra
x => x >= 0
This can be read as "given an arbitrary value x, check that it is non-negative". We can use all for any other purpose, such as checking that all values in a series lie inside the (0, 1) interval:
Austra
prices.all(value => 0 < value < 1)
Notice that in this new example, we have used another name for the "arbitrary given value": value instead of x. This renaming has no effect in the formula.
This example shows how to use the related method any:
Austra
prices.any(x => x >= 1)
In this case, we are checking whether exists at least one value in prices that is above 1.
Both any and all require a predicate as argument: a formula that given an arbitrary
value, returns true or false. The map method, instead, requires a more general
function that converts a real value into another one. Let's say we want to limit
values from a series, so that no one is greater than 1000:
Austra
prices.map(x => min(x, 1000))
In all cases, the type of the parameter of the lambda is determined by the method the lambda is passed, and so is the returned type. AUSTRA adds any required conversion, as when a double is required for the result and an integer expression is being returned. Regarding the name of the lambda's parameter, you can use any name you like, keeping in mind that it will shadow any predefined identifier inside the lambda function's body.
Function names as lambdas
In many cases, you need a lambda that takes a single parameter to transform it into another value from the same type. For instance, the sine function can be approximated using a spline over a uniform grid like this:
Austra
let s = spline(0, 2*pi, 1024, x => sin(x)) in
s[pi/4]
The above code can be shortened to this:
Austra
let s = spline(0, 2*pi, 1024, sin) in
s[pi/4];
Or even this if you need to qualify the function name for any reason:
Austra
let s = spline(0, 2*pi, 1024, math::sin) in
s[pi/4];
Since sin is a mono-parametric function and no parameters are supplied, the compiler understands that the function must be used to create a mono-parametric lambda, returning a real value.
Lambda functions with two parameters
Some methods require lambda arguments with more than one parameter. When a lambda requires two or more parameters, their names must be enclosed inside parenthesis, and must be separated by commas.
That is the case of the zip method, from series, vectors, and sequences, that combines two data samples into one:
Austra
aapl_prices.zip((x, y) => max(x, y))
zip can act on arguments with different lengths, so it only acts in the common part of both. It generates a new series, vector or sequence, and each item will be the combined value created by the lambda function. In the above example, it will be the maximum price for each common date.
Binary operators as lambdas
You can also use a binary operator as a shortcut for a lambda definition. This code uses the Reduce method on a sequence of integers for summing all items in the sequence:
Austra
iseq(1, 10).reduce(0, (x, y) => x + y)
You can substitute the lambda definition with a reference to the binary operator, including its class name:
Austra
iseq(1, 10).reduce(0, int::+)
This trick, so far, only works with binary operators.
Captured variables
The ncdf() method of a series takes a real value and classifies it according to its
position in the normal distribution implicitly defined by the series. By definition, it
is a value between 0 and 1. Even better, ncdf() is monotonic: if x < y, then
s.ncdf(x) < s.ncdf(y). All this means that this method is a nice way to compress an
arbitrary series, so all their values lie between 0 and 1, while preserving the shape of the series.
This formula does the trick:
Austra
aapl.map(x => aapl.ncdf(x))
Nothing remarkable here: aapl is a global identifier, and it should not surprise us
that we can use it both in the main formula and in the nested lambda. This is the
original series:
And this is the compressed series:
Please note that the main difference between both charts is the range of values.
What if what we really wanted was the compressed series with the simple returns
of prices? Not a big deal. This, obviously, works:
Austra
aapl.rets.map(x => aapl.rets.ncdf(x))
But we can do it much better, using a let clause:
Austra
let a = aapl.rets in
a.map(x => a.ncdf(x))
Though a is a local variable defined in the main body of the formula, we still can
reference it from our nested lambda function. This way, we avoid recalculating the returns of the series in the lambda's body.
Note
The series.ncdf(x) method assumes that values in the series can be described by a normal distribution. This is almost never true.
A most useful related method is series.movingNcdf(points), which calculates the ncdf for each value in the series, but calculates the two parameters that defines a normal distribution from a configurable interval of points preceding each calculation.
Nested lambdas
Another kind of capture takes place when a lambda function is defined inside another lambda. This formula finds all prime numbers up to 100, and uses nested lambdas:
Austra
iseq(2, 100).filter(x => iseq(2, x - 1).all(div => x % div != 0))
Note
The above code also uses sequences for generating a range or list of integers.
The underlined text is a definition of a lambda that is being used as the argument of the filter method. It's a function with a single parameter x. Note, however, that inside that lambda, we call another method that has its own lambda function, using the parameter div. The inner lambda can use both its own parameter div, but it also can use x, defined by the outer function.