I don't see any major problems with this approach, in fact it strikes me as quite elegant! However, I notice your Cube
callable/functor and your square
function are not composable with the rest of your operators, and this is trivially fixable by making them also functions returning lambdas:
Func square(Func op) {
return [op](double x) {
auto r = op(x);
return r*r;
};
}
And similarly for cube
. Note how I cache the result - this way if op
has side effects, they only happen once, as most consumers of the library might expect.
What I mean by composable is that you can now do stuff like:
Func complicatedOperation = square(cube(cube + square));
The way these would get composed is by having a small helper function, id
:
double id(double x) {
return x;
}
id
or "identity", a function which just returns its argument, provides a simple way of saying "value goes here" when the tree is being constructed.
and if you want to start the expression tree with the square or cube functions, define these:
liftedSquare = square(id);
liftedCube = cube(id);
Now, there's an easier way here - what about just having:
#include <cmath>
...
Func operator^(const Func& lhs, const Func& rhs){
return [lhs, rhs](const double x){
return std::pow(lhs(x), rhs(x));
};
}
No need for separate square
and cube
anymore! square
becomes auto square = id ^ lift(2);
. lift
is this function which is basically just a lazy version of id
, it takes a value and returns a function returning that value.
Then just chuck everything into a namespace so it doesn't interfere with other operator overloads, and you have this:
#include <functional>
#include <cmath>
#include <iostream>
namespace LazyOps {
using Func = std::function<double(double)>;
namespace {
double _id(const double x) {
return x;
}
}
Func id = _id;
Func lift(const double x) {
return [x](const double _) {
return x;
};
}
Func cube(const Func& op) {
return [op](const double x) {
auto r = op(x);
return r*r*r;
};
}
Func square(const Func& op) {
return [op](const double x) {
auto r = op(x);
return r*r;
};
}
Func operator+(const Func& lhs, const Func& rhs){
return [lhs, rhs](const double x){
return lhs(x) + rhs(x);
};
}
Func operator-(const Func& lhs, const Func& rhs){
return [lhs, rhs](const double x){
return lhs(x) - rhs(x);
};
}
Func operator*(const Func& lhs, const Func& rhs){
return [lhs, rhs](const double x){
return lhs(x) * rhs(x);
};
}
Func operator/(const Func& lhs, const Func& rhs){
return [lhs, rhs](const double x){
return lhs(x) / rhs(x);
};
}
Func operator^(const Func& lhs, const Func& rhs){
return [lhs, rhs](const double x){
return std::pow(lhs(x), rhs(x));
};
}
}
int main(){
using namespace LazyOps;
auto liftedSquare = square(id);
auto liftedCube = cube(id);
auto result1 = liftedSquare + liftedCube;
auto result2 = liftedSquare - liftedCube;
auto result3 = liftedSquare * liftedCube;
auto result4 = liftedCube / liftedSquare;
auto newSquare = id ^ lift(2);
auto result5 = result1 + result2 - result3 * result4;
auto result6 = cube(square(liftedCube / liftedSquare));
auto result7 = id ^ id;
double x = 4.0;
std::cout << "result1: " << result1(x) << "\n";
std::cout << "result2: " << result2(x) << "\n";
std::cout << "result3: " << result3(x) << "\n";
std::cout << "result4: " << result4(x) << "\n";
std::cout << "result5: " << result5(x) << "\n";
std::cout << "result6: " << result6(x) << "\n";
std::cout << "result7: " << result7(x) << "\n";
std::cout << "square(" << x << "): " << newSquare(x) << "\n";
}
Unfortunately, the compiler isn't great with compiling auto result = id ^ id;
just like that, therefore I had to put in a type deduction hint by putting the actual _id
function in a private anonymous namespace and defining an alias Func id = _id;
in the actual LazyOps
namespace.
As for whether this will blow up your stack, I can't really say, but hopefully accepting the Func
arguments as const references can help with that. Make a small script to generate some huge expression tree and see how it goes!