3

I have tried to write some code to implement a curried function in C++17. My curring implementation is down below (I will give you a minimal working example at the bottom of this question).

template <class Function, class... CapturedArgs>
class curried{
private:
    using CapturedArgsTuple = std::tuple<std::decay_t<CapturedArgs>...>;
    template <class... Args>
    static auto capture_by_value(Args&&... args){
        return std::tuple<std::decay_t<Args>...>(std::forward<Args>(args)...);
    }
public:
    curried(Function function, CapturedArgs&&... args)
        : m_function(function), m_capture(capture_by_value(std::move(args)...)){}

    curried(Function function, std::tuple<CapturedArgs...> args)
        : m_function(function), m_capture(std::move(args)){}

    template <class... NewArgs>
    auto operator()(NewArgs&&... args){
        auto new_args = capture_by_value(std::forward<NewArgs>(args)...);
        auto all_args = std::tuple_cat(m_capture, new_args);
        if constexpr(std::is_invocable_v<Function, CapturedArgs..., NewArgs...>){
            return std::apply(m_function, all_args);
        }else{
            return curried<Function, CapturedArgs..., NewArgs...>(m_function, all_args);
        }
    }
private:
    Function m_function;
    std::tuple<CapturedArgs...> m_capture;
};

And here is a test function:

void func(const string& str1, string& str2, string str3){
    str2 += "str2 ";
    cout << "str1 = " << str1 
         << ", str2 = " << str2 
         << ", str3 = " << str3 << endl;
}
int main(){
    string str1 = "Hello ", str2 = "World", str3 = "!";
    auto test = curried(func);
    auto test_two = test(std::cref(str1))(std::ref(str2));
    cout << "result : ";
    test_two(str3);
}

So far so good. I can see some log print on my terminal, like:

$ result : str1 = Hello , str2 = Worldstr2 , str3 = !

I got two question here:

The first one is How can I invoke a curried function by passing it a rvalue reference? I have already tried everything I can search for, but the result is either compile error or nothing.

void func_1(const string& str1, string& str2, string&& str3){
    str2 += "str2 ";
    cout << "str1 = " << str1 
         << ", str2 = " << str2 
         << ", str3 = " << str3 << endl;
}
int main(){
    string str1 = "Hello ", str2 = "World", str3 = "!";
    auto test = curried(func_1);
    auto test_two = test(std::cref(str1))(std::ref(str2));
    cout << "result : ";
    // test_two(std::move(str3)); Compile Error
    // test_two(string("!"));     Compile Error
    test_two(std::bind(std::move<string&>, str3));  // Compile successfully, but there's nothing output
}

In the process of solving the first problem, I found something strange. Here is an example:

void func_2(const string& str1, string& str2, string str3, string& str4){
    str2 += "str2 ";
    cout << "str1 = " << str1 
         << ", str2 = " << str2 
         << ", str3 = " << str3 
         << ", str4 = " << str4 << endl;
}
int main(){
    string str1 = "Hello ", str2 = "World", str3 = "!", str4 = "abc";
    auto test = curried(func_2);
    auto test_two = test(std::cref(str1))(std::ref(str2))(str3);
    cout << "result : ";
    test_two(std::ref(str4)); 
}

When I used func_2 to test my curried function, I got some error message:

$ g++ curried.cc -std=c++17
curried.cc: In instantiation of ‘auto curried<Function, CapturedArgs>::operator()(NewArgs&& ...) [with NewArgs = {std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&}; Function = void (*)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&); CapturedArgs = {std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >}]’:
curried.cc:60:15:   required from here
curried.cc:28:11: error: no matching function for call to ‘curried<void (*)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&), std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>::curried(void (*&)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&), std::tuple<std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&)’
   28 |    return curried<Function, CapturedArgs..., NewArgs...>(m_function, all_args);
      |           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
curried.cc:18:2: note: candidate: ‘curried<Function, CapturedArgs>::curried(Function, std::tuple<_Elements ...>) [with Function = void (*)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&); CapturedArgs = {std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&}]’
   18 |  curried(Function function, std::tuple<CapturedArgs...> args)
      |  ^~~~~~~
curried.cc:18:57: note:   no known conversion for argument 2 from ‘tuple<std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >>’ to ‘tuple<std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>’
   18 |  curried(Function function, std::tuple<CapturedArgs...> args)
      |                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~
curried.cc:15:2: note: candidate: ‘curried<Function, CapturedArgs>::curried(Function, CapturedArgs&& ...) [with Function = void (*)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&); CapturedArgs = {std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&}]’
   15 |  curried(Function function, CapturedArgs&&... args)
      |  ^~~~~~~
curried.cc:15:2: note:   candidate expects 4 arguments, 2 provided
curried.cc:7:7: note: candidate: ‘constexpr curried<void (*)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&), std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>::curried(const curried<void (*)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&), std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>&)’
    7 | class curried{
      |       ^~~~~~~
curried.cc:7:7: note:   candidate expects 1 argument, 2 provided
curried.cc:7:7: note: candidate: ‘constexpr curried<void (*)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&), std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>::curried(curried<void (*)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&), std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>&&)’
curried.cc:7:7: note:   candidate expects 1 argument, 2 provided
curried.cc: In function ‘int main()’:
curried.cc:60:10: error: void value not ignored as it ought to be
   60 |  test_two(str3)(std::ref(str4));
      |  ~~~~~~~~^~~~~~

So the second question is why can't I put the string parameter str3 before the string reference one str4? Why the order of parameter definition matter makes me totally at sea.


For your convenience, here is a minimal working example:

#include <iostream>
#include <functional>
#include <tuple>
using namespace std;

template <class Function, class... CapturedArgs>
class curried{
private:
    using CapturedArgsTuple = std::tuple<std::decay_t<CapturedArgs>...>;
    template <class... Args>
    static auto capture_by_value(Args&&... args){
        return std::tuple<std::decay_t<Args>...>(std::forward<Args>(args)...);
    }
public:
    curried(Function function, CapturedArgs&&... args)
        : m_function(function), m_capture(capture_by_value(std::move(args)...)){}

    curried(Function function, std::tuple<CapturedArgs...> args)
        : m_function(function), m_capture(std::move(args)){}

    template <class... NewArgs>
    auto operator()(NewArgs&&... args){
        auto new_args = std::make_tuple(std::forward<NewArgs>(args)...);
        auto all_args = std::tuple_cat(m_capture, std::move(new_args));
        if constexpr(std::is_invocable_v<Function, CapturedArgs..., NewArgs...>){
            return std::apply(m_function, all_args);
        }else{
            return curried<Function, CapturedArgs..., NewArgs...>(m_function, all_args);
        }
    }
private:
    Function m_function;
    std::tuple<CapturedArgs...> m_capture;
};

void func_1(const string& str1, string& str2, string&& str3){
    str2 += "str2 ";
    cout << "str1 = " << str1 
         << ", str2 = " << str2 
         << ", str3 = " << str3 << endl;
}

void func_2(const string& str1, string& str2, string str3, string& str4){
    str2 += "str2 ";
    cout << "str1 = " << str1 
         << ", str2 = " << str2 
         << ", str3 = " << str3 
         << ", str4 = " << str4 << endl;
}

int main()
{
    /* code */
    string str1 = "Hello ", str2 = "World", str3_for_func_1 = "!", 
           str3_for_func_2 = "!", str4 = "abc";
    auto question_1 = curried(func_1);  // For the first question
    auto question_2 = curried(func_2);  // For the second question
    auto question_1_two_params = question_1(std::cref(str1))(std::ref(str2));
    auto question_2_two_params = question_2(std::cref(str1))(std::ref(str2));   
    cout << "result : ";
    //question_1_two_params(std::move(str3_for_func_1));  // Compile Error  
    //question_1_two_params(string("abc"));  // Compile Error   
    //auto question_2_three_params = question_2_two_params(str3_for_func_2);  // Compile Error
    //question_2_three_params(std::ref(str4)); // It should output some log like "result : str1 = Hello, balabala..."
    return 0;
}

Compile Command:

$ g++ curryied.cc -std=c++17 -o curried 

My working environment is :

OS : Ubuntu-20.04 Compiler : gcc version 9.3.0

3
  • Have you tried giving a look at Functional Programming in C++? Here's the repo with the examples, and making a function curried is one of the examples.
    – Enlico
    Commented Feb 21, 2021 at 9:11
  • By the way, when it comes to actually being in need of this functionality as a user (as in you want to use it, not code it), be aware of boost::hana::curry, at least if you know the maximum number of arguments you want to curry.
    – Enlico
    Commented Feb 21, 2021 at 9:13
  • Yes, I'm reading this book. And in fact, the implementation of the curried class comes from an example in this book. That's a good book Commented Feb 21, 2021 at 9:26

1 Answer 1

2

Problem 1

One problem is at the line std::apply(m_function, all_args); where you are passing all_args as an lvalue to std::apply, which will pass it as an lvalue to func_1's third parameter, which will fail, because func_1's third parameter is an rvalue ref, which can't bind to an lvalue argument.

Indeed, changing that line to std::apply(m_function, std::move(all_args)); makes the first two // Compile Error lines actually compile and generate the correct output. Likewise, I would call std::move also on the other usage of all_args.

Problem 2

It looks like std::make_tuple(std::forward<NewArgs>(args)...); is not doing what you think it does. Changing it to std::tuple<NewArgs&&...>(std::forward<NewArgs>(args)...); solves the problem; that is equivalent to std::forward_as_tuple(std::forward<NewArgs>(args)...);.

The details of why this change works lie in the return types of std::make_tuple vs. std::forward_as_tuple: the latter returns a tuple of references, whereas the former returns a tuple of values which have been copied/moved from the arguments.

Now, follow my reasoning:

  • First, look at curried(Function function, std::tuple<CapturedArgs...> args): it takes the paramter args which should be of type std::tuple<CaptureArgs...>. Are we sure args has that type? Well, if template type deduction took place, it would be obvious that the answer is yes. However, the call to that constructor never takes advantange of type deduction, because the only call is in return curried<Function, CapturedArgs..., NewArgs...>(m_function, all_args); where the template arguments are explicitly provided.
  • So the question remains: is all_args of the type the construtor expects? Well, the template arguments CapturedArgs..., NewArgs... in the recursive call correspond to the class... CapturedArgs template parameters of the class, which are used to form the type of the argument of the constructor, std::tuple<CaptureArgs...>.
  • So the answer to this question is given by static_asserting, before the recursive return, that all_args is of type std::tuple<CapturedArgs..., NewArgs...>:
    static_assert(std::is_same_v<decltype(all_args), std::tuple<CapturedArgs..., NewArgs...>>);
    return curried<Function, CapturedArgs..., NewArgs...>(m_function, all_args);
    
  • Unfortunately you can't put this assertion in the code, as long as you pass values wrapped in std::ref/std::cref, because those fail the static_assertion but are still valid inputs, exactly because of how std::reference_wrappers work. You could write a more complicated assertion or you can momentarily change std::ref(bla) to bla and so on, and check that the static_assert I gave you passes when using std::forward_as_tuple and fails when using std::make_tuple.

Thanks for asking this question. It was a great opportunity for me to dive again into this complicated topic and finally understand it!

One more point

Above I suggested that you use std::forward_as_tuple(std::forward<NewArgs>(args)...);.

Well, probably this suggestion is wrong.

At page 238, the author explicitly states that he wants the tuple to store copies, to prevent scenarios where the curried function survives its arguments. Therefore, it's probably best to go with this instead (notice, there's no && in the template argument passed to std::tuple):

        auto new_args = std::tuple<NewArgs...>(std::forward<NewArgs>(args)...);
7
  • 1
    In the first place, I tried to search for something about rvalue reference and std::apply which can give me some hints, and google gave me this post: stackoverflow.com/questions/34877699/…. You can see the second answer and then you will understand why I did this. Thanks for your answer, my first question is solved while the second is not. I have corrected my code. Commented Feb 21, 2021 at 11:16
  • I have no idea too. If I pass std::ref(str3) and std::ref(str4) to question_2_two_params and question_2_three_params, then everything goes well. However, it's not my intent. The code can work follow my intent is when and only when the only one value parameter be put to the last position in the parameter list. Commented Feb 21, 2021 at 11:52
  • 1
    @PhoenixChao, I have provided a complete answer now.
    – Enlico
    Commented Feb 21, 2021 at 14:11
  • @PhoenixChao, in hindsight, I would change the title of your question, as it turned out that the issue was not related to the interface of operator().
    – Enlico
    Commented Feb 21, 2021 at 14:21
  • That's it!! That's it!!! Thank you so much! I am still thinking about this question, but I haven't come up with anything yet. I think your answer is right. Commented Feb 21, 2021 at 14:27

Not the answer you're looking for? Browse other questions tagged or ask your own question.