For example, declares a generic lambda lambda_plus. That lambda holds the value of a function which applies operator+ to two arguments. Well, sort of. If I understand the standard correctly, that line does nothing more than declare a template named lambda_plus. The following line forces the instantiation of that template. In instantiating the template, all of the 'auto' types in the previous line are deduced to int, and + is determined to be the builtin integer operator+. Thus z is assigned the integer value 8.
Composing Two Lambdas
The example code below uses this operator to take three lambdas and compose them. The composition works like this: first you apply dbl(), then the result of that is fed to incr(), and the result of that is fed to output_decorate(). The value that output_decorate returns is the return value of dbl_incr_decor. As expected, the output of this code is "11 2 ". The lambda templates have all been instantiated twice: once for int and once for double.
We can also compose inline, like this:
This produces 33.
A Handy Map
This code takes as input the vector data, outputs "3 5 7 9 ", and constructs a new vector map_results with the values 3, 5, 7, 9, while leaving data unmodified. I'll leave it as an exercise for the reader to construct a lambda tmap which applies a function to a container, modifying its contents (or you can look at the example code on gitHub).
But What About Plain Old Functions?
This is nice and all, but it really won't be a lot of use unless there is a way to compose plain old functions (POFs) and lambdas. Unfortunately, it looks as though there's no way to seamlessly compose lambdas and functions. Fortunately, there is an alternative that's not too painful. The template below will convert any function to a lambda. With the ability to convert any function to a lambda, this means we can not only compose a lambda and a POF, but we can also compose two POFs. This produces "1,1". All of the examples so far have used functions of only one argument. This isn't needed in all cases; the rightmost function (only) will accept more than one argument. There are working examples in the code at GitHub (linked above) of composing functions with two arguments, and with zero arguments.One more tool that is useful for this style of programming is the ability to bind arguments. Here's a template for that, and some examples: The template _L1 binds the first argument to the function, then allows the other arguments to be filled in at the call site. There are similar templates _L2 and _L3 that bind the first two or three arguments.
A Note About the API: A Work in Progress
- _L1, _L2, _L3 are less than elegant. But the template metaprogramming way will be something like _L<1>::close, which is even less elegant.
- Presently, only the first function in the chain of composition can have more than one argument. There's at least one way to alleviate this: make functions that return tuples, and provide an automated way of unpacking the tuples into the argument list of the next function. If you've never seen Haskell in action --- this is more useful than you might think. It should pair wonderfully with Ranges.
- I would love to find a way to directly compose plain old functions, without needing the _L template.
- As written, map will need to be written separately for many of the different container types. There's probably a better way.
- (edited) The Biggie: operator * applies to anything in the global namespace. Using a variadic function instead is probably the best alternative that doesn't, but I don't like it much. Boost::Proto was my first choice, but I didn't see a way to offer a clean interface with it. I suppose operator, rather than operator* might result in fewer conflicts.
- (edited again) Another comment on that last issue: If there were an is_lambda type trait, we could just restrict the operator* to working on lambdas and instances of std::function. That would be a clean solution.
Very nice post! Just a little suggestion: you can easily forward parameters:
ReplyDeletetemplate < typename Lambda1, typename Lambda2 >
auto operator* (Lambda1 l1, Lambda2 l2)
{
return [l1, l2](auto&& ... x) {
return l1(l2(forward < decltype(x) > (x)...));
};
};
The other copies will be elided automatically (or moved).
Marco
Marco, thanks for the useful comment. I hadn't known that forward could be applied to the whole parameter pack of a variadic template. Good to know.
Delete