Skip to main content

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.

  1. 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:

    JSONtutorial-Lambda_1st_output.png

    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, the filter 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 our map function. We have to modify the ATL in the text that begins The best sales were in.

  2. 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 of map. The effect is to get an array of all the offices where sales exceeded the sales target. The map 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:

    JSONtutorial-Lambda_2nd_output.png

    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 and filter again, this time to further highlight the top-performing offices. We’re also going to use a text function called joinStrings. We use joinStrings to make a series of sales percentages appear as a comma-separated text phrase.

  3. 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 the offices 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:

    JSONtutorial-Lambda_3rd_output.png

    Notice that the percent symbol doesn’t appear after each percentage figure. We have to add it as an argument in the ATL.

  4. Add the percent symbol as an argument after the percentage function, like this (addition in bold):

    (percentage(office.sales-office.target, office.target),"%"))]] respectively.

  5. Preview the narrative.

    JSONtutorial-Lambda_4th_output.png

    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.

  6. Wrap the percentage function in a call to the joinStrings function, like this (addition in bold):

    joinStrings(percentage(office.sales-office-target, office.target),"%"))]] respectively.

  7. Preview the narrative.

    JSONtutorial-Lambda_5th_output.png

    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. The sort 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 comparison sign(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.

  8. 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 and b 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.

  9. Preview the narrative. It should look like this:

    JSONtutorial-Lambda_6th_output.png

In this step of the tutorial, we’ve learned:

  • What lambda expressions look like in ATL.

  • Use of the map, filter, and sort functions.

  • Use of the joinStrings function.

Now let’s move on to find out how variables can simplify our writing of ATL.

Using variables