Comments 40
template <typename ... As>
constexpr auto part (As && ... as)
Почему нельзя использовать "стандартную" сигнатуру?
template<typename F, typename ... As >
constexpr auto part (F && f, As &&... as )
Это позволит проконтролировать, что первым аргументом должен идти объект, над которым будет выполненно преобразование. А если сохранить объект отдельно от кортежа, то можно вызывать просто apply. Стандартные apply и invoke оба обрабатывают callable-объекты, и скорее всего apply будет реализован через invoke.
Проконтролировать в каком смысле? Проверить соответствие концепту?
Да, можно будет проверить. Ну и по аргументам видно что передавать, но это видимо ближе к плюсам, если отталкивать от функционального программирования то ваш вариант предпочтительнее.
Лично мне непонятно, как это проверять.
В аргументах может прийти и функция, и функциональный объект, и лямбда, и reference_wrapper
, и указатель на поле класса, и указатель на метод класса.
А можно ли произвести вызов определяет std::invoke
, но не в момент захвата аргументов, а в момент "добрасывания" оставшихся.
Вот для понятности кода действительно можно было бы выделить первый аргумент. Ну и для избежания лишнего вызова std::invoke
тоже. Просто мне не хотелось писать метафункцию unwrap_reference
, потому что std::make_tuple
и так делает всё необходимое.
В целом замечание справедливое, буду думать.
template <class F, class... Args>
auto part( F f, Args... args )
{
return [=]( auto&&... args2 ) {
return f( args..., std::forward<decltype( args2 )>( args2 )... );
};
}
Понимаете ли вы, что этот код не эквивалентен моему? И почему?
struct functor {
void operator()( int ) const
{
std::cout << "int\n";
}
void operator()( std::reference_wrapper<int> ) const
{
std::cout << "std::reference_wrapper<int>\n";
}
};
int k = 10;
part( functor(), k )();
part( functor(), std::ref( k ) )();
Поясните, что вы имели в виду, я не понял.
Давайте я тоже переформулирую:
Я утверждаю, что ваш код не эквивалентен моему, поэтому предлагать его на замену, как минимум, некорректно.
О, давайте так.
У меня есть список из пары тестиков: https://github.com/izvolov/burst/blob/master/test/functional/part.cpp
Напишите ваш код на лямбдах так, чтобы он проходил все тесты.
С лямбдами будет скорее всего все как у вас, просто кортеж переедет в capture.
Типа токого
template<typename ...As>
constexpr decltype(auto) part(As && ...as)
{
return [as = std::make_tuple(std::forward<As>(as)...)](auto && ...as2)
{
return apply(invoke, std::tuple_cat(forward_tuple(as),
std::forward_as_tuple(as2...)));
};
}
или такого
template<typename F, typename ...As>
constexpr decltype(auto) part(F && f, As && ...as)
{
return [f = std::forward<F>(f),
a = std::make_tuple(std::forward<As>(as)...)](auto&&... as2)
{
return apply(f, std::tuple_cat(forward_tuple(a),
std::forward_as_tuple(as2...)));
};
}
У вас вроде понагляднее.
Кстате, следует упомянуть что forward_as_tuple
и invoke
у вас — это дополнительные функциональные объекты, обертки над стандартными функциями. Стандартную функцию в apply таким образом не запихать.
К сожалению, так не получится.
Да, с копированием и переносом проблема решена, но с лямбдами не получится учесть различия в вызове между lvalue
и rvalue
. У меня для этого и создан функциональный объект с отдельными операторами "()".
А различать их нужно для того, чтобы не копировать лишний раз захваченные аргументы при вызове, если известно, что вызывающий объект — rvalue
. В этом случае захваченные аргументы "выбрасываются" наружу.
А различать их нужно для того, чтобы не копировать лишний раз захваченные аргументы при вызове, если известно, что вызывающий объект — rvalue
Так вроде дополнительного копирования и не будет, они же вроде по ссылкам уйдут.
auto c = Caller();
// только одно копирование, при создание объекта part, при создание кортежа.
// при вызове долнительного копирование не будет
part(c)(3.14);
Допустим, класс Caller
определён так:
struct Caller
{
auto operator () (std::string s) const
{
return s;
}
};
Тогда в следующем коде произойдёт копирование строки в момент вызова:
part(Caller{}, std::string("qwerty"))();
Соглашусь, лямбдами это не покрыть. Интересно, part мне начинает нравится.
Забавно, что схожее предложение на днях было опубликовано в рамках WG21.
С другой стороны данная статья может быть использована как наглядный пример для дальнейшего устранения недостатков лямбд (variadic move-capture, const mutable lambda call, rvalue lambda и т.д.)
Можно развить мой пример из статьи про сравнение по модулю n
.
Только теперь у нас будет не сравнение по модулю, а произведение по модулю. Первым аргументом оно так же будет принимать, собственно, модуль, а остальными — значения, которые нужно перемножить.
auto modulo_product (auto modulo, auto ... as);
auto f = part(modulo_product, 7); // Произведение по модулю 7.
auto g = part(modulo_product, 13); // Произведение по модулю 13.
С стд::биндом
такое не изобразишь.
… желание делать такое — это симптом проблемы под названием «функциональное программирование головного мозга». В плюсах такое выглядит чересчур инородно.
Обоснуете?
Вы ведь статью не читали, да?
Дело в том, что у меня в статье есть следующая фраза:
Всё же не стоит забывать, что C++ не является функциональным языком. Нельзя так просто взять и перенести конструкции и идеологию одного языка на другой.
Именно поэтому я и создаю инструмент, который, по моему мнению, прекрасно вливается в идеологию плюсов. Не создаёт никаких перекосов и сильно упрощает жизнь в определённых ситуациях.
Это чисто функциональная мулька.
В нефункциональных языках не принято…
… в нефункциоальном языке нельзя…
… не должен задумывать ...
Пожалуйста, обоснуйте.
Всмысле? Как ещё я могу это обосновать? Что ещё тут можно добавить?
Ну пока что обоснований не было. Были только эмоции.
Правильно ли я понял, что использовать лямбды и std::bind
можно, а частичное применение в том виде, в котором я предлагаю, — нет?
Ну то есть если я напишу
const auto f =
[modulo] (auto ... as)
{
return modulo_product(modulo, as...);
};
то всё хорошо.
Но как только я написал
const auto f = part(modulo_product, modulo);
то я уже богомерзкий функциональщик, покушающийся на основы мироздания?
Это как если вы перекусываете гамбургером по пути куда-то в целях экономии времени, то это одно. Это не маркирует вас каким-то особым отношением к гамбургерам. Всякое бывает в дороге. Захотелось есть, а времени было только на гамбургер. Все всё понимают. И совсем другое дело, когда вы арендуете зал для торжественных приёмов, организуете статусное мероприятие, но при этом подаёте гостям гамбургеры. Когда вы делаете такое, вы не можете оправдаться тем, что просто сделали всё на скорую руку, и что просто не хватило времени на приготовление настоящей еды. Вас обязательно обвинят в том, что вы форсите применение гамбургеров в неуместном для этого контексте.
Дружище, извини, но это какая-то ахинея.
alias f = partial!(modulo_product, 7);
И чем же это проще того, что сделано у меня?
Начнём с того, что partial
в языке D не поддерживает произвольное количество аргументов. Можно зафиксировать только один.
Следовательно, утверждение "это уже сделано за вас" ложно.
К тому же непонятно, что мне делать с тем, чего в языке D нет. Снова искать другой язык? Как выдумаете, удастся ли найти язык, в котором есть всё?
Я в этом случае просто создавал лямбду. Это не очень чисто идеологически, зато дёшево и практично.
Лямбды — это хорошо.
Но на практике выясняется, что лямбды очень часто повторяются. То есть приходится в разных участках кода в пределах одного проекта определять одинаковые лямбды. Пусть даже относительно простые.
К тому же у лямбд очень громоздкий синтаксис. Поэтому если в определённых ситуациях удаётся "собрать" функцию из "кубиков" вместо того, чтобы определять её заново, то это очень приятно.
Вот std::bind
и моё частичное применение в этом помогают.
Элементы функционального программирования в C++: частичное применение