Using lambda expressions in higher-order functions
A lambda expression — an anonymous function — is a function without a name. This is what a lambda expression looks like in ATL:
x -> x + 1
The x
on the left-hand side is an input parameter and x + 1
on the right-hand side is the output expression. An arrow ->
is always placed between the input and output of a lambda expression.
The above lambda expression adds one to the input number.
You cannot use a lambda expression on its own in ATL but certain higher-order functions such as map
, filter
, and sort
require a lambda expression as a parameter. We’re going to use these higher-order functions in this part of the tutorial.
The map
function loops over the items in a list (or an array), applying a lambda expression to each item in turn. Let’s see how this works:
[[map((1,2,3), x -> x + 1)]]
This map
function's first parameter is a list of numbers and its second parameter is our lambda expression that adds one to its input. It takes the first item in the list, adds one, then takes the second item, adds one, and so on. The output is a list in which each input number has been incremented by 1, that is (2,3,4). In preview, the output list is printed as 2, 3 and 4.
Why do we want to use the map
function, you ask? Well, we’re going to use it to loop over all the offices in our data so that we can then use the filter
function to pull out only the ones that had sales figures exceeding their targets.
For the map
function, the ATL we will use looks like this:
[[map(WholeJSON.offices, office -> office.name)]]
Does anything about the map
function seem familiar? You may recall that in an earlier step of this tutorial, we used the forAll
function in a similar fashion, to loop through all the offices. forAll
is another higher-order function. The map
function is similar to the forAll
function, but it does not require the name of a separate function to be applied to the input. Instead, you define a lambda expression to be used as the second parameter inside the call to map
.
Can you predict what the lambda expression, office -> office.name
, will do? Its input will be an item in the offices array and it outputs the value of that item’s name field. The result of mapping the lambda expression to the offices array will therefore be a list of office names.
In the TotalSales script, after “of our target”, add the following:
The best sales were in [[map(WholeJSON.offices, office -> office.name)]].
If you preview the script output, you’ll see the following:
At this point, what we’re saying is that all the offices had “the best sales”. Our narrative is inaccurate because we cannot say where the best sales were until we specify what we mean by “best”. For that, we’ll apply the
filter
function.First, let’s look at a simple
filter
function to understand how it works:[[filter((1,2,3), x -> x > 1)]]
Like
map
, thefilter
function’s second argument is a lambda expression, but this time the lambda expression, x -> x > 1, is used to filter the items in the input list. This lambda expression tests whether the item is greater than 1 so the output is the list (2,3). In Preview, this is printed as 2 and 3.The
filter
function we will apply is going to give us only the offices that exceeded their sales targets:[[filter(WholeJSON.offices, office -> office.sales > office.target)]]
The lambda expression is office -> office.sales > office.target. Its input is an item in the offices array and it only outputs the item if the value of the item’s sales field is greater than the value of its target field.
We’re going to use this
filter
function to filter the input for ourmap
function. We have to modify the ATL in the text that begins The best sales were in.In the snippet
[[map(WholeJSON.offices, office -> office.name)]]
, change the ATL to look like this:[[map(filter(WholeJSON.offices, office -> office.sales > office.target), office -> office.name)]]
Our
filter
function is now the first parameter ofmap
. The effect is to get an array of all the offices where sales exceeded the sales target. Themap
function then applies its lambda expression,office -> office.name
, to each item in the filtered array to get the names of the offices.If you preview the script output, you should see the following:
Great, it's clear we have applied a filter because we’ve narrowed the list of offices from six to three.
Now, let’s use
map
andfilter
again, this time to further highlight the top-performing offices. We’re also going to use a text function calledjoinStrings
. We usejoinStrings
to make a series of sales percentages appear as a comma-separated text phrase.At the end of our ATL in TotalSales (but before the period) add the following:
, which exceeded their sales targets by [[map(filter(WholeJSON.offices, office -> office.sales > office.target), office -> (percentage(office.sales-office.target,office.target)))]] respectively
What is this ATL saying? The first lambda expression — the “best sales” lambda — says “loop through the
offices
array and for each office that has sales above its target, give the office name.” The second lambda expression — the percentages lambda — says “loop through theoffices
array and for each office that has sales above its target, give the exceeded amount as a percentage. If you preview the output, you should see this:Notice that the percent symbol doesn’t appear after each percentage figure. We have to add it as an argument in the ATL.
Add the percent symbol as an argument after the
percentage
function, like this (addition in bold):(percentage(office.sales-office.target, office.target),"%"))]] respectively.
Preview the narrative.
Well, that didn’t go as planned, did it? The percent symbols are appearing, but the list shouldn’t have “and” before each one. How can we fix this? We need to use the
joinStrings
function.Wrap the
percentage
function in a call to thejoinStrings
function, like this (addition in bold):joinStrings(percentage(office.sales-office-target, office.target),"%"))]] respectively.
Preview the narrative.
Oh good, we fixed it! Now maybe we should tidy up those three percentages. It would be better if they appeared in descending numerical order (i.e. highest to lowest). We’ll also need to make sure their respective office locations get put into the same order. For this, we’ll use the
sort
function.With the
sort
function, we have to provide two arguments: 1) the list, column, or row to sort; and 2) a comparator function. Thesort
function uses either a predefined comparator function or a lambda comparator function to compare elements in a list for sorting them. Let’s look at two examples that sort a list of numbers:[[sort((2,1,3), numericSort())]]
[[sort((2,1,3), (x,y) -> sign(x - y))]]
The result in both cases is a list of the numbers in ascending order
(1,2,3)
, printed in Preview as 1, 2 and 3. The first sort uses the predefined function,numericSort
. The second uses a lambda function,(x,y) -> sign(x - y)
.What happens when the expression
(x-y) -> sign(x - y)
is evaluated? The comparisonsign(x - y)
returns 1 when x is greater than y, in which case the numbers are swapped. When x is less than y,sign(x - y)
returns -1 and the numbers are not swapped. Finally, when x is equal to y,sign(x - y)
returns 0 and the numbers are not swapped. This is repeated until no more swapping is required.Other predefined comparator functions are available for sorting numbers and strings. See the sort function.
We need to add the
sort
function in two places: one for the mention of office locations, and another for the mention of sales percentages. Add to your ATL so that the second paragraph of your TotalSales script looks like this (additions are highlighted in bold for easier reading):Total sales were [[currencyFormat(WholeJSON.total,'','¤#,###')]] across the month, which is [[percentage(WholeJSON.total,WholeJSON.target,'')]]% of our target. The best sales were in [[map(sort(filter(WholeJSON.offices, office -> office.sales > office.target), (a,b) -> sign(percentage(b.sales-b.target, b.target) - percentage(a.sales-a.target, a.target))), office -> office.name)]], which exceeded their sales targets by [[map(sort(filter(WholeJSON.offices, office -> office.sales > office.target), (a,b) -> sign(percentage(b.sales-b.target, b.target) - percentage(a.sales-a.target, a.target))), office -> joinStrings(percentage(office.sales-office.target, office.target),"%"))]] respectively.
What we’ve done with the above ATL is tell the system that we want it to sort the list of percentage figures (in both places within the sentence), with
a
andb
representing the sales figures and sales targets respectively.The reason this ATL is getting so lengthy is because we added two more lambda expressions, which do the math required so that the
sort
function can do its job.Preview the narrative. It should look like this:
In this step of the tutorial, we’ve learned:
What lambda expressions look like in ATL.
Use of the
map
,filter
, andsort
functions.Use of the
joinStrings
function.
Now let’s move on to find out how variables can simplify our writing of ATL.