Home > OS >  chaining callables in C
chaining callables in C

Time:01-08

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…

CodePudding user response:

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.

CodePudding user response:

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)))
// input as static range^
std::cout << fgh_x[0]; // Result is the only element in the array.

Demo (simple use)

Demo (create a composer abstraction)


To get a better feeling of the challenges this presents in C , try to re-write your Python example using type hints. Even though it seems "impossible" to solve for the general case, in C you can do it in many ways. Function composition, the way you use it here to feed the result of the leftmost operation to the operation on its right, is a "left fold" operator and if you're willing to venture into functional programming lands writing the following is doable:

auto cfs = compose(f1, f2, f3);
std::cout << cfs(2, 3) << std::endl;

i.e. even having varying number of inputs, as long as the call chain can be composed. I've written a series of articles on the topic years ago, probably this being closest to the subject.

That said, you can also avoid the "functional nuances" and start from scratch using some cool C 20 features. Here is the centerpiece of a general function composer:

template <class... F>
auto composer(F&&... args)
{
    return [...functions = args](auto x)
    {
        return recurse_invoke(x, functions...);
    };
}

which you'd use as:

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

The composer uses a C 20 variadic capture clause, which allows it to manage chains of arbitrary length. Of course, a proper implementation would handle argument forwarding, which is shown in the following

Demo

A slight variation which needs no helper functions is (here with perf-forwarding handling):

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));
        }
    };
}

Demo

CodePudding user response:

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.

  • Related