Currying is the technique of transforming a function that takes multiple arguments in such a way that it can be called as a chain of functions, each with a single argument. I've discussed Currying on this blog previously in Fun With Lambdas C++14 Style and Dependently-Typed Curried printf. Both blogposts discuss currying of functions proper. I.e., they discuss how C++ can treat functions as values at runtime.
However, currying is not limited to just functions. Types can also be curried---if they take type arguments. In C++, we call them templates. Templates are "functions" at type level. For example, passing two type arguments
So, the question today is: Can C++ templates be curried? As it turns out, they can be. Rather easily. So, here we go...
The technique is very simple. There's a function called
Note that C++ allows templates to have default type arguments. Therefore, a template could be instantiated by providing "minimum" number of arguments. For example,
When
However, currying is not limited to just functions. Types can also be curried---if they take type arguments. In C++, we call them templates. Templates are "functions" at type level. For example, passing two type arguments
std::string
and int
to std::map
gives std::map<std::string, int>
. So std::map
is a type-level function that takes two (type) arguments and gives another type as a result. They are also known as type constructors.
So, the question today is: Can C++ templates be curried? As it turns out, they can be. Rather easily. So, here we go...
#include <type_traits> #include <functional> #include <map> #include <iostream> template <template <class...> class C, class... T, class D = C<T...>> constexpr std::true_type valid(std::nullptr_t); template <template <class...> class C, class... T> constexpr std::false_type valid(...); template <class TrueFalse, template <class...> class C, class... ArgsSoFar> struct curry_impl; template <template <class...> class C, class... ArgsSoFar> struct curry_impl<std::true_type, C, ArgsSoFar...> { using type = C<ArgsSoFar...>; }; template <template <class...> class C, class... ArgsSoFar> struct curry_impl<std::false_type, C, ArgsSoFar...> { template <class... MoreArgs> using apply = curry_impl<decltype(valid<C, ArgsSoFar..., MoreArgs...>(nullptr)), C, ArgsSoFar..., MoreArgs...>; }; template <template <class...> class C> struct curry { template <class... U> using apply = curry_impl<decltype(valid<C, U...>(nullptr)), C, U...>; }; int main(void) { using CurriedIsSame = curry<std::is_same>; static_assert(curry<std::is_same>::apply<int>::apply<int>::type::value); curry<std::less>::apply<int>::type less; std::cout << std::boolalpha << less(5, 4); // prints false using CurriedMap = curry<std::map>; using MapType = CurriedMap::apply<int>::apply<long, std::less<int>, std::allocator<std::pair<const int, long>>>::type; static_assert(std::is_same<MapType, std::map<int, long>>::value); }Wandbox
The technique is very simple. There's a function called
valid
that has two overloads. The first one returns std::true_type
only if C<T..>
is a valid instantiation of template C
with argument list T...
. Otherwise, it returns std::false_type
. C
is type constructor that we would like to curry. This function uses the SFINAE idiom.
curry_impl
is the core implementation of template currying. It has two specializations. The std::true_type
specialization is selected when valid
returns std::true_type
. I.e., curried version of the type constructor has received the minimum number of type arguments to form a complete type. In other words, ArgsSoFar
are enough. curry_impl<C, ArgsSoFar...>::type
is same as instantiation of the type constructor with the valid type arguments (C<ArgsSoFar...>
).
Note that C++ allows templates to have default type arguments. Therefore, a template could be instantiated by providing "minimum" number of arguments. For example,
std::map
could be instantiated in three ways giving the same type:
std::map<int, long>
std::map<int, long, std::less<int>>
std::map<int, long, std::less<int>, std::pair<const int, long>>
When
ArgsSoFar
are not enough, curry_impl<std::false_type>
carries the partial list of type arguments (ArgsSoFar
) at class template level. It allows passing one or more type arguments (MoreArgs
) to the type constructor through the apply
typedef. When ArgsSoFar
and MoreArgs
are enough to form a valid instantiation, curry_impl<std::true_type>
is chosen which yields the fully instantiated type.
Comments