Part 1: Anonymous Functions (lambda)

You have seen how functional programming allows you to write functions that can be used for mapping and reducing data. However, to date, this hasn’t saved you from typing much code. This is because you have had to declare every function that you want to use, i.e. you had to use syntax such as;

int sum(int x, int y)
{
    return x + y;
}

to both provide the code to the function (return x+y;) and also to assign that function to an initial variable (sum).

Anonymous functions (also called lambdas) allow you to declare the code for functions without having to assign them to a variable. They are used for use-once functions, such as the sum function, where it doesn’t make sense to declare it separately. For example, create a new C++ file called lambda.cpp and copy into it;

#include "part1.h"

using namespace part1;

int main(int argc, char **argv)
{
    auto a = std::vector<int>( {1, 2, 3, 4, 5} );
    auto b = std::vector<int>( {6, 7, 8, 9, 10} );

    auto result = map( [](int x, int y){ return x + y;}, a, b );

    print_vector(result);

    return 0;
}

Compile and run using

g++ --std=c++14 -Iinclude lambda.cpp -o lambda
./lambda

This should print

[ 7 9 11 13 15 ]

This code has used the C++ lambda function syntax to create an anonymous function that is passed as an argument to map. The format of the C++ lambda syntax is;

[]( arguments ){ expressions; }

where arguments is a comma separated list of arguments to the function, and expressions are the lines of code that make up the function.

Anonymous lambda functions are just like any other function. The only difference is that they have been created without being initially assigned to a named variable. The unnamed function created using

[]( arguments ){ expressions; }

is completely identical to

auto name(arguments)
{
    expressions;
}

except that the normal version assigns this function to a variable called name, while the anonymous lambda version creates the function without assigning it to a variable.

C++ lambda functions are extremely powerful. For example, edit your lambda.cpp and set it equal to;

#include "part1.h"

using namespace part1;

int main(int argc, char **argv)
{
    auto a = std::vector<int>( { 1, 2, 3, 4, 5} );

    auto product = reduce( [](int x, int y){ return x * y; }, a );

    std::cout << product << std::endl;

    return 0;
}

Compile and run using

g++ --std=c++14 -Iinclude lambda.cpp -o lambda
./lambda

This should print 120. Now edit lambda.cpp to contain

#include "part1.h"

using namespace part1;

int main(int argc, char **argv)
{
    auto a = std::vector<int>( { 1, 2, 3, 4, 5} );

    auto squares = map( [](int x){ return x*x; }, a );

    print_vector(squares);

    return 0;
}

Compile and run using

g++ --std=c++14 -Iinclude lambda.cpp -o lambda
./lambda

This should print [ 1 4 9 16 25 ].

Creating functions dynamically

As well as using lambdas to create functions as arguments, you can also use lambdas to more quickly create functions, e.g. edit your lambda.cpp to contain;

#include <iostream>

int main(int argc, char **argv)
{
    auto square = [](int x){ return x * x; };

    std::cout << square(5) << std::endl;

    return 0;
}

Compile and run using

g++ --std=c++14 lambda.cpp -o lambda
./lambda

This should print 25. Here you have created a simple function that accepts one argument, and returns that argument squared. You have immediately assigned this function to the variable square, allowing you to call this function via this variable.

C++ lambda functions can be as big or complicated as you want. However, to maintain readability, you should keep them as simple as needed.

Binding arguments

One good use of lambdas is to use them to create specialised versions of more complex functions. For example, edit your lambda.cpp to contain;

#include <iostream>

using namespace std;

int sum(int x, int y)
{
    return x + y;
}

int main(int argc, char **argv)
{
    auto plus_five = [](int x){ return sum(x,5); };

    std::cout << plus_five(7) << std::endl;

    return 0;
}

Compile and run using

g++ --std=c++14 lambda.cpp -o lambda
./lambda

This should print 12. Here, we have created a lambda function that takes a single argument (x), and that only calls the function sum with arguments x and 5. This is assigned to the variable plus_five. This means that plus_five is now a function that takes a single argument, and returns the result of adding five to that argument.

In this example, we have used a lambda to bind the value of the second argument of sum to the number 5. The use of lambda has reduced the amount of code needed to create the plus_five function. Compare this to what is needed if we didn’t use lambda.

#include <iostream>

using namespace std;

int sum(int x, int y)
{
    return x + y;
}

int plus_five(int x)
{
    return sum(x, 5);
}

int main(int argc, char **argv)
{
    std::cout << plus_five(7) << std::endl;

    return 0;
}

While this saving is small for these simple functions, I hope you can see that binding can be really useful if you have larger or more complicated functions.

The saving is also very useful when we want to create specialised functions for mapping or reduction, e.g. edit your lambda.cpp and set the contents to;

#include "part1.h"

using namespace part1;

int multiply(int x, int y)
{
    return x * y;
}

int main(int argc, char **argv)
{
    auto a = std::vector<int>( { 1, 2, 3, 4, 5} );

    auto two_times_a = map( [](int x){ return multiply(x, 2); }, a );

    print_vector(two_times_a);

    return 0;
}

Compile and run using;

g++ --std=c++14 -Iinclude lambda.cpp -o lambda
./lambda

This should print [ 2 4 6 8 10 ].

Captures

A benefit of lambda functions that make them extremely useful is that they can capture the values of any variables that are defined in the enclosing scope of the lambda. What this means is that the lambda function can use (and potentially change) any variables that are available at the time of creating the lambda. For example, change your lambda.cpp to contain;

#include <iostream>

int main(int argc, char **argv)
{
    int a = 45;

    auto func = [=](int x){ return x + a; };

    std::cout << func(5) << std::endl;

    return 0;
}

Compile and run using;

g++ --std=c++14 lambda.cpp -o lambda
./lambda

This should print out 50. So what has happened here? First, we defined a variable called a that exists in the scope of the main function, using;

int a = 45;

Next, we created a new lambda function using

auto func = [=](int x){ return x + a; };

This function accepted a single argument (x), which it added to the value of a. The result of calling this function with the argument 5, as in

func(5)

is 50, because a = 45 and x = 5, so x + a equals 50. You will notice that our lambda function had a small addition compared to the lambda functions used before;

[=](int x){ return x + a; }

The [] that signalled the start of the lambda function has been replaced by [=]. This is because the [] is the “capture specification”. It tells C++ how the lambda function should capture the values of any variables that are available in the scope of the function. There are three main capture specifications;

Use [] if you don’t want to capture any variables, use [=] if you only want to read those variables (i.e. capture by value), and use [&] if you want to write to any variables (i.e. capture by reference).

Capturing by reference is useful when you want to change the value outside the function. For example, change your lambda.cpp to contain;

#include <iostream>

int main(int argc, char **argv)
{
    int a = 45;

    auto func = [&](int x){ a += 10; return x + a; };

    std::cout << func(5) << std::endl;

    std::cout << func(5) << std::endl;

    return 0;
}

Compile and run using;

g++ --std=c++14 lambda.cpp -o lambda
./lambda

This should print

60
70

This is because the lambda function has changed the value of a by increasing it by 10 before every function call. Note that this is potentially confusing (calling this function has the side-effect of changing a, meaning that every call produces a different result). You should thus exercise caution when using capture by reference.

Capture-by-value is the more common scenario. Note that the variables are captured (copied) at the time the lambda function is created. For example, edit your lambda.cpp to contain;

#include <iostream>

int main(int argc, char **argv)
{
    for (int i=1; i<=10; ++i)
    {
        std::cout << "i == " << i << std::endl;

        auto func = [&](int x){ return x + i; };

        std::cout << "func(5) equals " << func(5) << std::endl;
    }

    return 0;
}

Compile and run using;

g++ --std=c++14 lambda.cpp -o lambda
./lambda
i == 1
func(5) equals 6
i == 2
func(5) equals 7
i == 3
func(5) equals 8
i == 4
func(5) equals 9
i == 5
func(5) equals 10
i == 6
func(5) equals 11
i == 7
func(5) equals 12
i == 8
func(5) equals 13
i == 9
func(5) equals 14
i == 10
func(5) equals 15

Can you see what has happened?

The lambda function in this example is created within the scope of the for loop. This means that a new lambda function is created for each iteration of the loop, with its own copy of the value of i.


Exercise

Rewrite your countlines.cpp program so that it uses a lambda function in reduce to calculate the total number of lines across all plays.

If you get stuck or want some inspiration, a possible answer is given here.


Previous Up Next