Lecture 11: Functions as objects (function pointers), a.k.a. functional programming

\( \newcommand\bigO{\mathrm{O}} \)

These notes are incomplete, they don't cover captures inside lambdas. The "Lambda Capture" section of this page is a very detailed description of captures inside lambdas. As a quick summary, there are two types of captures for allowing the lambda to use the variables outside the lambda. We can pass these parameters either by value, or by reference. We can also specify a default capturing method (= for pass by value, and & for pass by reference). The capture specifiers go inside the square brackets in the lambda expression. For example, the following lambda captures foo by reference, and everything else by value:

[=, &foo](...) {}

A lot of programming is noticing repetition or low-level details, and abstracting over them. This is what allows us to build complex programs like a web browser or a compiler without worrying about details like how the variables are shared between the registers and the stack. We have been climbing the ladder of abstraction with the programming constructs we have used so far:

In this lecture, we will continue the same thread and abstract over functions. Although I have "functional programming" in the title, this is just an introduction. While it is useful, it is only the tip of the iceberg. If you like the programming style here, I urge you to check a language where functional programming is a first-class citizen.

1. An example with some repetition

Suppose, we are extending our awesome math library from the error handling lecture and decided to add functions for computing squares. This is the code we have now:

struct SqrtError {};

// Compute square root using Newton's method, the implementation does not matter
// (that's the point of this lecture!).
double Sqrt(double a) {
  if (a < 0) {
    throw SqrtError{};
  }
  if (a == 0) {
    return 0;
  }
  double x = 1;
  double b = a;
  // pick a good estimate, this is not the fastest way to do it
  if (a > 1) {
    while (b > 1) {
      b /= 4;
      x *= 2;
    }
  } else if (a < 1) {
    while (b < 1) {
      b *= 4;
      x /= 2;
    }
  }
  for (int i = 0; i < 7; ++i) {
    x = (x + (a / x) ) / 2;
  }
  return x;
}

// other math functions
double Abs(double x) {
  if (x == 0) return +0.0;
  if (x < 0) return -x;
  return x;
}

double Square(double x) {
  return x * x;
}

int Round(double x) {
  return (int)x;
}

Let's also add a couple of helpers to make our lives easier:

template<class It>
void Print(It begin, It end) {
  for (; begin != end; ++begin) {
    cout << *begin << '\t';
  }
  cout << '\n';
}

template<class Vector>
void PrintCollection(const string & name, Vector v) {
  cout << name << ":\t";
  Print(v.begin(), v.end());
}

Now, here is a program that uses the functions above:

// the goal of this program is to apply each function to some inputs
int main(int argc, char * argv[]) {
  vector<double> v{-2.718, -1, 0, 1, 2, 3, 4, 3.14};
  // create a second vector without negatives for square root
  vector<double> v2{0, 1, 2, 3, 4, 3.14, 36, 64, 1e32};

  vector<double> squares;
  for (double x : v) {
    squares.push_back(Square(x));
  }
  vector<double> absolutes;
  for (double x : v) {
    absolutes.push_back(Abs(x));
  }
  vector<int> converted_to_ints;
  for (double x : v) {
    converted_to_int.push_back(Round(x));
  }
  vector<double> square_roots;
  for (double x : v2) { // note that this uses v2
    square_roots.push_back(Sqrt(x));
  }

  // print the output
  PrintCollection("squares", squares);
  PrintCollection("vector 1", v);
  PrintCollection("absolutes", absolutes);
  PrintCollection("converted ints", converted_to_ints);
  PrintCollection("vector 2", v2);
  PrintCollection("square roots", square_roots);


  return 0;
}

The code above has a bunch of repeated loops. How can we merge them so the code is easier to manage? If all of them were over v, we could perhaps merge the loop bodies, but the last loop is over v2. The simplest way to abstract repeated code that is parametric over some values is to create a function. For example, if we had a bunch of expressions like x * x - x and y * y - y, we could create a function int f(int a) { return a * a - a; } to abstract over them. So, what are the parameters that change between the loops? The input vector and the function we apply. So, we need a function wrapping the following code snippet:

vector<double> output;
for (double x : INPUT) {
  output.push_back(FUNCTION_TO_APPLY(x));
}
return output;

1.1. Function types

So, we need types for INPUT and FUNCTION_TO_APPLY to turn this snippet to a true function. INPUT will be a vector<double> (that is the type of v and v2), and FUNCTION_TO_APPLY should have the same type as, say, Square. What is the type of Square? Deductively, its type needs to contain two bits of information:

  • The input types, so that the compiler can notice that Square("foo") is a type error.
  • The output type, so that the compiler can, again, notice that string name = Square(42) is a type error.

So, we need a type that contains both of them. In C++, function types are written like so:

return_type (*)(param_type1, param_type_2, ...)

So, the type of Square is double (*)(double). Notice that this looks a lot like its signature: double Square(double), only that the function name is replaced with (*) to denote that this is a pointer to the address of the code of the function, i.e. a function pointer. So, what we are actually going to pass around is the address of the function, and the C++ compiler will generate the code to jump to that address to call the function. With all of this at hand, here is a first version of our functionThis function is conventionally named Map because it maps each element to the result.:

// an alternative signature is
//  vector<double> Map(const vector<double> & input,
//                    double function_to_apply(double))

  vector<double> Map(const vector<double> & input,
                     (double (*)(double)) function_to_apply) {
    vector<double> output;
    for (double x : input) {
      output.push_back(function_to_apply(x));
    }
    return output;
  }

The function signature above looks pretty awful, we can prettify it a bit using type aliases:

// this statement creates a new alias for the type `double(*)(double)`
using double_to_double = double(*)(double);
// we could also use a typedef like below, but it is harder to read IMO.
// typedef double(*)(double) double_to_double;

vector<double> Map(const vector<double> & input,
                   double_to_double function_to_apply) {
  vector<double> output;
  for (double x : input) {
    output.push_back(function_to_apply(x));
  }
  return output;
}

Map is an example of a higher-order function: it takes a function as an input and uses it so it kind of operates at a higher level. All the functions we have written before then were first-order functions (they did not take any functions so they were at the bottom of this hierarchy). Now, we can clean up our main function a bit:

// ...
vector<double> squares = Map(v, Square);
vector<double> absolutes = Map(v, Abs);
vector<int> converted_to_ints;
for (double x : v) {
  converted_to_int.push_back(Round(x));
}
vector<double> square_roots = Map(v2, Sqrt);
// ...

Much nicer! Note that we haven't rewritten the code that computes converted_to_ints because this version of Map works on only functions that convert a double to another double but we need to return a vector<int>. Time for more abstraction!

2. std::function and mixing templates with function pointers

We can generalize our Map function to work on arbitrary inputs and outputs using templates:

// In is the parameter type of the function we are expecting, Out is the return type
template<class In, class Out>
vector<Out> Map(const vector<In> & input,
                (Out(*)(In)) function_to_apply) {
  vector<Out> output;
  for (double x : input) {
    output.push_back(function_to_apply(x));
  }
  return output;
}

The code above is good enough for us. While at it, we may want to enable some conversions for the function types (for example, if our function returns a short and a short can be automatically converted to an int, we may want to apply it to generate an int as well). The standard library (but not the language!) ships with a type to represent functions that has these conveniences built-in: std::function. Let's rewrite Map using it:

#include <functional>

template<class In, class Out>
vector<Out> Map(const vector<In> & input,
                function<Out(In)> function_to_apply) {
  vector<Out> output;
  for (double x : input) {
    output.push_back(function_to_apply(x));
  }
  return output;
}

Now, we can rewrite our main function to have no loops:

vector<double> squares = Map<double, double>(v, Square);
vector<double> absolutes = Map<double, double>(v, Abs);
vector<int> converted_to_ints = Map<double, int>(v, Round);
vector<double> square_roots = Map<double, double>(v2, Sqrt);

We need to give Map the template parameters because the compiler cannot infer those in this case. Now that we have our Map function all well-rounded, we can also improve its implementation: we can allocate the space for the output all at once to reduce the number of allocations.

#include <functional>

template<class In, class Out>
vector<Out> Map(const vector<In> & input,
                function<Out(In)> function_to_apply) {
  vector<Out> output;
  output.reserve(input.size()); // the line we just added to do only 1 allocation
  for (double x : input) {
    output.push_back(function_to_apply(x));
  }
  return output;
}

3. Higher-order functions in the standard library

Note: all functions here are parts of the <algorithm> library or <numeric> library. So, you need to include those headers.

Map is a very generic function, and it would be useful whenever we need to apply an unknown transformation over a bunch of things. The standard library of course has its version of it called std::transform with some differences:

  • it uses iterators so it works on any type of collection (linked lists, vectors, sets, …) that have an iterator interface.
  • it uses some type introspection to not need the template arguments to be specified most of the time.
  • rather than creating its own output, it requires a pre-allocated output iterator (so, we create the vector first, then call std::transform).

Without further ado, here is how we would compute converted_to_ints using std::transform (compare it to how we called Map earlier):

// pre-allocate the space by creating a vector of the same size as the input
vector<int> converted_to_ints(v.size()); 
std::transform(v.begin(), // beginning of input
               v.end(), // end of input
               converted_to_ints.begin(), // beginning of output
               Round); // function to apply

Also note that transform is pretty versatile. We could choose to pass around only a small range of the input by giving it the appropriate iterators. Now, let's look into other higher-order functions we can use:

3.1. std::reduce and std::transform_reduce

Sometimes we want to sum or multiply all elements of a collection to reduce it to a singular value. For example, one way of computing the factorial is to have a collection of first N positive numbers and to multiply them. We can write a loop like this to do it:

vector<int> firstN{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int result = 1;
for (int x : firstN) {
  result = result * x;
}

Since eliminating loops is the theme of this lecture, we will abstract over this loop. We can abstract over 3 things:

  • the initial value 1
  • the input vector firstN
  • the multiplication operation result * x

std::reduce (also std::accumulate, check out cppreference for the difference) does exactly this abstraction. So, we can rewrite the code above as:

int times(int a, int b) { return a * b; }

int result = std::reduce(firstN.begin(), firstN.end(), 1, times);

The first two parameters are the input iterators (like transform), the third one is the initial value 1, and the last one is the function we are using to reduce the result. So, the call to std::reduce above is roughly the same as (with subject to reordering):

times(...times(times(times(times(1, 1), 2), 3) ...), 10)
// which is same as 1 * 1 * 2 * 3 * ... * 10

3.1.1. Anonymous functions

It is kind of silly to create a function with a name just for multiplication. We can also create an anonymous function like so (here, you can think of putting just [] for the function name, we could actually put some other useful things there but that's beyond CS 32):

// this is an anonymous function that computes a * b
[](int a, int b) -> int { return a * b; }

The -> int part above denotes the return type. We can omit it most of the time, and the compiler can infer it for anonymous functions. Let's use our anonymous function in the call to reduce:

int result = std::reduce(firstN.begin(), firstN.end(), 1,
                         [](int a, int b) { return a * b; });

This idea of creating functions on the fly and passing them around is the core of functional programming.

For a * b, there is already a function in the standard library because it is so common: std::multiplies() (it needs the parentheses because of historical design decisions). There is also std::plus() for addition.

3.1.2. Back to reduce and transform_reduce

We have seen the two basic operations: transforming a collection, and reducing it. We can also combine these, but we would be creating an unnecessary intermediate collection (output of transform below):

input --[transform]--> intermediate output --[reduce]--> single value

For example, consider this small programming problem: we have a bunch of strings we want to put to a buffer, but we want to allocate the buffer only once, so we need the total size. We can break it into two problems:

  1. Compute the size of each string (transform)
  2. Add the sizes (reduce)

We can write the following snippet to solve this using transform and reduce:

vector<string> names{"Mehmet", "Alice", "John"};
vector<size_t> sizes(names.size()); // we are wasting O(|names|) space here, because we won't need each size individually
transform(names.begin(), names.end(), sizes.begin(),
          // a function that computes the size of a string
          [](const string& name) { return name.size(); });
size_t total_size = reduce(sizes.begin(), sizes.end(),
                           0, // initial value
                           std::plus()); // to add each size

The issue here is that we are wasting so much space while reduce only needs 1 element at a time to apply std::plus(). So, the standard library has a fused version of transform and reduce imaginatively called transform_reduce to perform this common operation without wasted space. We can rewrite the code above as:

vector<string> names{"Mehmet", "Alice", "John"};
size_t total_size =
  transform_reduce(names.begin(), names.end(), // input
                   0, // initial value of the result for reduce
                   std::plus(), // reducing function
                   [](const string& name) { return name.size(); }); // transform function

3.2. std::sort, std::find_if and std::remove_if

Suppose we are implementing an in-memory student roster for something like GauchoSpace, and we have these classes at hand:

#ifndef student_h
#define student_h

#include <string>

struct Person {
  std::string first_name;
  std::string last_name;
};

struct Student : public Person {
  std::string major;

  Student(std::string first_name,
          std::string last_name,
          std::string major) :
    Person{first_name, last_name},
    major(major) {}
};

#endif

And, in our main function, we have the following example roster:

vector<Student> roster{
  Student("Mehmet", "Emre", "CS"),
  Student("Arthur", "Murray", "Dance"),
  Student("Charles", "Darwin", "Biology"),
  Student("Alan", "Turing", "CS"),
};

Normally, we would use a proper database with indices for better complexity, but our roster is small enough so we will implement the functionalities using linear search etc.

3.2.1. Finding a specific element

We may be interested in finding a specific student. std::find is similar to map.find which finds an element we are looking for. We may be also interested in finding an element that satisifies some criteria. For example, we may be interested in finding the first student in the roster whose last name is Emre. To do so, we can use std::find_if:

auto iterator = std::find_if(roster.begin(), roster.end(), //input
                             // a "predicate", find_if returns the first value that makes this function's return value true
                             // if it can't find such a value, it returns roster.end()
                             [](const Student& s) {
                               return s.last_name == "Emre";
                             });

We can combine functions like these to build a search functionality for our roster. Generally, processing data by generalizing different operations for each element is where this style of functional programming shines. That's why it is popular in my area of programming languages/compilers.

3.2.2. Sorting the whole list

GauchoSpace has this functionality where you can sort the roster by different keys. We can implement it by having different versions of operator < for Student perhaps? Then, we need to copy over our roster and re-build each Student object in different versions of the class. Another alternative is to have a sorting algorithm that accepts a comparison function so we can swap out operator <. This is what std::sort does. Normally, it takes only the begin/end iterators, but we can also give it a third parameter to specify what to use instead of operator <. For example, we can sort the students by first name using the following:

std::sort(roster.begin(), roster.end(),
          [](Student& a, Student& b) {
            return a.first_name < b.first_name;
          }
          );

3.2.3. Removing some elements

We may also need to filter (keep) some elements. C++'s std::remove_if has a way to do the opposite, removing some elements. It works on different types of containers so it does not have an idea on how to shrink the actual container, we need to do that bit by hand. remove_if removes the elements that satisfy a condition, then moves other elements back in an efficient ( \( \bigO(n) \) ) manner. It returns where the new end of the range is. We can then erase the elements afterward to finish the clean-up. (Exercise: try the code below with and without the call to vector::erase)

auto new_end =
  std::remove_if(roster.begin(),
                 roster.end(),
                 // suppose this is a CS-only class, so we drop everyone who is
                 // not a CS major
                 [](const Student& s) {
                   return s.major != "CS";
                 });
roster.erase(new_end, roster.end());

Footnotes:

1

For example, if we had a bunch of expressions like x * x - x and y * y - y, we could create a function int f(int a) { return a * a - a; } to abstract over them.

2

This function is conventionally named Map because it maps each element to the result.

Author: Mehmet Emre

Created:

The material for this class is based on Prof. Richert Wang's material for CS 32