3

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.)

T.C.
  • 133,968
  • 17
  • 288
  • 421
Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • 1
    You need sfinae augmentation. Without that your task is impractical; there is no way to soft fail on argument mismatch. See [here](http://stackoverflow.com/q/26655685/1774667) for my curry question (and synthesized answer below) – Yakk - Adam Nevraumont Dec 27 '15 at 17:18
  • Clang is happy if you give `bound_f` an explicit return type (`-> decltype(f(x, xs...))`) so that it SFINAEs, and then just use `is_zero_callable{}`. GCC isn't, though. – T.C. Dec 27 '15 at 18:49
  • 1
    If you write the generic lambda as a standalone class template instead (with the return-type SFINAE on `operator()`), then it works with both compilers. However, I'm slightly concerned that in the terminal case we may run into the "every valid specialization requires an empty pack => ill-formed NDR" rule. – T.C. Dec 27 '15 at 19:02

2 Answers2

4

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;
T.C.
  • 133,968
  • 17
  • 288
  • 421
  • The quote you implicitly refer to regarding empty packs is presumably defective, at least due to issue 1785 (and its consequences here are unintended, no?). – Columbo Jan 15 '16 at 00:27
  • @Columbo For CWG 1785, I think the defect is in the first sentence rather than the second. http://wg21.cmeerw.net/cwg/issue2067 is more on point. – T.C. Jan 15 '16 at 00:31
  • Weird, I didn't see that one when looking up [temp.res]; it certainly is. What's the fundamental reason such templates are disallowed, anyway? – Columbo Jan 15 '16 at 01:19
  • @Columbo It's added by [CWG 1231](http://wg21.link/CWG1231). Looking at the examples given there, I can sort of see why they banned them... – T.C. Jan 28 '16 at 04:06
1

under file check. please.

https://github.com/sim9108/Study2/blob/master/SDKs/curryFunction.cpp

// ConsoleApplication4.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include <string>
#include <functional>
#include <type_traits>

// core function
template<typename FN, std::size_t N>
struct curry
{
    FN fn_;
    curry(FN fn) :fn_{ fn }
    {
    }

    template<typename... TS, typename = std::enable_if_t< (N - sizeof...(TS)) != 0, int >>
    auto operator()(TS... ts1) {
        auto fn = [f = this->fn_, ts1...](auto... args) mutable {
            return f(ts1..., args...);
        };
        return curry<decltype(fn), N - sizeof...(TS)>(fn);
    }

    template<typename... TS, typename Z = void, typename = std::enable_if_t< (N - sizeof...(TS)) == 0, int > >
    auto operator()(TS... ts1) {
        return fn_(ts1...);
    }
};

//general  make curry function
template<typename R, typename... Args>
auto make_curry(R(&f)(Args...)) {
    auto fn = [&f](Args... args) {
        return f(args...);
    };
    return curry<decltype(fn), sizeof...(Args)>(fn);
}

//general  make curry member function
template<typename C, typename R, typename... Args>
auto make_curry(R(C::*f)(Args...), C c) {
    auto fn = [f, c](Args... args) mutable {
        return (c.*f)(args...);
    };
    return curry<decltype(fn), sizeof...(Args)>(fn);
}

template<typename C, typename R, typename... Args>
auto make_curry(R(C::*f)(Args...) const, C c) {
    auto fn = [f, c](Args... args) mutable {
        return (c.*f)(args...);
    };
    return curry<decltype(fn), sizeof...(Args)>(fn);
}

//general  make curry lambda function
template<typename C>
auto make_curry(C&& c) {
    using CR = std::remove_reference_t<C>;
    return make_curry(&CR::operator(), c);
}

using std::string;
using std::function;

string func(string a, string b, string c) {
    return "general function:" + a + b + c;
}

struct A {
    string func(string a, string b, string c) {
        return "member function:" + a + b + c;
    };
};

int main(int argc, char* argv[])
{
    {  //general function curry
        auto c = make_curry(func);

        auto m1 = c("t1")("t2")("t3");
        auto m2 = c("test1")("test2")("test3");

        auto m3 = c("m1");
        auto m4 = m3("m2");
        auto m5 = m4("m3");

        std::cout << m5 << std::endl;
        std::cout << m2 << std::endl;
        std::cout << m5 << std::endl;
    }

    { //member function curry
        A a;
        auto c = make_curry(&A::func, a);

        auto m1 = c("t1")("t2")("t3");
        auto m2 = c("test1")("test2")("test3");

        auto m3 = c("m1");
        auto m4 = m3("m2");
        auto m5 = m4("m3");

        std::cout << m1 << std::endl;
        std::cout << m2 << std::endl;
        std::cout << m5 << std::endl;
    }
    { //lambda function curry
        auto fn = [](string a, string b, string c) {
            return "lambda function:" + a + b + c;
        };
        auto c = make_curry(fn);

        auto m1 = c("t1")("t2")("t3");
        auto m2 = c("test1")("test2")("test3");

        auto m3 = c("m1");
        auto m4 = m3("m2");
        auto m5 = m4("m3");

        std::cout << m1 << std::endl;
        std::cout << m2 << std::endl;
        std::cout << m5 << std::endl;

    }

    auto func3 = make_curry(func);
    std::cout << func3("Hello, ")( "World!", " !hi") << std::endl;
    std::cout << func3("Hello, ","World!")(" !hi") << std::endl;
    std::cout << func3("Hello, ","World!", " !hi") << std::endl;
    std::cout << func3()("Hello, ", "World!", " !hi") << std::endl;
    return 0;
}
윤훈남
  • 11
  • 1