The complicated thing in this section is the concat function. It looks horribly complicated because it unpacks the parameter pack ts into another lambda expression, which recursively calls concat again, with less parameters:
template <typename T, typename ...Ts>
auto concat(T t, Ts ...ts)
{
if constexpr (sizeof...(ts) > 0) {
return [=](auto ...parameters) {
return t(concat(ts...)(parameters...));
};
} else {
return [=](auto ...parameters) {
return t(parameters...);
};
}
}
Let's write a simpler version, which concatenates exactly three functions:
template <typename F, typename G, typename H>
auto concat(F f, G g, H h)
{
return [=](auto ... params) {
return f( g( h( params... ) ) );
};
}
This already looks similar, but less complicated. We return a lambda expression, which captures f, g, and h. This lambda expression arbitrarily accepts many parameters and just forwards them to a call chain of f, g, and h. When we write auto combined (concat(f, g, h)), and later call that function object with two parameters, such as combined(2, 3), then the 2, 3 are represented by the params pack from the preceding concat function.
Looking at the much more complex, generic concat function again; the only thing we do really differently is the f ( g( h( params... ) ) ) concatenation. Instead, we write f( concat(g, h) )(params...), which evaluates to f( g( concat(h) ) )(params...) in the next recursive call, which then finally results in f( g( h( params... ) ) ).