In C++14, what is a good way to curry functions or function objects?
In particular, I have an overloaded function foo
with some random number of overloads: some overloads may be found via ADL, others may be defined in a myriad of places.
I have a helper object:
static struct {
template<class...Args>
auto operator()(Args&&...args)const
-> decltype(foo(std::forward<Args>(args)...))
{ return (foo(std::forward<Args>(args)...));}
} call_foo;
that lets me pass the overload set around as a single object.
If I wanted to curry foo
, how should I do so?
As curry
and partial function application are often used interchangeably, by curry
I mean if foo(a,b,c,d)
is a valid call, then curry(call_foo)(a)(b)(c)(d)
must be a valid call.
Here is code i have been playing with while learning variadic templates.
It is toy example for ATD for function pointers, and ATD on std::function.
I have made example on lambdas, but havent find way to extract aruments, so no ATD for lamnda (yet)
Here is my current best attempt.
SFINAE utility helper class:
A traits class that tells you if Sig is a valid invokation -- ie, if
std::result_of<Sig>::type
is defined behavior. In some C++ implementations simply checkingstd::result_of
is enough, but that isn't required by the standard:Curry helper is sort of a manual lambda. It captures a function and one argument. It isn't written as a lambda so we can enable proper rvalue forwarding when it is used in an rvalue context, which is important when currying:
The meat and potatoes:
The final function is humorously short.
Note that no type erasure is done. Also note that the theoretical hand-crafted solution can have far fewer
move
s, but I don't think I needlessly copy anywhere.Here is a test function object:
And some test code to see how it works:
live example
The resulting code is more than a bit of a mess. Lots of methods had to be repeated 3 times to handle
&
,const&
and&&
cases optimally. The SFINAE clauses are long and complex. I ended up using both variardic args and varargs, with the varargs there to ensure a non-important signature difference in the method (and lower priority I think, not that it matters, the SFINAE ensures only one overload is ever valid, exceptthis
qualifiers).The result of
curry(call_foo)
is an object that can be called one argument at a time, or many arguments at a time. You can call it with 3 arguments, then 1, then 1, then 2, and it does mostly the right thing. No evidence is exposed telling you how many arguments it wants, other than just trying to feed it arguments and seeing if the call is valid. This is required to handle overloading cases.A quirk of the multiple-argument case is that it won't partially pass the packet to one
curry
, and use the rest as arguments to the return value. I could change that relatively easily by changing:to
and the other two similar ones. That would prevent the technique of "jumping over" an overload that would otherwise be valid. Thoughts? It would mean that
curry(foo)(a,b,c)
would be logically identical tocurry(foo)(a)(b)(c)
which seems elegant.Thanks to @Casey whose answer inspired much of this.
Most recent revision. It makes
(a,b,c)
behave much like(a)(b)(c)
unless it is call that works directly.live version
Here's my attempt using eager semantics, i.e., returning as soon as enough arguments are accumulated for a valid call to the original function (Demo at Coliru):
and an alternate implementation without eager semantics that requires an extra
()
to invoke the partial application making it possible to access e.g. bothf(int)
andf(int, int)
from the same overload set:This isn't a trivial problem. Getting ownership semantics right is tricky. For comparison, let's consider a few lambdas and how they express ownership:
My implementation by default captures by value, captures by reference when passed a
std::reference_wrapper<>
(same behavior asstd::make_tuple()
withstd::ref()
), and forwards the invoking arguments as is (lvalues remain lvalues, rvalues remain rvalues). I couldn't decide on a satisfactory solution formutable
, so all value captures are effectivelyconst
.Capture of a move-only type makes the functor move-only. This in turn means if
c
is acurry_t
andd
is a move-only type, andc(std::move(d))
doesn't invoke the captured functor, then bindingc(std::move(d))
to a lvalue means subsequent calls either have to contain enough arguments to invoke the captured functor, or the lvalue must be converted to an rvalue (possibly viastd::move()
). This took some care with reference qualifiers. Keep in mind that*this
is always an lvalue, so the ref-qualifiers had to be applied tooperator()
.There's no way to know how many arguments the captured functor needs as there can be any number of overloads, so be careful. No
static_assert
s thatsizeof...(Captures) < max(N_ARGS)
.Overall, the implementation takes about 70 lines of code. As discussed in the comments, I followed the convention of
curry(foo)(a, b, c, d)
andfoo(a, b, c, d)
being (roughly) equivalent, allowing access to every overload.Live demo on Coliru demonstrating reference qualifier semantics.