2

I come from the python world where I could define a chain of operations and call them in a for loop:

class AddOne:
    def __call__(self, x, **common_kwargs):
        return x+1
class Stringify:
    def __call__(self, x, **common_kwargs):
        return str(x)
class WrapNicely:
    def __call__(self, s, **common_kwargs):
        return "result="+s
data = 42
for operation in [AddOne(), Stringify(), WrapNicely()]:
    data = operation(data)
output = data

(Note: the goal is to have complex operations. Ideally, common kwargs could be given)

What would be the equivalent in C++ if the return type can be different after each call?

I'm not sure I could find anything close but I may have search with wrong keywords…

Jav
  • 1,445
  • 1
  • 18
  • 47
  • You can use a `std::vector` of `std::function`. – wohlstad Jan 07 '23 at 10:04
  • how to you make the calls ? (I'm editing the question regarding the return type that may be different) – Jav Jan 07 '23 at 10:08
  • `std::function` probably won't do the trick here, since different return types are used, that is unless something like `std::variant` is used as parameter/return type... – fabian Jan 07 '23 at 10:11
  • c++ (unlike python) is strongly type. `data` cannot change its type, so `data = operation(data)` means the input of the functions is the same as the output (or at least convertable). You can use `std::variant` and similar, but it will not be as dynamic as in python anyway. – wohlstad Jan 07 '23 at 10:13
  • @fabian the question was edited to add the info about different return types. – wohlstad Jan 07 '23 at 10:14

3 Answers3

4

C++ is statically typed, so options here are limited:

  • Create a chain of functions that can be determined at compile time.
  • Create functions with parameter and return type being the same
  • Return a type that could "store multiple alternative types" such as std::variant

For the first alternative you could create a class template that executes functions via recursive calls, but it's a bit more complex than your python code:

template<class...Fs>
class Functions
{
    std::tuple<Fs...> m_functions;

    template<size_t index, class Arg>
    decltype(auto) CallHelper(Arg&& arg)
    {
        if constexpr (index == 0)
        {
            return std::forward<Arg>(arg);
        }
        else
        {
            return std::get<index - 1>(m_functions)(CallHelper<index - 1>(std::forward<Arg>(arg)));
        }
    }

public:
    Functions(Fs...functions)
        : m_functions(functions...)
    {
    }

    template<class Arg>
    decltype(auto) operator()(Arg&& arg)
    {
        return CallHelper<sizeof...(Fs)>(std::forward<Arg>(arg));
    }
};

int main() {
    Functions f{
        [](int x) { return x + 1; },
        [](int x) { return std::to_string(x); },
        [](std::string const& s) { return "result=" + s; }
    };

    std::cout << f(42) << '\n';
}

Note: This requires the use of a C++ standard of at least C++17.

fabian
  • 80,457
  • 12
  • 86
  • 114
  • Thanks. I think it answers the question quite well. Unfortunately it's clearly less readable than python's version and I'm not sure the c++ code readability would improve by implementing this. Thanks again. – Jav Jan 07 '23 at 10:52
  • @Jav if code readability is priority, then something is wrong with architecture or chosen tool – Swift - Friday Pie Jan 07 '23 at 13:43
3

TL;DR

Use composition from ranges:

using std::views::transform;

auto fgh = transform(h) | transform(g) | transform(f);
auto fgh_x = std::array{42} | fgh; // Calculate f(g(h(x)))
// single element range ^^
//                      ^^ ranges::single_view{42} is an alternative

std::cout << fgh_x[0]; // Result is the only element in the array.

Demo


DIY

I've written a series of articles on C++ functional programming years ago, for some thoughts on composition you can start from this one.

That said, you can also avoid the "functional nuances" and start from scratch. Here is a generic composer of callables:

template <class F, class... Fs>
auto composer(F&& arg, Fs&&... args)
{
    return [fun = std::forward<F>(arg), 
            ...functions = std::forward<Fs>(args)]<class X>(X&& x) mutable {
        if constexpr (sizeof...(Fs))
        {
            return composer(std::forward<Fs>(functions)...)(
                std::invoke(std::forward<F>(fun), std::forward<X>(x)));
        }
        else
        {
            return std::invoke(std::forward<F>(fun), std::forward<X>(x));
        }
    };
}

which you'd use as:

// Store the composed function or call it right away.
composer(lambda1, lambda2, lambda3)(42); 

Demo

Nikos Athanasiou
  • 29,616
  • 15
  • 87
  • 153
  • Is the definition of `recurse_invoke` in vour series of blog posts? If so, I'd suggest to include it here, thanks. – davidhigh Jan 07 '23 at 12:29
  • 1
    @davidhigh It's shown in the Demo link. The blog posts go a different way, implementing this logic with more "formal" FP tools, where composition is a welcome by-product, not the goal. – Nikos Athanasiou Jan 07 '23 at 12:31
1

When teaching C++ to python developers, you've got to be careful in order to overcome the "C++ is so complicated" prejudice.

In this regard, you have two options:

  • If you want to chain operations, you can directly nest lambdas just as in python. It's only a different syntax, see my anser below.

  • However, if you use the chaining more often and want to apply the linear compose(f,g,h) syntax (which save you from typing a few char's), you should generate a composer yourself. The other answers follow this path, and for brevity I'd suggest the answer of @NikosAthanasiou.

So, here is the short version: Given some variable x and assuming it is a number (as you apply +1), you can directly chain the lambdas:

auto operation = [](auto x) { return [](auto y) { return "result="+std::to_string(y); }(x+1); };

ans use it as

std::vector<int> v;  // -> fill the vector v 
std::vector<std::string> w;
for(auto& x : v)
{
    w.push_back(operation(x));
}

Only thing which you miss is the in-place mutation from int to string. For this, see the other answers using a std::variant, but why you should? ... use it only when you really need it.

davidhigh
  • 14,652
  • 2
  • 44
  • 75
  • Thanks. Maybe my Python example is oversimplified but the goal here is to have complex operations described in dedicated objects implementing `operator()`. How would it work with your implementation? I'm going to edit the question with this aspect. – Jav Jan 07 '23 at 12:25
  • 1
    Basically you have two options: accept some overhead and write a composer (see the answer of @NikosAthanasiou, with which you can write `auto h = compose(f,g)`. Or apply the composition directly using nested lamda syntax as in this answer, i.e. `auto h = [](auto&& ... x) { return g(f(std::forward(x) ...)); }`. My suggestion: if you are chaining frequently, use the composer, otherwise chain by hand. – davidhigh Jan 07 '23 at 12:34
  • Can you rephrase your question with this aspect? Also, avoid mentioning "the accepted answer" as it could change :) – Jav Jan 07 '23 at 12:35
  • @Jav: fair point, I've done some adjustments. – davidhigh Jan 07 '23 at 12:49