2
\$\begingroup\$

This is a follow-up question for A recursive_find_if Template Function with Unwrap Level Implementation in C++. I am trying to make some constrains on Proj and UnaryPredicate in recursive_all_of, recursive_find_if, recursive_any_of and recursive_none_of template functions. The recursive_invocable concept specifies that a callable type F can be called with unwrapped arguments T....

  • recursive_invocable Concept Implementation

    //  is_recursive_invocable template function implementation
    template<std::size_t unwrap_level, class F, class... T>
    requires(unwrap_level <= recursive_depth<T...>())
    static constexpr bool is_recursive_invocable()
    {
        if constexpr (unwrap_level == 0) {
            if constexpr (std::invocable<F, T...>)
                return true;
            else
                return false;
        } else if constexpr (unwrap_level > 0) {
            return is_recursive_invocable<
                        unwrap_level - 1,
                        F,
                        std::ranges::range_value_t<T>...>();
        } else {
            return false;
        }
    }
    
    //  recursive_invocable concept
    template<std::size_t unwrap_level, class F, class... T>
    concept recursive_invocable =
            is_recursive_invocable<unwrap_level, F, T...>();
    
  • recursive_project_invocable Concept Implementation

    //  is_recursive_project_invocable template function implementation
    template<std::size_t unwrap_level, class Proj, class F, class... T>
    requires(unwrap_level <= recursive_depth<T...>() &&
             recursive_invocable<unwrap_level, Proj, T...>)
    static constexpr bool is_recursive_project_invocable()
    {
        if constexpr (unwrap_level == 0) {
            if constexpr (std::invocable<F, std::invoke_result_t<Proj, T...>>)
                return true;
            else
                return false;
        } else if constexpr (unwrap_level > 0) {
            return is_recursive_project_invocable<
                        unwrap_level - 1,
                        Proj,
                        F,
                        std::ranges::range_value_t<T>...>();
        } else {
            return false;
        }
    }
    
    //  recursive_project_invocable concept
    template<std::size_t unwrap_level, class Proj, class F, class... T>
    concept recursive_project_invocable =
            is_recursive_project_invocable<unwrap_level, Proj, F, T...>();
    

Full Testing Code

The full testing code:

//  recursive_invocable and recursive_project_invocable Concept Implementation in C++

#include <algorithm>
#include <array>
#include <cassert>
#include <chrono>
#include <concepts>
#include <deque>
#include <execution>
#include <exception>
//#include <experimental/ranges/algorithm>
#include <experimental/array>
#include <functional>
#include <iostream>
#include <iterator>
#include <ranges>
#include <string>
#include <utility>
#include <vector>

//  is_reservable concept
template<class T>
concept is_reservable = requires(T input)
{
    input.reserve(1);
};

//  is_sized concept, https://codereview.stackexchange.com/a/283581/231235
template<class T>
concept is_sized = requires(T x)
{
    std::size(x);
};

template<typename T>
concept is_summable = requires(T x) { x + x; };

//  recursive_unwrap_type_t struct implementation
template<std::size_t, typename, typename...>
struct recursive_unwrap_type { };

template<class...Ts1, template<class...>class Container1, typename... Ts>
struct recursive_unwrap_type<1, Container1<Ts1...>, Ts...>
{
    using type = std::ranges::range_value_t<Container1<Ts1...>>;
};

template<std::size_t unwrap_level, class...Ts1, template<class...>class Container1, typename... Ts>
requires (  std::ranges::input_range<Container1<Ts1...>> &&
            requires { typename recursive_unwrap_type<
                                    unwrap_level - 1,
                                    std::ranges::range_value_t<Container1<Ts1...>>,
                                    std::ranges::range_value_t<Ts>...>::type; })                //  The rest arguments are ranges
struct recursive_unwrap_type<unwrap_level, Container1<Ts1...>, Ts...>
{
    using type = typename recursive_unwrap_type<
        unwrap_level - 1,
        std::ranges::range_value_t<Container1<Ts1...>>
        >::type;
};

template<std::size_t unwrap_level, typename T1, typename... Ts>
using recursive_unwrap_type_t = typename recursive_unwrap_type<unwrap_level, T1, Ts...>::type;

//  recursive_variadic_invoke_result_t implementation
template<std::size_t, typename, typename, typename...>
struct recursive_variadic_invoke_result { };

template<typename F, class...Ts1, template<class...>class Container1, typename... Ts>
struct recursive_variadic_invoke_result<1, F, Container1<Ts1...>, Ts...>
{
    using type = Container1<std::invoke_result_t<F,
        std::ranges::range_value_t<Container1<Ts1...>>,
        std::ranges::range_value_t<Ts>...>>;
};

template<std::size_t unwrap_level, typename F, class...Ts1, template<class...>class Container1, typename... Ts>
requires (  std::ranges::input_range<Container1<Ts1...>> &&
            requires { typename recursive_variadic_invoke_result<
                                    unwrap_level - 1,
                                    F,
                                    std::ranges::range_value_t<Container1<Ts1...>>,
                                    std::ranges::range_value_t<Ts>...>::type; })                //  The rest arguments are ranges
struct recursive_variadic_invoke_result<unwrap_level, F, Container1<Ts1...>, Ts...>
{
    using type = Container1<
        typename recursive_variadic_invoke_result<
        unwrap_level - 1,
        F,
        std::ranges::range_value_t<Container1<Ts1...>>,
        std::ranges::range_value_t<Ts>...
        >::type>;
};

template<std::size_t unwrap_level, typename F, typename T1, typename... Ts>
using recursive_variadic_invoke_result_t = typename recursive_variadic_invoke_result<unwrap_level, F, T1, Ts...>::type;

//  recursive_array_invoke_result implementation
template<std::size_t, typename, typename, typename...>
struct recursive_array_invoke_result { };

template<   typename F, 
            template<class, std::size_t> class Container,
            typename T,
            std::size_t N>
struct recursive_array_invoke_result<1, F, Container<T, N>>
{
    using type = Container<
        std::invoke_result_t<F, std::ranges::range_value_t<Container<T, N>>>,
        N>;
};

template<   std::size_t unwrap_level,
            typename F, 
            template<class, std::size_t> class Container,
            typename T,
            std::size_t N>
requires (  std::ranges::input_range<Container<T, N>> &&
            requires { typename recursive_array_invoke_result<
                                    unwrap_level - 1,
                                    F,
                                    std::ranges::range_value_t<Container<T, N>>>::type; })                //  The rest arguments are ranges
struct recursive_array_invoke_result<unwrap_level, F, Container<T, N>>
{
    using type = Container<
        typename recursive_array_invoke_result<
        unwrap_level - 1,
        F,
        std::ranges::range_value_t<Container<T, N>>
        >::type, N>;
};

template<   std::size_t unwrap_level,
            typename F,
            template<class, std::size_t> class Container,
            typename T,
            std::size_t N>
using recursive_array_invoke_result_t = typename recursive_array_invoke_result<unwrap_level, F, Container<T, N>>::type;

//  recursive_array_unwrap_type struct implementation, https://stackoverflow.com/a/76347485/6667035
template<std::size_t, typename>
struct recursive_array_unwrap_type { };

template<template<class, std::size_t> class Container,
              typename T,
              std::size_t N>
struct recursive_array_unwrap_type<1, Container<T, N>>
{
    using type = std::ranges::range_value_t<Container<T, N>>;
};

template<std::size_t unwrap_level, template<class, std::size_t> class Container,
              typename T,
              std::size_t N>
requires (  std::ranges::input_range<Container<T, N>> &&
            requires { typename recursive_array_unwrap_type<
                                    unwrap_level - 1,
                                    std::ranges::range_value_t<Container<T, N>>>::type; })                //  The rest arguments are ranges
struct recursive_array_unwrap_type<unwrap_level, Container<T, N>>
{
    using type = typename recursive_array_unwrap_type<
        unwrap_level - 1,
        std::ranges::range_value_t<Container<T, N>>
        >::type;
};

template<std::size_t unwrap_level, class Container>
using recursive_array_unwrap_type_t = typename recursive_array_unwrap_type<unwrap_level, Container>::type;

//  https://codereview.stackexchange.com/a/253039/231235
template<std::size_t dim, class T, template<class...> class Container = std::vector>
constexpr auto n_dim_container_generator(T input, std::size_t times)
{
    if constexpr (dim == 0)
    {
        return input;
    }
    else
    {
        return Container(times, n_dim_container_generator<dim - 1, T, Container>(input, times));
    }
}

template<std::size_t dim, std::size_t times, class T>
constexpr auto n_dim_array_generator(T input)
{
    if constexpr (dim == 0)
    {
        return input;
    }
    else
    {
        std::array<decltype(n_dim_array_generator<dim - 1, times>(input)), times> output;
        for (size_t i = 0; i < times; i++)
        {
            output[i] = n_dim_array_generator<dim - 1, times>(input);
        }
        return output;
    }
}

//  recursive_depth function implementation
template<typename T>
constexpr std::size_t recursive_depth()
{
    return std::size_t{0};
}

template<std::ranges::input_range Range>
constexpr std::size_t recursive_depth()
{
    return recursive_depth<std::ranges::range_value_t<Range>>() + std::size_t{1};
}

//  recursive_depth function implementation with target type
template<typename T_Base, typename T>
constexpr std::size_t recursive_depth()
{
    return std::size_t{0};
}

template<typename T_Base, std::ranges::input_range Range>
requires (!std::same_as<Range, T_Base>)
constexpr std::size_t recursive_depth()
{
    return recursive_depth<T_Base, std::ranges::range_value_t<Range>>() + std::size_t{1};
}

//  is_recursive_invocable template function implementation
template<std::size_t unwrap_level, class F, class... T>
requires(unwrap_level <= recursive_depth<T...>())
static constexpr bool is_recursive_invocable()
{
    if constexpr (unwrap_level == 0) {
        if constexpr (std::invocable<F, T...>)
            return true;
        else
            return false;
    } else if constexpr (unwrap_level > 0) {
        return is_recursive_invocable<
                    unwrap_level - 1,
                    F,
                    std::ranges::range_value_t<T>...>();
    } else {
        return false;
    }
}

//  recursive_invocable concept
template<std::size_t unwrap_level, class F, class... T>
concept recursive_invocable =
        is_recursive_invocable<unwrap_level, F, T...>();

//  is_recursive_project_invocable template function implementation
template<std::size_t unwrap_level, class Proj, class F, class... T>
requires(unwrap_level <= recursive_depth<T...>() &&
         recursive_invocable<unwrap_level, Proj, T...>)
static constexpr bool is_recursive_project_invocable()
{
    if constexpr (unwrap_level == 0) {
        if constexpr (std::invocable<F, std::invoke_result_t<Proj, T...>>)
            return true;
        else
            return false;
    } else if constexpr (unwrap_level > 0) {
        return is_recursive_project_invocable<
                    unwrap_level - 1,
                    Proj,
                    F,
                    std::ranges::range_value_t<T>...>();
    } else {
        return false;
    }
}

//  recursive_project_invocable concept
template<std::size_t unwrap_level, class Proj, class F, class... T>
concept recursive_project_invocable =
        is_recursive_project_invocable<unwrap_level, Proj, F, T...>();

/*  recursive_all_of template function implementation with unwrap level
*/
template<std::size_t unwrap_level, class T, class Proj = std::identity, class UnaryPredicate>
requires(   unwrap_level <= recursive_depth<T>() &&
            recursive_invocable<unwrap_level, Proj, T> &&
            recursive_project_invocable<unwrap_level, Proj, UnaryPredicate, T>)
constexpr auto recursive_all_of(T&& value, UnaryPredicate&& p, Proj&& proj = {}) {
    if constexpr (unwrap_level > 0)
    {
        return std::ranges::all_of(value, [&](auto&& element) {
            return recursive_all_of<unwrap_level - 1>(element, p, proj);
        });
    }
    else
    {
        return std::invoke(p, std::invoke(proj, value));
    }
}

/*  recursive_find_if template function implementation with unwrap level
*/
template<std::size_t unwrap_level, class T, class Proj = std::identity, class UnaryPredicate>
requires(   unwrap_level <= recursive_depth<T>() &&
            recursive_invocable<unwrap_level, Proj, T> &&
            recursive_project_invocable<unwrap_level, Proj, UnaryPredicate, T>)
constexpr auto recursive_find_if(T&& value, UnaryPredicate&& p, Proj&& proj = {}) {
    if constexpr (unwrap_level > 0)
    {
        return std::ranges::find_if(value, [&](auto& element) {
            return recursive_find_if<unwrap_level - 1>(element, p, proj);
        }) != std::ranges::end(value);
    }
    else
    {
        return std::invoke(p, std::invoke(proj, value));
    }
}

//  recursive_any_of template function implementation with unwrap level
template<std::size_t unwrap_level, class T, class Proj = std::identity, class UnaryPredicate>
requires(   unwrap_level <= recursive_depth<T>() &&
            recursive_invocable<unwrap_level, Proj, T> &&
            recursive_project_invocable<unwrap_level, Proj, UnaryPredicate, T>)
constexpr auto recursive_any_of(T&& value, UnaryPredicate&& p, Proj&& proj = {})
{
    return recursive_find_if<unwrap_level>(value, p, proj);
}

//  recursive_none_of template function implementation with unwrap level
template<std::size_t unwrap_level, class T, class Proj = std::identity, class UnaryPredicate>
requires(   unwrap_level <= recursive_depth<T>() &&
            recursive_invocable<unwrap_level, Proj, T> &&
            recursive_project_invocable<unwrap_level, Proj, UnaryPredicate, T>)
constexpr auto recursive_none_of(T&& value, UnaryPredicate&& p, Proj&& proj = {})
{
    return !recursive_any_of<unwrap_level>(value, p, proj);
}

template<std::ranges::input_range T>
constexpr auto recursive_print(const T& input, const int level = 0)
{
    T output = input;
    std::cout << std::string(level, ' ') << "Level " << level << ":" << std::endl;
    std::transform(input.cbegin(), input.cend(), output.begin(), 
        [level](auto&& x)
        {
            std::cout << std::string(level, ' ') << x << std::endl;
            return x;
        }
    );
    return output;
}

template<std::ranges::input_range T>
requires (std::ranges::input_range<std::ranges::range_value_t<T>>)
constexpr T recursive_print(const T& input, const int level = 0)
{
    T output = input;
    std::cout << std::string(level, ' ') << "Level " << level << ":" << std::endl;
    std::ranges::transform(std::ranges::cbegin(input), std::ranges::cend(input), std::ranges::begin(output),
        [level](auto&& element)
        {
            return recursive_print(element, level + 1);
        }
    );
    return output;
}

void recursive_find_if_tests()
{
    auto test_vectors_1 = n_dim_container_generator<4, int, std::vector>(1, 3);
    test_vectors_1[0][0][0][0] = 2;
    assert(recursive_find_if<4>(test_vectors_1, [](auto&& i) { return i % 2 == 0; }));

    auto test_vectors_2 = n_dim_container_generator<4, int, std::vector>(3, 3);
    assert(recursive_find_if<4>(test_vectors_2, [](auto&& i) { return i % 2 == 0; }) == false);
    
    //  Tests with std::string
    auto test_vector_string = n_dim_container_generator<4, std::string, std::vector>("1", 3);
    assert(recursive_find_if<4>(test_vector_string, [](auto&& i) { return i == "1"; }));
    assert(recursive_find_if<4>(test_vector_string, [](auto&& i) { return i == "2"; }) == false);

    //  Tests with std::string, projection
    assert(recursive_find_if<4>(
        test_vector_string,
        [](auto&& i) { return i == "1"; },
        [](auto&& element) {return std::to_string(std::stoi(element) + 1); }) == false);
    assert(recursive_find_if<4>(
        test_vector_string,
        [](auto&& i) { return i == "2"; },
        [](auto&& element) {return std::to_string(std::stoi(element) + 1); }));
    
    //  Tests with std::array of std::string
    std::array<std::string, 3> word_array1 = {"foo", "foo", "foo"};
    assert(recursive_find_if<1>(word_array1, [](auto&& i) { return i == "foo"; }));
    assert(recursive_find_if<1>(word_array1, [](auto&& i) { return i == "bar"; }) == false);

    //  Tests with std::deque of std::string
    std::deque<std::string> word_deque1 = {"foo", "foo", "foo", "bar"};
    assert(recursive_find_if<1>(word_deque1, [](auto&& i) { return i == "foo"; }));
    assert(recursive_find_if<1>(word_deque1, [](auto&& i) { return i == "bar"; }));
    assert(recursive_find_if<1>(word_deque1, [](auto&& i) { return i == "abcd"; }) == false);
    assert(recursive_find_if<2>(word_deque1, [](auto&& i) { return i == 'a'; }));
    assert(recursive_find_if<2>(word_deque1, [](auto&& i) { return i == 'b'; }));
    assert(recursive_find_if<2>(word_deque1, [](auto&& i) { return i == 'c'; }) == false);

    std::vector<std::wstring> wstring_vector1{};
    for(int i = 0; i < 4; ++i)
    {
        wstring_vector1.push_back(std::to_wstring(1));
    }
    assert(recursive_find_if<1>(wstring_vector1, [](auto&& i) { return i == std::to_wstring(1); }));
    assert(recursive_find_if<1>(wstring_vector1, [](auto&& i) { return i == std::to_wstring(2); }) == false);

    std::vector<std::u8string> u8string_vector1{};
    for(int i = 0; i < 4; ++i)
    {
        u8string_vector1.push_back(u8"\u20AC2.00");
    }
    assert(recursive_find_if<1>(u8string_vector1, [](auto&& i) { return i == u8"\u20AC2.00"; }));
    assert(recursive_find_if<1>(u8string_vector1, [](auto&& i) { return i == u8"\u20AC1.00"; }) == false);

    std::pmr::string pmr_string1 = "123";
    std::vector<std::pmr::string> pmr_string_vector1 = {pmr_string1, pmr_string1, pmr_string1};
    assert(recursive_find_if<1>(pmr_string_vector1, [](auto&& i) { return i == "123"; }));
    assert(recursive_find_if<1>(pmr_string_vector1, [](auto&& i) { return i == "456"; }) == false);
    std::cout << "All tests passed!\n";

    return;
}

void recursive_any_of_tests()
{
    auto test_vectors_1 = n_dim_container_generator<4, int, std::vector>(1, 3);
    test_vectors_1[0][0][0][0] = 2;
    assert(recursive_any_of<4>(test_vectors_1, [](auto&& i) { return i % 2 == 0; }));

    auto test_vectors_2 = n_dim_container_generator<4, int, std::vector>(3, 3);
    assert(recursive_any_of<4>(test_vectors_2, [](auto&& i) { return i % 2 == 0; }) == false);
    
    //  Tests with std::string
    auto test_vector_string = n_dim_container_generator<4, std::string, std::vector>("1", 3);
    assert(recursive_any_of<4>(test_vector_string, [](auto&& i) { return i == "1"; }));
    assert(recursive_any_of<4>(test_vector_string, [](auto&& i) { return i == "2"; }) == false);

    //  Tests with std::string, projection
    assert(recursive_any_of<4>(
        test_vector_string,
        [](auto&& i) { return i == "1"; },
        [](auto&& element) {return std::to_string(std::stoi(element) + 1); }) == false);
    assert(recursive_any_of<4>(
        test_vector_string,
        [](auto&& i) { return i == "2"; },
        [](auto&& element) {return std::to_string(std::stoi(element) + 1); }));
    
    //  Tests with std::array of std::string
    std::array<std::string, 3> word_array1 = {"foo", "foo", "foo"};
    assert(recursive_any_of<1>(word_array1, [](auto&& i) { return i == "foo"; }));
    assert(recursive_any_of<1>(word_array1, [](auto&& i) { return i == "bar"; }) == false);

    //  Tests with std::deque of std::string
    std::deque<std::string> word_deque1 = {"foo", "foo", "foo", "bar"};
    assert(recursive_any_of<1>(word_deque1, [](auto&& i) { return i == "foo"; }));
    assert(recursive_any_of<1>(word_deque1, [](auto&& i) { return i == "bar"; }));
    assert(recursive_any_of<1>(word_deque1, [](auto&& i) { return i == "abcd"; }) == false);
    assert(recursive_any_of<2>(word_deque1, [](auto&& i) { return i == 'a'; }));
    assert(recursive_any_of<2>(word_deque1, [](auto&& i) { return i == 'b'; }));
    assert(recursive_any_of<2>(word_deque1, [](auto&& i) { return i == 'c'; }) == false);

    std::vector<std::wstring> wstring_vector1{};
    for(int i = 0; i < 4; ++i)
    {
        wstring_vector1.push_back(std::to_wstring(1));
    }
    assert(recursive_any_of<1>(wstring_vector1, [](auto&& i) { return i == std::to_wstring(1); }));
    assert(recursive_any_of<1>(wstring_vector1, [](auto&& i) { return i == std::to_wstring(2); }) == false);

    std::vector<std::u8string> u8string_vector1{};
    for(int i = 0; i < 4; ++i)
    {
        u8string_vector1.push_back(u8"\u20AC2.00");
    }
    assert(recursive_any_of<1>(u8string_vector1, [](auto&& i) { return i == u8"\u20AC2.00"; }));
    assert(recursive_any_of<1>(u8string_vector1, [](auto&& i) { return i == u8"\u20AC1.00"; }) == false);

    std::pmr::string pmr_string1 = "123";
    std::vector<std::pmr::string> pmr_string_vector1 = {pmr_string1, pmr_string1, pmr_string1};
    assert(recursive_any_of<1>(pmr_string_vector1, [](auto&& i) { return i == "123"; }));
    assert(recursive_any_of<1>(pmr_string_vector1, [](auto&& i) { return i == "456"; }) == false);
    std::cout << "All tests passed!\n";

    return;
}

int main()
{
    auto start = std::chrono::system_clock::now();
    recursive_find_if_tests();
    auto end = std::chrono::system_clock::now();
    std::chrono::duration<double> elapsed_seconds = end - start;
    std::time_t end_time = std::chrono::system_clock::to_time_t(end);
    std::cout << "Computation finished at " << std::ctime(&end_time) << "elapsed time: " << elapsed_seconds.count() << '\n';
    return EXIT_SUCCESS;
}

The output of the test code above:

All tests passed!
Computation finished at Wed Feb 14 04:17:35 2024
elapsed time: 5.9873e-05

Godbolt link is here.

All suggestions are welcome.

The summary information:

  • Which question it is a follow-up to?

    A recursive_find_if Template Function with Unwrap Level Implementation in C++

  • What changes has been made in the code since last question?

    I am trying to implement recursive_invocable and recursive_project_invocable concepts for recursive_all_of, recursive_find_if, recursive_any_of and recursive_none_of template functions in this post.

  • Why a new review is being asked for?

    Please review the implementation of recursive_invocable and recursive_project_invocable concepts. Is the constraint designed properly? All suggestions are welcome.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Your code is correct, however it can be simplified in a number of ways:

Unnecessary if-statements

Whenever you see if (foo) return true; else return false;, you can replace that by return foo.

Next, consider that unwrap_level is an unsigned integer, and you already checked the case where it is zero or where it is greater than zero. The last else-branch is therefore useless.

So your code in is_recursive_invocable() can be replaced with:

if constexpr (unwrap_level == 0) {
    return std::invocable<F, T...>;
} else {
    return is_recursive_invocable<unwrap_level - 1, F,
                                  std::ranges::range_value_t<T>...>();
}

About is_recursive_project_invocable()

Note that in the standard library, there is no concept named projected_invocable(). Instead, functions like std::ranges::find_if() check for something like std::invocable<F, std::invoke_result_t<Proj, T...>> instead. You already have a way to get the recursive invoke result, so you could write:

template<class F, std::size_t unwrap_level, class Proj, class... T>
concept recursive_projected_invocable =
    std::invocable<F, recursive_variadic_invoke_result_t<unwrap_level, Proj, T...>>;

Note that I moved the template parameter F to the front (which could also be done for recursive_invocable). This allows you to use this concept as shown below:

Remove redundant constraints

Your recursive_find_if() function checks for three things in its requires-clause. However, the first two are redundant; recursive_project_invocable alone is enough to validate that the unwrap_level is correct, that Proj is valid at that level, and that the result of that can be used by the predicate. So, you can simplify it to:

template<std::size_t unwrap_level, class T, class Proj = std::identity,
         recursive_projected_invocable<unwrap_level, Proj, T> UnaryPredicate>
constexpr auto recursive_find_if(T&& value, UnaryPredicate&& p, Proj&& proj = {}) {
    …
}
\$\endgroup\$
1
  • \$\begingroup\$ Although std::ranges::find_if() check for something like std::invocable<F, std::invoke_result_t<Proj, T...>>, in the recursive case, std::invocable is going to apply to the inner most type. std::invocable<F, recursive_variadic_invoke_result_t<unwrap_level, Proj, T...>> is apply std::invocable to the outer most type after projection. This is different. \$\endgroup\$
    – JimmyHu
    Feb 19 at 6:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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