For example,
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
auto lambda_plus = [] (auto x, auto y) { return x+y; }; | |
int z = lambda_plus(5,3); |
Composing Two Lambdas
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
template<typename Lambda1, typename Lambda2> | |
auto operator* (Lambda1 l1, Lambda2 l2) | |
{ return [l1, l2](auto ... x) { return l1(l2(x...)); }; }; |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
auto incr = [](auto a) {return a+1;}; | |
auto dbl = [](auto a) {return a*2;}; | |
auto output_decorate = [](auto a) {cout<<a<<" "<<endl; return a;}; | |
auto dbl_incr_decor = output_decorate * incr * dbl; | |
int x = dbl_incr_decor(5); | |
double y = dbl_incr_decor(.5); |
We can also compose inline, like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
auto triple = [](auto a) { return a*3; }; | |
cout << (triple * incr *dbl)(5) << endl; |
This produces 33.
A Handy Map
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
auto map = [] (const auto &function, const auto &container ) | |
{ | |
auto retval = container; | |
retval.clear(); | |
for (const auto &elem : container) | |
retval.push_back(function(elem)); | |
return retval; | |
}; | |
vector<int> data {1,2,3,4}; | |
vector<int> map_results = map(double_incr_decor,data); |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
template<typename return_type, typename ...params> | |
auto _L( return_type (*fp) (params... args)) | |
{ return [fp] (params... args) { return fp(args...); }; }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
auto incr = [](auto a) {return a+1;}; | |
int add1(int x) { return x+1; } | |
int sub1(int x) { return x-1; } | |
auto ident = _L(add1) * _L(sub1); | |
auto ident2 = incr * _L(sub1); | |
cout << ident(1) << "," << ident2(1) << endl; |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
template<typename return_type, typename arg1_type, typename ...params> | |
auto _L1( return_type (*fp) (arg1_type x, params...args), arg1_type x) | |
{ return [fp, x] (params...args) { return fp(x,args...); }; }; | |
double mult(double x, double y) { return x*y; } | |
auto to_radians = _L1(mult,M_PI/180.0); | |
auto cos_in_degrees = _L(cos) * to_radians; | |
cout << cos_in_degrees(45.0) << endl; |
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