Pull to refresh

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 ) )();

Поясните, что вы имели в виду, я не понял.


Давайте я тоже переформулирую:
Я утверждаю, что ваш код не эквивалентен моему, поэтому предлагать его на замену, как минимум, некорректно.

С лямбдами будет скорее всего все как у вас, просто кортеж переедет в 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);

ideone

Допустим, класс 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 и т.д.)

Спасибо за полезную ссылку.

Как-то не слишком много внимания уделено целесообразности. Мне, например, не очевидно, что существует реальный кейс, в котором недостаточно std::bind. Типа, у меня уже есть функция, у которой я хочу зафиксировать несколько аргументов, но я почему-то ещё не знаю точно, сколько у неё этих аргументов вообще. Не представляю себя в подобной ситуации. Обычно, когда у меня есть конкретная функция, то я уже знаю о ней вообще всё. Уж количество аргументов так точно.

Можно развить мой пример из статьи про сравнение по модулю n.


Только теперь у нас будет не сравнение по модулю, а произведение по модулю. Первым аргументом оно так же будет принимать, собственно, модуль, а остальными — значения, которые нужно перемножить.


auto modulo_product (auto modulo, auto ... as);

auto f = part(modulo_product, 7); // Произведение по модулю 7.
auto g = part(modulo_product, 13); // Произведение по модулю 13.

С стд::биндом такое не изобразишь.

Действительно. Через std::bind так сделать не получится. Признаю, что это реальный кейс, имеющий смысл. Хотя, справедливости ради, нужно сказать, что желание делать такое — это симптом проблемы под названием «функциональное программирование головного мозга». В плюсах такое выглядит чересчур инородно. Даже при том, что это мультипарадигмальный язык.
… желание делать такое — это симптом проблемы под названием «функциональное программирование головного мозга». В плюсах такое выглядит чересчур инородно.

Обоснуете?

Под мультипарадигмальностью языка я понимаю то, что он позволяет разумно сочетать различные парадигмы. Не нужно писать на плюсах так, как будто это функциональный язык. Чрезмерный перекос в сторону функциональной парадигмы точно так же неуместен, как и полный глухой отказ от всех функциональных штучек.

Вы ведь статью не читали, да?

Читал. Но более двух часов назад. Поэтому уже всё забыл.

Дело в том, что у меня в статье есть следующая фраза:


Всё же не стоит забывать, что C++ не является функциональным языком. Нельзя так просто взять и перенести конструкции и идеологию одного языка на другой.

Именно поэтому я и создаю инструмент, который, по моему мнению, прекрасно вливается в идеологию плюсов. Не создаёт никаких перекосов и сильно упрощает жизнь в определённых ситуациях.

Тогда нельзя засчитать пример с произведением по модулю. Это чисто функциональная мулька. В нефункциональных языках не принято делать функции, которые не имеют вообще никакого собственного содержания. Нужно фиксировать либо алгоритм, либо параметры. Структурное программирование называется структурным, именно потому что базовые блоки, из которых составляется программа, имеют определённую структуру. Они не являются полностью абстрактными операциями, приобретающими какой-либо смысл только в контексте использования. Хорошую функцию в нефункциоальном языке нельзя использовать где угодно, как угодно и для чего угодно. Обычно её можно использовать только так, как это было задумано автором. И он, естественно, не должен задумывать такое использование, которое ничем не ограничено. Если он задумывает чисто абстрактную функцию, которая может делать что угодно в зависимости от параметров, то он просто махровый функциональщик. Такой автор должен перестать обманывать себя, совершить каминг-аут и прекратить изображать из себя обычного программиста.
Это чисто функциональная мулька.

В нефункциональных языках не принято…

… в нефункциоальном языке нельзя…

… не должен задумывать ...

Пожалуйста, обоснуйте.

Всмысле? Как ещё я могу это обосновать? Что ещё тут можно добавить? Неужели для вас является новостью то, что у функциональных языков есть некоторые дистинктивные особенности, которые отличают их от других и делают их функциональными? По большому счёту таких особенностей две. Первая состоит в том, что абстрактность функций порой опасно приближается к границе с абсурдом. Чтобы получить нормальную полноценную функцию, которая делает что-то конкретное, часто нужно соединить вместе пяток абстрактных недофункций, каждая из которых привносит в это действие какой-то отдельный аспект. Вторая особенность состоит в упоре на иммутабилити и избегании побочных эффектов. Обе эти особенности являются дистинктивными. Нефункциональным языкам они не свойственны. Про программиста, который упорно старается применить их в нефункциональном языке везде, где только может, говорят, что у него ФПГМ. Хотя умеренное применение лямбд и std::bind'а — это современная норма.
Всмысле? Как ещё я могу это обосновать? Что ещё тут можно добавить?

Ну пока что обоснований не было. Были только эмоции.


Правильно ли я понял, что использовать лямбды и std::bind можно, а частичное применение в том виде, в котором я предлагаю, — нет?


Ну то есть если я напишу


const auto f =
    [modulo] (auto ... as)
    {
        return modulo_product(modulo, as...);
    };

то всё хорошо.
Но как только я написал


const auto f = part(modulo_product, modulo);

то я уже богомерзкий функциональщик, покушающийся на основы мироздания?

Именно так. В первом случае вы создаёте лямбду в локальном контексте. Она как-то там используется. Желательно однократно. И затем быстро уничтожается. Так сказать, быстро использованная и сразу же разрушенная лямбда не считается созданной. Во втором же случае вы оформляете процесс создания таких функций в отдельную функцию, придавая тем самым этому процессу более высокий статус. Сделав такое, вы больше не можете говорить, что вы просто балуетесь функциональными штучками в мультипарадигменном языке. Потому что такая манера совершенно точно является функциональным стилем.

Это как если вы перекусываете гамбургером по пути куда-то в целях экономии времени, то это одно. Это не маркирует вас каким-то особым отношением к гамбургерам. Всякое бывает в дороге. Захотелось есть, а времени было только на гамбургер. Все всё понимают. И совсем другое дело, когда вы арендуете зал для торжественных приёмов, организуете статусное мероприятие, но при этом подаёте гостям гамбургеры. Когда вы делаете такое, вы не можете оправдаться тем, что просто сделали всё на скорую руку, и что просто не хватило времени на приготовление настоящей еды. Вас обязательно обвинят в том, что вы форсите применение гамбургеров в неуместном для этого контексте.

Дружище, извини, но это какая-то ахинея.

Ничего страшного. Это стандартное развитие старого как мир сюжета. То, что мои рассуждения в конце концов будут квалифицированы как чушь, было понятно ещё в самый первый момент, когда в ответ на моё оценочное суждение были затребованы обоснования. Это как рак. Если бы люди не умирали от других причин, то в конце концов они умирали бы от рака. Так же и здесь. Всякий раз, если не происходит ничего по-настоящему необычного, любому собеседнику рано или поздно удаётся полностью разобраться в ситуации и понять, что я несу чушь.
UFO just landed and posted this here
А можно просто писать на D, а не усложнять себе жизнь.

alias f = partial!(modulo_product, 7);

И чем же это проще того, что сделано у меня?

Тем, что это уже сделано за вас.

Начнём с того, что partial в языке D не поддерживает произвольное количество аргументов. Можно зафиксировать только один.


Следовательно, утверждение "это уже сделано за вас" ложно.


К тому же непонятно, что мне делать с тем, чего в языке D нет. Снова искать другой язык? Как выдумаете, удастся ли найти язык, в котором есть всё?

Вы можете объявить свою функцию и передавать сколько угодно аргументов как хотите.

auto f(A...)(A args) { return foo(1, 2, 3, args, 4, 5); }
UFO just landed and posted this here

Я в этом случае просто создавал лямбду. Это не очень чисто идеологически, зато дёшево и практично.

Лямбды — это хорошо.


Но на практике выясняется, что лямбды очень часто повторяются. То есть приходится в разных участках кода в пределах одного проекта определять одинаковые лямбды. Пусть даже относительно простые.


К тому же у лямбд очень громоздкий синтаксис. Поэтому если в определённых ситуациях удаётся "собрать" функцию из "кубиков" вместо того, чтобы определять её заново, то это очень приятно.
Вот std::bind и моё частичное применение в этом помогают.

Sign up to leave a comment.

Articles