I'm trying to implement a generic curry function in C++14 that takes a callable object as an input parameter and allows currying.
Desired syntax:
auto sum3 = [](int x, int y, int z){ return x + y + z; };
int main()
{
assert(curry(sum3)(1)(1)(1) == 3);
auto plus2 = curry(sum3)(1)(1);
assert(plus2(1) == 3);
assert(plus2(3) == 5);
}
My implementation idea is as follows: have the curry function return an unary function that binds its argument to a future call to the original function, recursively. Call the bound original function on the "last recursive step".
Detecting the "last recursive step" is the problematic part.
My idea was detecting whether or not the current bound function (during the recursion) was legally callable with zero arguments, using a is_zero_callable type trait:
template <typename...>
using void_t = void;
template <typename, typename = void>
class is_zero_callable : public std::false_type
{
};
template <typename T>
class is_zero_callable<T, void_t<decltype(std::declval<T>()())>>
: public std::true_type
{
};
Unfortunately, I cannot seem to find a way to check the correct function for zero-argument "callability" - I need to somehow check if the function that will be returned from the currently bound function is zero-callable.
Here's what I've got so far (godbolt link):
template <typename TF, bool TLastStep>
struct curry_impl;
// Base case (last step).
// `f` is a function callable with no arguments.
// Call it and return.
template <typename TF>
struct curry_impl<TF, true>
{
static auto exec(TF f)
{
return f();
}
};
// Recursive case.
template <typename TF, bool TLastStep>
struct curry_impl
{
static auto exec(TF f)
{
// Bind `x` to subsequent calls.
return [=](auto x)
{
// This is `f`, with `x` bound as the first argument.
// (`f` is the original function only on the first recursive
// step.)
auto bound_f = [=](auto... xs)
{
return f(x, xs...);
};
// Problem: how to detect if we need to stop the recursion?
using last_step = std::integral_constant<bool, /* ??? */>;
// `is_zero_callable<decltype(bound_f)>{}` will not work,
// because `bound_f` is variadic and always zero-callable.
// Curry recursively.
return curry_impl<decltype(bound_f),
last_step{}>::exec(bound_f);
};
}
};
// Interface function.
template <typename TF>
auto curry(TF f)
{
return curry_impl<TF, is_zero_callable<decltype(f)>{}>::exec(f);
}
Is my approach/intuition viable? (Is it actually possible to stop the recursion by detecting whether or not we've reached a zero-arg-callable version of the original function?)
...or is there a better way of solving this problem?
(Please ignore the missing perfect forwarding and the lack of polish in the example code.)
(Note that I've tested this currying implementation using an user-specified template int TArity parameter to stop the recursion, and it worked properly. Having the user manually specify the arity of the original f function is unacceptable, however.)
The minimal changes required to make this work in Clang is
auto bound_f = [=](auto... xs) -> decltype(f(x, xs...))
// ^^^^^^^^^^^^^^^^^^^^^^^^^
{
return f(x, xs...);
};
using last_step = std::integral_constant<bool,
is_zero_callable<decltype(bound_f)>{}>;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Explicitly specifying the return type should make it SFINAE-friendly and capable of being detected by is_zero_callable. Unfortunately, GCC is unhappy with this, probably due to a bug.
A generic lambda is basically a class with a templated operator(), so we can just write it ourselves:
template<class F, class T>
struct bound_function {
F f;
T arg;
template<class... Args>
auto operator()(Args... args) const -> decltype(f(arg, args...)){
return f(arg, args...);
}
};
Note that I'm imitating the semantics of the generic lambda here and making the operator() const. A full-featured implementation will likely want to overload on constness and value categories.
Then
auto bound_f = bound_function<TF, decltype(x)>{f, x};
works in both GCC and Clang, but has a theoretical problem: when only f(arg) is valid (and not with extra arguments), then instantiating bound_function (which instantiates a declaration of its operator()) is ill-formed NDR because every valid specialization of operator()'s declaration requires an empty parameter pack.
To avoid this, let's specialize bound_function for the "no further arguments needed" case. And since we are computing this information anyway, let's just express it in a member typedef.
template<class F, class T, class = void>
struct bound_function {
using zero_callable = std::false_type;
F f;
T arg;
template<class... Args>
auto operator()(Args... args) const -> decltype(f(arg, args...)){
return f(arg, args...);
}
};
template<class F, class T>
struct bound_function<F, T, void_t<decltype(std::declval<const F&>()(std::declval<const T&>()))>> {
using zero_callable = std::true_type;
F f;
T arg;
decltype(auto) operator()() const {
return f(arg);
}
};
Then
auto bound_f = bound_function<TF, decltype(x)>{f, x};
using last_step = typename decltype(bound_f)::zero_callable;
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