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…

5
  • You can use a std::vector of std::function.
    – wohlstad
    Commented Jan 7, 2023 at 10:04
  • how to you make the calls ? (I'm editing the question regarding the return type that may be different)
    – Jav
    Commented Jan 7, 2023 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
    Commented Jan 7, 2023 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
    Commented Jan 7, 2023 at 10:13
  • @fabian the question was edited to add the info about different return types.
    – wohlstad
    Commented Jan 7, 2023 at 10:14

4 Answers 4

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.

2
  • 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
    Commented Jan 7, 2023 at 10:52
  • @Jav if code readability is priority, then something is wrong with architecture or chosen tool Commented Jan 7, 2023 at 13:43
4

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

2
  • Is the definition of recurse_invoke in vour series of blog posts? If so, I'd suggest to include it here, thanks.
    – davidhigh
    Commented Jan 7, 2023 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. Commented Jan 7, 2023 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.

4
  • 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
    Commented Jan 7, 2023 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<decltype(x)>(x) ...)); }. My suggestion: if you are chaining frequently, use the composer, otherwise chain by hand.
    – davidhigh
    Commented Jan 7, 2023 at 12:34
  • Can you rephrase your question with this aspect? Also, avoid mentioning "the accepted answer" as it could change :)
    – Jav
    Commented Jan 7, 2023 at 12:35
  • @Jav: fair point, I've done some adjustments.
    – davidhigh
    Commented Jan 7, 2023 at 12:49
0

I needed something that would work in C++14 and this is what I came up with:

template<typename TCallable>
auto&& _compose(TCallable&& aCallable)
{
    return aCallable;
}

template<typename TCallable, typename... TCallables>
auto _compose(TCallable&& aCallable, TCallables&&... aCallables)
{
    return [&](auto&& aPreviousStep, auto&&... aArgs) {
        auto lStep = aCallable(aPreviousStep, aArgs...);
        return _compose(aCallables...)(lStep, aArgs...);
    };
}

I return a lambda in which I take a single Callable (aCallable), get the "step value" (lStep) from it, and then pass it to a new Callable made of rest of them (aCallables) using the same function (compose).

I also pass all the shared arguments (aArgs) together with the "step value".

Then I need to capture the Callables:

template<typename... TCallables>
auto compose(TCallables... aCallables)
{
    return [=](auto&& aPreviousStep, auto&&... aArgs) {
        return _compose(aCallables...)(aPreviousStep, aArgs...);
    };
}

or their captures won't stay in the memory

It can be used like this:

std::cout << compose(
    [](auto x, auto shared1, auto shared2) { return x + 1; },
    [](auto x, auto shared1, auto shared2) { return std::to_string(x); },
    [](auto s, auto shared1, auto shared2) { return "result=" + s; }
)(42, 0, 0);

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.