The Universality and Expressiveness of std::accumulate

The Universality and Expressiveness of std::accumulate

By Paul Keir

Overload, 23(130):13-15, December 2015


Folding is a highly generic operation available through std::accumulate. Paul Keir goes beyond reduction, with the help of C++14’s polymorphic lambdas.

Graham Hutton’s 1999 monograph: A tutorial on the universality and expressiveness of fold [ Hutton99 ] provides a fascinating and eminently readable analysis of an elementary reduction operator from functional programming: fold . Hutton’s tutorial is punctuated by increasingly compelling examples, written in Haskell, of the use of fold to implement common list operations, including summation; concatenation; reversal; function composition; left folds; and concluding with the Ackermann function. The fold combinator is directly comparable to the C++ standard library function: std::accumulate . In this article, I focus on the fold examples from Hutton, and present equivalent generic C++ incarnations of each; making thorough use of appropriate C++14 library and language features, including polymorphic lambda functions 1 .

The four-parameter accumulate function constructs a result from the repeated pairwise application of a given binary function, to elements supplied from both an iterator range; and a single “zero” value, having the type of the result. A canonical example is summation; the value returned from the call to accumulate in listing 1 is 10.

int xs[]{1,2,3,4};
accumulate(cbegin(xs), cend(xs), 0, plus<>{});
			
Listing 1

Hutton’s first four examples use fold to implement arithmetic and logic operations using built-in functions to specify the relevant binary operations. Similar function objects are provided by the C++ standard library within the <functional> header. The example above demonstrates the use of plus<> for operator+ ; while operator* , operator&& , and operator|| correspond to multiplies<> , logical_and<> , and logical_or<> respectively. Note that C++14 conveniently defines a default template argument for such function objects; std::plus<> invokes a specialisation which infers the relevant types from its arguments.

Accommodating std::accumulate

A binary operator is said to be associative when an expression involving a sequence of its applications produces the same result, regardless of the order of evaluation. The four C++ function objects from the previous section all denote associative operations. Consider addition: both (1 + 2) + 3 and 1 + ( 2 + 3 ) produce the same result; 6. Operator precedence is irrelevant when faced with a ⊕ b ⊕ c; the question is whether to evaluate from the left ; or from the right . In particular, which order does accumulate use? It uses a left ordering.

An elementary non-associative binary operation is subtraction . The call to accumulate in listing 2 would then produce a result equal to (( 0 - 1) - 2 ) - 3, i.e. - 6; evaluating from the left. In contrast, an evaluation ordered from the right , say 1- ( 2 - ( 3 - 0 )), produces a result of 2. Alas, the remaining examples from Hutton [ Hutton99 ] firmly assume the fold operation evaluates from the right .

vector<double> xs{1,2,3};
accumulate(cbegin(xs), cend(xs), 0, minus<>{});
			
Listing 2

Producing a result from accumulate equal to a right fold requires two interventions: we need the order of traversal across the container elements reversed; and for the order of the arguments given to the binary operation to be switched 2 . Listing 3 defines such a wrapper for accumulate , called foldr . The C++14 functions crbegin and crend return const iterators to the reverse beginning and reverse end of the container argument c . Meanwhile, the flip function, uses std::bind to reverse the argument order for the binary function object; e.g. flip(minus<>{})(0,1) evaluates to 1; not - 1.

template <typename F>
auto flip(const F &f) { return bind(f,_2,_1); }

template <typename F, typename T, typename C>
T foldr(const F &f, const T &z, const C &c) {
  return accumulate(crbegin(c), crend(c), z,
    flip(f));
}
			
Listing 3

The definition of foldr in listing 3 removes the need to call crbegin , crend and flip . It also allows a single container argument to drive the C++ fold ; much as with C++ ranges [ Niebler15 ]. This allows listings here to remain concise; while also facilitating a comparison of the syntactical differences between Haskell and C++. We can now invoke a right fold . Assuming ` C ` creates an arbitrary standard sequence container, with inferred value type 3 , the call to foldr in listing 4 returns the integer 2.

foldr(minus<>{}, 0, C(1,2,3))
			
Listing 4

Non-reducing folds

Using a fold to concatenate two containers first requires a small helper function, which should return a new container, by adding a single element to the front of an existing one. Haskell provides the (:) operator for this job. Listing 5 defines this using its traditional Lisp name: “cons”.

auto cons = [=](auto x, auto xs) {
  decltype(xs) ys{x};
  copy(begin(xs), end(xs), back_inserter(ys));
  return ys;
};
			
Listing 5

Like subtraction , the cons function is non-associative ; and non-commutative . cons though, expects two different argument types. Listing 6 provides foldr with cons as the binary function, and an empty container as the “zero” or starting value; to define an identity fold . That is, id(C(1,2,3)) will return a container object of the same type; with the same 3 elements. Meanwhile, the genericity of C++ allows a similar invocation which only changes the container type: foldr(cons, list<int>{}, C(1,2,3)) .

auto id = [=](auto xs) {
  return foldr(cons, decltype(xs){}, xs);
};
			
Listing 6

To append one container to another, listing 7 again uses cons for foldr ’s first argument; while providing the second, a container, as its “zero” value. Note that while the elements of, say, append(C('a'),C('b')) and C('a','b') are equal, so too are they equal to append(C<list>('a'),C<vector>('b')) ; as the definition is sufficiently generic.

auto append = [=](auto xs, auto ys) {
  return foldr(cons, ys, xs);
};
			
Listing 7

Folding with lambda arguments

The three functions 4 of listing 8 provide each corresponding foldr invocation with a binary lambda function, as, like Haskell, no equivalents exist within the standard library. The length function returns the size of its container argument, using a lambda function with unnamed first argument. Both reverse and map return a container 5 ; with map utilising the closure aspect of lambda expressions to capture f .

auto length = [=](auto xs){
  return foldr(
    [=](auto, auto n){ return 1+n; },
    0,
    xs);
};
auto reverse = [=](auto xs){
  return foldr(
    [=](auto y, auto ys)
      { return append(ys,decltype(xs){y}); },
    decltype(xs){},
    xs);
};
auto map = [=](auto f, auto xs){
  return foldr(
    [=](auto y, auto ys){ return cons(f(y),ys); },
    vector<decltype(f(*xs.begin()))>{},
    xs);
};
			
Listing 8

Tuples allow a single fold to perform more than one calculation. For example, listing 9 defines a function which returns both the size of a container, and the sum of its elements 6 .

auto sumlength = [=](auto xs){
  return foldr(
    [=](auto n, auto p){
      return make_pair(n + p.first, 1 + p.second);
    },
    make_pair(0,0),
    xs);
};
			
Listing 9

Functions from folds

The result of applying the composition of two functions f and g to an argument x can be achieved in C++ with the expression: f(g(x)) . In Haskell an elementary binary operator, (.) , can also be used; accepting two functions as arguments, and returning a new function representing their composition. In listing 10, the fold ’s binary function argument is a comparable lambda expression for composition. The result of invoking compose with a container of callable elements is a function object representing the chained composition of each function in sequence. The “zero” argument of foldr uses a simple lambda identity function; though notice it is wrapped by the type of the container element: an instantiation of std::function . Why? While the type of each lambda expression is unique, the type of each container element is the same. std::function provides exactly the required homogeneity; each lambda accepting and returning say an int , becomes simply std::function<int(int)> . The “zero”, meanwhile, needs the same wrapper, as it provides the type of the fold ’s result.

auto compose = [=](auto fs){
  using fn_t = typename decltype(fs)::value_type;
  return foldr(
    [=](auto f, auto g)
      { return [=](auto x){ return f(g(x)); }; },
    fn_t([=](auto x){ return x; }),
    fs);
};
			
Listing 10

One of the most intriguing functions capable of definition by a right fold , such as our foldr , is a left fold . Listing 11 provides such a definition. As before, an identity function is required for the fold ’s starting value, and again this wild lambda needs the guiding hand of std::function ; though the type in question is calculated in a different manner. Unlike compose , the function object returned by foldr is invoked within foldl ; upon z . Our journey has brought us full circle to a left fold , akin to std::accumulate ; an invocation such as foldl(minus<>{}, 0, C(1,2,3)) will produce -6; much as listing 2.

auto foldl = [=](auto f, auto z, auto xs){
  using z_t  = decltype(z);
  using fn_t = std::function<z_t(z_t)>;
  return foldr(
    [=](auto x, auto g){
      return [=](auto a){ return g(f(a,x)); };
    },
    fn_t([=](auto x){ return x; }),xs)(z);
};
			
Listing 11

One last comment regarding left and right folds: should you ever be in the embarrassing situation of being uncertain of the handedness of your fold definition, the expression in listing 12 could be useful. Simply replace fold with either foldr or foldl ; for a true or false evaluation respectively.

fold([](auto x, auto){ return x; },
  false, C(true))
			
Listing 12

Our final fold example, and so too in [ Hutton99 ], is Ackermann’s function [ Ackermann28 ]. Ackermann’s function is commonly cited as a recursive function which is not primitive recursive in a first-order programming language. John Reynolds, however, demonstrated [ Reynolds85 ] that the function is primitive recursive in a higher-order programming language. The C++ implementation in listing 13 includes similar idioms to previous examples, but is given additional treatment to avoid the use of currying seen in Hutton’s definition. While the binary lambda function provided to the innermost fold in the curried original appears unary, λ y g , the C++ version must be uncurried: λ y as g(as) . Note too, that these Ackermann folds encode the traditional natural number arguments within the size of the input and output container values.

auto ack = [=](auto xs, auto ys){
  using ys_t = decltype(ys);
  using fn_t = std::function<ys_t(ys_t)>;
  return [=](auto zs){
    return foldr(
      [=](auto, auto g){
        return [=](auto ws){
          return foldr(
            [=](auto, auto as){ return g(as); },
            g(ys_t{1}),
            ws
          );
        };
      },
      fn_t([=](auto bs){ return cons(1,bs); }),
      zs
    );
  }(xs)(ys);
};
			
Listing 13

Summary

C++ lambda functions, including the polymorphic variety now available in C++14, allow the generic fold operation supported by std::accumulate to extend well beyond simple reductions. While a complex fold can be less readable or idiomatic than the traditional form, the approach can be refined to construct and transform programs, as well as prove specific program properties; while building on established software engineering principles of reuse and abstraction.

The article places less emphasis on performance considerations, instead focusing on pedagogy and algorithmic aspects; while maintaining parity with the equivalent Haskell, with consistent use of auto for type inference.

C++17 will introduce fold expressions [ Sutton14 ]. Here, a finite set of operators, will share the brevity of the comma in pack expansion; consider * versus std::multiplies<>{} . One restriction is the variadic template pack length must be known at compile-time.

Appendix

All examples, along with code for dropWhile , filter and other folds are available at https://bitbucket.org/pgk/accumulate .

References

[Ackermann28] W. Ackermann. Zum Hilbertschen Aufbau der reellen Zahlen Mathematische Annalen, 1928.

[Bird88] R. Bird and P. Wadler. Introduction to Functional Programming Prentice Hall, 1988.

[Gill93] A. Gill, J. Launchbury and S.P. Jones. A Short Cut to Deforestation . ACM Press, 1993.

[Hutton99] G. Hutton. A tutorial on the universality and expressiveness of fold Cambridge University Press, 1999.

[Niebler15] E. Niebler. Working Draft, C++ extensions for Range s. The C++ Standards Committee, 2015.

[Reynolds85] J.C. Reynolds. Three approaches to type structure Springer-Verlag, 1985.

[Sutton14] A. Sutton and R. Smith. Folding expressions . The C++ Standards Committee, 2014.

  1. Note that most of these fold examples can also be found in [ Bird88 ] or [ Gill93 ].
  2. This is described as the third duality theorem in [ Bird88 , p68].
  3. Assume a vector default; i.e. C(1,2) becomes: C<vector,int>(1,2) .
  4. A definition of filter from [ Hutton99 ] appears in the appendix.
  5. Note that map returns a vector object here solely for brevity.
  6. A similar tuple-based definition of dropWhile appears in the appendix.





Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.