While building a small lambda-based metaprogramming library, I had the necessity of using recursion in a C++14 generic lambda, to implement a left-fold.
My own solution was passing the lambda itself as one of its parameters, like this:
template <typename TAcc, typename TF, typename... Ts>
constexpr auto fold_l_impl(TAcc acc, TF f, Ts... xs)
{
// Folding step.
auto step([=](auto self)
{
return [=](auto y_acc, auto y_x, auto... y_xs)
{
// Compute next folding step.
auto next(f(y_acc, y_x));
// Recurse if required.
return static_if(not_empty(y_xs...))
.then([=]
{
// Recursive case.
return self(self)(next, y_xs...);
})
.else_([=]
{
// Base case.
return next;
})();
};
});
// Start the left-fold.
return step(step)(acc, xs...);
}
step
is the "main" lambda that starts off the recursion. It returns a function with the desired left-fold signature (accumulator, current item, remaining items...).
The function calls itself recursively by using self(self)(next, y_xs...)
.
I've recently come across this proposal that wants to add a Y Combinator to the Standard Library, and after reading it, it seems extremely similar to what I am doing here.
Unfortunately, the concept of the Y Combinator still doesn't "click" for me - I am missing something and I cannot visualize how to generalize what I did with the self
parameter for any function, avoiding the step
boilerplate.
I've read this excellent StackOverflow answer regarding the matter, but it still didn't "click" for me.
(From that answer) a recursive factorial is defined this way:
fact =
(recurs) =>
(x) =>
x == 0 ? 1 : x * recurs(x - 1);
The recurs
parameter seems to have the same role as my self
parameter. What I do not understand is how recurs
is called without passing recurs
into itself again.
I have to call self
like this: self(self)(params...)
.
recurs
, however, is called like recurs(params...)
.
Attempting to call self(params...)
results in a compiler error informing me that self
requires only a single parameter (which is the auto self
lambda parameter).
What am I missing here? How could I rewrite my fold_l_impl
lambda in such a way that its recursion could be generalized through the use of a Y Combinator?
The Y combinator is a central concept in lambda calculus, which is the formal foundation of functional languages. Y allows one to define recursive functions without using self-referential definitions.
The Y combinator is a formula which lets you implement recursion in a situation where functions can't have names but can be passed around as arguments, used as return values, and defined within other functions. It works by passing the function to itself as an argument, so it can call itself.
A lambda expression, sometimes referred to as a lambda function or as a lambda, enables you to write anonymous (unnamed) functions “in place”. This is particularly useful when you want to pass an operation as an argument to an algorithm. It makes the Standard Library algorithms more usable.
It is a convenient way to define an anonymous function object or functor. It is convenient because we can define it locally where we want to call it or pass it to a function as an argument. Lambda is easy to read too because we can keep everything in the same place.
Here is a y combinate where the lambda is passed a recurs that doesn't need to be passed recurs:
template<class F>
struct y_combinate_t {
F f;
template<class...Args>
decltype(auto) operator()(Args&&...args)const {
return f(*this, std::forward<Args>(args)...);
}
};
template<class F>
y_combinate_t<std::decay_t<F>> y_combinate( F&& f ) {
return {std::forward<F>(f)};
};
then you do:
return y_combinate(step)(acc, xs...);
and change
return self(self)(next, y_xs...);
to
return self(next, y_xs...);
the trick here is I used a non-lambda function object that has access to its own this
, which I pass to f
as its first parameter.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With