C++ Variadic templates. Каррирование и частичное применение

Доброго времени суток, уважаемое Хабрасообщество.
Недавно приходилось наблюдать дискуссию о каррировании и частичном применении. Суть этой полемики состояла в том, что лучше, для практических целей, иметь в языке программирования: встроенное частичное применение (например, как в Nemerle) или встроенное каррирование (как, например, в Haskell).
Nemerle:
def sum3(x: int, y: int, z: int): int // определяем функцию
{
  x + y + z;
};
def sum3y = sum3(_, 5, _); // передаем только второй аргумент
def sum3yz = sum3y(_, 5); // передаем еще третий
def sum3yzx = sum3yz(5); // …и первый, получаем 15

Haskell:
sum3 x y z = x + y + z -- определяем функцию
sum3x = sum3 5 -- передаем только первый аргумент
sum3xy = sum3x 5 -- передаем второй
sum3xyz = sum3xy 5 -- …и третий, получаем 15

Лично я думаю, что нужно реализовать обе сущности. Тем более уже достаточно времени прошло от того момента когда в gcc появились возможности из грядущего стандарта C++, а именно Variadic templates. Как вы поняли, в статье предлагается реализация каррирования и частичного применения для C++ с помощью Variadic templates. В ходе работы использовались MinGW gcc 4.6.1 и Code::Blocks 10.05.

Каррирование


Начнем с каррирования, тем более что это интуитивно понятно. Целью будем считать функцию высшего порядка, которая принимает функцию и возвращает ее каррированный вариант. Далее этой функции можно передать произвольное количество аргументов, получая в результате другую функцию, которая принимает остальные аргументы и выдает окончательный результат.
std::function< int(int, int, int) > f =
[] ( int x, int y, int z )
{
    return x + y + z;
};
auto f1 = carry(f);
auto f2 = f1(5, 5);
auto v15 = f2(5);

Нам нужен объект, который будет хранить целевую функцию, и сам будет вести себя как функция, что принимает нефиксированное количество аргументов. А результатом действия этого объекта-функции на свои аргументы должен быть другой объект, который кроме функции еще сохраняет переданные аргументы, поскольку в C++ данные, которые на стеке, живут не вечно, то их нужно именно скопировать, то есть для такого случая нужны копируемые объекты. Адреса, конечно, никто не запрещал. Философствовать в этом направлении можно очень долго и, я думаю, для каждого случая найти оптимальное решение. Можно даже, в перспективе, обозначить как-то, нужно ли копировать тот или иной аргумент, а может ссылки будет достаточно.
Далее этот объект должен вести себя как простая функция и принимать конкретное количество аргументов, а именно оставшиеся. Итак, нам нужен шаблонный класс, который зависит от типа целевой функции. Далее, для сохранения переданных аргументов нужны их количество и типы, а для определения результирующей функции нужны количество и типы оставшихся аргументов. Экспериментальным путем было определено, что лучше всего передавать шаблону тип функции и два набора индексов: переданных и оставшихся. Реализация ориентировалась на обертку std::function, а аргументы сохранялись в std::tuple. Также использовался ряд вспомогательных шаблонов для манипуляции числами и типами во время компиляции – надеюсь, их имена будут хорошим пояснением их сути, поскольку они сами могут претендовать на отдельную библиотеку, и описывать их тут не представляется возможным.
Ниже приведен код класса, который сохраняет функцию и данные, а также код класса, который ведет себя как функция, принимающая нефиксированное количество аргументов. Хочу обратить внимание на обильное использование pack expansion для шаблонов.
template< class, class, class >
class CarryHolder;
template< class OUT_TYPE, class... IN_TYPES, uint32_t... CAP_INDEXES, uint32_t... RES_INDEXES >
class CarryHolder< std::function< OUT_TYPE ( IN_TYPES... ) >,
    UintContainer< CAP_INDEXES... >, // индексы захваченных аргументов
    UintContainer< RES_INDEXES... > > // индексы оставшихся аргументов
{
public:
    typedef std::function< OUT_TYPE ( IN_TYPES... ) > FuncType; //  тип целевой функции
    typedef std::tuple<
        typename UnQualify< // убираем const и &
            typename GetNthType< CAP_INDEXES, TypeContainer<IN_TYPES...> >::Result
        >::Result...
    > CleanCapTupleType; // тип кортежа хранящего захваченные аргументы
    typedef std::function< OUT_TYPE
        ( typename GetNthType< RES_INDEXES, TypeContainer<IN_TYPES...> >::Result... )
    > ReturnFuncType; // тип результирующей функции

public:
    CarryHolder( FuncType const& f,
        typename UnQualify<
            typename GetNthType< CAP_INDEXES, TypeContainer<IN_TYPES...> >::Result
        >::Result const&... capturedValues ):
        _function(f), _capturedData( capturedValues... ) { };

    CarryHolder( CarryHolder const& other ):
        _function(other._function), _capturedData( other._capturedData ) { };

    // принимает оставшиеся аргументы
    inline OUT_TYPE operator()( typename GetNthType< RES_INDEXES, TypeContainer<IN_TYPES...> >::Result... other_values )
    {
        // разворачиваем сохраненные аргументы и только что полученные
        return _function( std::get< CAP_INDEXES > (_capturedData)..., other_values ... );
    };

private:
    CarryHolder();
    FuncType _function;
    CleanCapTupleType _capturedData;
};

template< class >
class Carry;
template< class OUT_TYPE, class... IN_TYPES >
class Carry< std::function< OUT_TYPE (IN_TYPES...) > >
{
public:
    typedef typename std::function< OUT_TYPE (IN_TYPES...) > FuncType;
    constexpr static uint32_t ArgsCount = GetTypesLength< TypeContainer<IN_TYPES...> >::Result;

    Carry( Carry const& carry ): _function( carry._function ) { };
    Carry( FuncType const& f ): _function( f ) { };

    template< class... INNER_IN_TYPES >
    inline auto operator()( INNER_IN_TYPES const& ... values ) ->
        typename CarryHolder<
            FuncType,
            // генерируем последовательность индексов захваченных аргументов
            typename UintRange< GetTypesLength< TypeContainer<INNER_IN_TYPES...> >::Result >::Result,
            // генерируем последовательность индексов оставшихся аргументов
            typename UintRangeFromTo< GetTypesLength< TypeContainer<INNER_IN_TYPES...> >::Result, ArgsCount >::Result
        >::ReturnFuncType // возвращаем CarryHolder завернутый в std::function
    {
        typedef CarryHolder<
            FuncType,
            typename UintRange< GetTypesLength< TypeContainer<INNER_IN_TYPES...> >::Result >::Result,
            typename UintRangeFromTo< GetTypesLength< TypeContainer<INNER_IN_TYPES...> >::Result, ArgsCount >::Result
        > CarryHolderSpec;
        return CarryHolderSpec( _function, values... );
    };

private:
    Carry();
    FuncType _function;
};

template< class FUNC_TYPE >
Carry<FUNC_TYPE> carry( FUNC_TYPE const& f ) // функция для удобства
{
    return Carry<FUNC_TYPE>(f);
};


Перестановка аргументов


Для реализации частичного применения можно использовать подход, состоящий в перестановке аргументов местами с дальнейшим каррированием. Будет это выглядеть так:
std::function< int(int, int, int) > f =
[] ( int x, int y, int z )
{
    return x + y + z;
};
auto f1 = permute<2, 1, 0>(f);

Нам, как и раньше, понадобиться объект, который будет хранить целевую функцию, но вести себя он будет как обычная функция, принимающая конкретное число аргументов, а точнее переставленные аргументы целевой функции. Для этого нужен шаблонный класс, который зависит от типа функции и от последовательности индексов – перестановки аргументов. Также необходимым будет шаблон для дополнения перестановки (пояснения ниже) и поиска обратной перестановки (инверсный индекс). Прямая перестановка нужна для формирования последовательности типов входных параметров, а обратная для вставки аргументов во время вызова функции. Также используется внутренний класс для развертывания типа инверсного индекса. Ниже приведен код класса, который реализует данный функционал.
template< class, class >
class Permutation;
template< class OUT_TYPE, class... IN_TYPES, uint32_t... INDEXES >
class Permutation<
    std::function< OUT_TYPE (IN_TYPES...) >, // тип целевой функции
    UintContainer< INDEXES... > > // перестановка аргументов
{
public:
    typedef std::function< OUT_TYPE (IN_TYPES...) > FuncType;
    typedef std::function< OUT_TYPE (typename GetNthType< INDEXES, TypeContainer<IN_TYPES...> >::Result...) > NewFuncType;

    Permutation( Permutation const& perm ): _function( perm._function ) { };
    Permutation( FuncType const& f ): _function( f ) { };

private:
    // вспомогательный метод для развертывания найденной обратной перестановки
    template< uint32_t... INVERSE >
    inline OUT_TYPE apply( UintContainer< INVERSE... >, // нам нужен только тип, т.е. индексы
        typename GetNthType< INDEXES, TypeContainer<IN_TYPES...> >::Result... values )
    {
        // сохраняем аргументы в std::tuple по индексу перестановки
        std::tuple< typename GetNthType< INDEXES, TypeContainer<IN_TYPES...> >::Result... > data( values... );
        // извлекаем аргументы из std::tuple по инверсному индексу
        return _function( std::get< INVERSE > (data)... );
    };

public:
    inline OUT_TYPE operator()( typename GetNthType< INDEXES, TypeContainer<IN_TYPES...> >::Result... values )
    {
        // находим инверсную перестановку
        typename InversePermutation< UintContainer<INDEXES...> >::Result inverse;
        return apply( inverse, values... );
    };

private:
    Permutation();
    FuncType _function;
};

// функция для удобства; заворачивает Permutation в std::function
template< uint32_t... INDEXES, class FUNC_TYPE >
auto permute( FUNC_TYPE const& f ) ->
    typename Permutation<FUNC_TYPE,
        // дополняем перестановку, т.е. добавляем в конец недостающие индексы
        typename ComplementRange<
            GetArgumentsCount<FUNC_TYPE>::Result, UintContainer < INDEXES... >
        >::Result
    >::NewFuncType
{
    typedef Permutation<FUNC_TYPE,
        typename ComplementRange<
            GetArgumentsCount<FUNC_TYPE>::Result, UintContainer < INDEXES... >
        >::Result
    > PermutationType;
    return PermutationType(f);
};

Теперь частичное применение, имея описанный функционал, становиться тривиальным – ниже код.
template< uint32_t... INDEXES, class FUNC_TYPE >
auto partApply( FUNC_TYPE const& f ) ->
    decltype(carry(permute<INDEXES...>(f)))
{
    return carry(permute<INDEXES...>(f));
};

А учитывая возможность дополнения индексов, это можно использовать указывая не все индексы:
std::function< int(int, int, int) > f =
[] ( int x, int y, int z )
{
    return x + y + z;
};
auto f1 = permute<2>(f); // эквивалентно <2, 0, 1> - значит "ввести третий аргумент первым"

Вот такие возможности открывают Variadic templates. Позже, если будет интересно, выложу код.
На самом деле каррирование вышло не совсем классическим, поскольку является одношаговым и «обязательным», то есть, передав каррированной (в смысле реализованного функционала) функции все аргументы, все равно получим функцию, которая не принимает аргументов. Также неклассическая суть заметна во время манипуляции с квалификаторами. Но все это — особенности C++.

UPDATE:
Обнаружил, что CarryHolderSpec в Carry::operator() не нужно заворачивать без надобности в std::function, поскольку происходит повторное копирование аргументов. Но, думаю, ссылки на временные объекты помогут это обойти.

UPDATE:
Перенес топик в «Ненормальное программирование», думаю тут ему будет уютней.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 62

    +11
    Надеюсь мне не придётся когда нибудь разбираться в таком коде.
      –1
      много шаблонов?
        +8
        много синтаксических хитростей вокруг простой сущности
          +4
          Начинает напоминать Perl. Там берёшь любую комбинацию символов. Ну типа
          &**#&^^*QWT а это оказывается рабочая программа.
            0
            тут ответил, что не так уж много (точнее: много одной синтаксической хитрости), а сущность хоть и простая, но в C++ ее реализации я еще не встречал
              +2
              В том-то все и дело, что сущность простая, а реализация совсем очень даже не. Хороший язык — тот, который позволяет простые идеи реализовывать просто. Это не про С++.
                +2
                но мы ведь с Вами находимся в ветке C++ — тут все пишется на C++: и простое, и сложное=). Не обделять же C++ программистов функциональными прелестями.
                  +2
                  Мне кажется, если язык не предназначен для простой реализации простых идей, то стоит задуматься, а надо ли это вообще? Может выбрать другой инструмент? Можно для шурупа использовать молоток, но может лучше взять отвертку? А вообще статья интересная. По крайней мере понятно, что в продакшене лучше сюда не ходить :)
                    +1
                    я уже тут ответил, что эта реализация — не сложная, просто маловато опыта у людей с шаблонами нового стандарта. А как было сказано тут, статью можно использовать чтоб лучше разобраться с новыми возможностями.
                  0
                  Хороший язык предоставляет хороший компромисс между простотой описания, скоростью работы и универсальностью решений. Это про C++
            0
            > Надеюсь мне не придётся когда нибудь разбираться в таком коде.

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

              Есть два подмножества C++ одно из них для разработчиков библиотек, другое для пользователей этих библиотек. И, действительно, оставаясь пользователем библиотеки совершенно не обязательно знать её внутреннее устройство.

              Поэтому, ответ Да, не придётся, если не хотите. Однажды загрузите boost и без проблем воспользуетесь готовой функциональностью.

              C++, фактически не изменяясь множество лет, позволял идти в ногу с современными тенденциями языков программирования без потери своей эффективности. Развивались идеи использования существующих возможностей, в отличие от подходов множества других языков, которые появляются на пике интереса к определённой философии программирования, справляют похороны C++, затем на каждое модное веянье язык начинает пухнуть, тяжелеть и, в конце концов, обрушивается под собственным весом. Появляется новый язык, снова хоронит C++ и всё повторяется…
              0
              много шаблонов?
                +4
                Да.

                Сколько человек сходу разберется в этих конструкциях, если им понадобится их чуть чуть подправить?

                Чем больше я читаю таких постов, тем больше понимаю, что все должно быть просто, а языки надо развивать не в сторону дополнительных извращений, а в сторону простоты…
                  0
                  На самом деле, все, что используется здесь, — это развертывание шаблонных типов или аргументов (pack expansion). А выглядит оно страшно только потому, что используется нетривиальным (но очевидным) способом, т.е. не просто
                  Templ<T...>
                  но и
                  Templ1<Templ2<T>...>
                  или
                  g(f<v>...)
                  а может также потому, что это фича с нового стандарта и еще мало кем используется. А все остальное — это обыкновенное ООП: несколько классов с обычными методами и конструкторами — даже наследования нету.
                    +2
                    Наверное Вы правы, но сходу когда смотришь на код, выглядит все довольно необычно и запутано. Ко всему новому надо привыкать, а чтобы было желание привыкать, надо увидеть пользу, которая перевешивает новые проблемы. Тут же сразу в глаза бросается страшный код и о пользе даже не думаешь:)
                    0
                    Мне нравится направление развития языка D. Совершенно непонятно, зачем в C++ пошли противоположным путём.
                  +1
                  Интересно, но я ни разу ни сталкивался с тем, чтобы иметь необходимость такого функционала в С++, пока что.
                    0
                    Есть возможное удобное применение — callback'и. Они используются достаточно широко и часто нужно иметь возможность их конфигурировать — я видел два варианта:
                    1. 1. передавать вместе с callback'ом нужные данные, а потом во время вызова callback'а передавать их ему;
                    2. 2. писать класс с полями, с помощью инициализации которых и будет конфигурироваться callback.

                    Данный же подход позволяет использовать лишь функцию. А при передачи ей нужных аргументов происходит то самое конфигурирование callback'а.
                      0
                      Позвольте, а чем обычные функторы не подходят в случае callback'ов?
                        +1
                        Если нужно передавать только callback, но все же нужны для него некоторые данные, то используется функтор с полями:
                        struct GreaterThan // определяем
                        {
                            const int _value;
                            GreaterThan( int value ): _value(value) { };
                            inline bool operator()( int arg ) { return arg > value; };
                        };
                        auto callback = GreaterThan(3); // конфигурируем

                        Данный же подход позволяет обойтись определением только одной функции:
                        inline bool gt( int x, int y ) { return x > y; }; // определяем
                        auto callback = partApply<1>(gt)(3); // конфигурируем
                        +3
                        boost::bind(&MyClass::method, this, x, y) чем плохо?
                          0
                          Не знал, почитал, спасибо: это, действительно, то же. Тогда пост можно рассматривать как объяснение реализации или, возможно, другой подход.
                            0
                            Это не то, у boost::bind нет произвольного кол-ва аргументов.
                              +1
                              то есть функтор, получаемый с помощью boost::bind, имеет точное кол-во аргументов.
                                +1
                                И что? Функция получаемая в результате каррирования или частичного применения по определению имеет определённое количество аргументов.
                                Если нужно, то можно к ней ещё раз применить каррирование или частичного применение.
                      +3
                      Haskell Brooks Curry != carry.
                        +3
                        Выглядит как полный ппц!
                        Теоретически это мб интересно. Но практически — это bad design!
                          +1
                            0
                            это соответствующий проект для CodeBlocks
                            0
                            Пример интересен как пруф-оф-консепт или в блог ненормальное программирование :-)
                            Применение каррирование и прочих функциональных штук в строго-типизированном языке — это извращение, на мой взгляд.
                              +3
                              типизация Haskell'а намного «строже» C++ной
                              +2
                              Вы переизобрели std::bind. Он есть в стандартной библиотеке c++11.
                              У него более универсальный синтаксис. Можно сделать:

                              auto f2 = std::bind(f1, _2, _1, 5)

                              Это переставит первый и второй аргумент местами, а третий специализирует значением 5
                              после этого вызов f2(1, 2) эквивалентен f1(2, 1, 5)
                                0
                                вы не до конца поняли статью,
                                повторюсь, функтор, получаемый boost::bind, имеет ограниченное кол-во аргументов.
                                То есть этот пример на boost::bind не сделаешь
                                auto f1 = carry(f);
                                auto f2 = f1(5, 5);
                                auto v15 = f2(5);
                                  0
                                  Такой вариант не подходит?

                                  auto f2 = bind(f, 5, 5, _1);
                                  auto v15 = bind(f2, 5);

                                  int res1= f2(1); //res1=11
                                  int res2 = v15(); //res2=15
                                    0
                                    спрошу тогда так: вы сможете с помощью bind создать такой функтор f1, что будет верно следующее:
                                    f1(5, 5)(5) == f1(5)(5, 5) == f1(5, 5, 5) == f1(5)(5)(5)
                                      0
                                      это вопрос :)
                                        +1
                                        Я понимаю чем код написанные автором отличается, от реализации std::bind.
                                        Дело в том что задача поставленная автором решена в std::bind. И решение из std::bind более точно соответствует определеню каррирования и частичного применения чем у автора.
                                        Автор попытался сделать так что бы это внешне выглядело как функциональных языках программирования, но в результате у него получилось не каррирование и частичное применение а нечто другое.

                                        Ответ на ваш вопрос:
                                        Точно такой же функтор при помощи std::bind создать нельзя, но ваше равенство можно переписать в виде:

                                        bind(bind(f1, 5, 5, _1), 5) == bind(bind(f1, 5, _1, _2), 5, 5) == bind(f1, 5, 5, 5) ==
                                        bind(bind(bind(f1, 5, _1, _2), 5, _1), 5)

                                        PS: Знак == в выражении выше это не оператор сравнение С++, а знак тождественного равенства функций.
                                  +4
                                  Я не понимаю, что вам всем не так?
                                  1) человек сделал новую и интересную вещь
                                  2) продемонстрировал новые возможности c++11
                                  Довольно полезная статья для тех, кто хочет глубже изучить c++.
                                    0
                                    Все вроде согласны на счет (1) и (2), просто есть сомнения на счет практической применимости.
                                      +1
                                      Так весь буст в таком стиле написан, или про него тоже сомнения?
                                        +1
                                        Буст очень разный и неоднородный, вы про какую часть сейчас говорите?
                                    0
                                    Автор, уточните пожалуйста, что Вы понимаете под каррированием и частичным применением функции. Чем оправданы фразы «Каррирование является частным случаем частичного применения» и «или достаточно иметь только каррирование (как, например, в Haskell)»?
                                      0
                                      Конечно, пожалуйста.
                                      Каррирование — это представление функции от нескольких аргументов в виде функции, что принимает первый аргумент и возвращает функцию, которая, в свою очередь, принимает второй аргумент и так далее до результата.
                                      Частичное применение — это передача функции части аргументов, получая при этом функцию, что принимает остальные аргументы.
                                      По-этому, если рассматривать данные возможности с точки зрения их одноразового применения, то есть, если имеются две функции, первая из которых каррированна, а с другой можно проводить частичное применение, то в случае с каррированной функцией можно зафиксировать только первый аргумент, а в случае с частично применяемой — любой или любые. То есть в этом смысле каррирование есть частный случай частичного применения.
                                      Что касается «или достаточно иметь только каррирование (как, например, в Haskell)» — это мысль не моя и на нее я уже ответил в топике.
                                        0
                                        Благодарю. С определениями спорить не буду. Но Вы несколько неверно их трактуете. Каррирование — это операция приведения некаррированной функции к каррированному виду. Само по себе каррирование не может считаться частным случаем частичного применения функции, это всего лишь математическое преобразование функции. Для случая каррированных функций (не каррирования, а каррированных функций) задача частичного применения решаема (это доказано и использовано в Haskell). В общем случае эта задача НЕ решаема, т.е. встроенной реализации частичного применения нет ни в одном языке, то, что есть — означивание параметров и конструирование функций, вызывающих исходную с заданным значением определенного параметра — называется замыканием. Поэтому неверно говорить, что каррирование есть частный случай частичного применения.
                                          0
                                          Но я же указал, в каком смысле сравнивал эти два понятия.
                                          Нащет уровня встроености чего-то в язык, каюсь, что настолько строго данный вопрос не рассматривал, но нету ведь особой разницы: компилятор этим занимаеться или библиотечная функция. Я наведу пример з Nemerle:
                                            0
                                            извините, нечаянно нажал…

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

                                            з.ы. Вы могли бы мне кинуть информацию о каррировании и частичном применении в Хаскеле? Буду очень благодарен.
                                              0
                                              Разница тут принципиальная. Это смешение понятий. Замыкание — это просто «таскание контекста с собой», т.е. не строится фактически новая функция вместо старой, строится функция, которая вызывает старую с фиксированным значением некоторых параметров. Частичное применение — это построение новой функции, производной от старой, но не вызывающей старую, так называемой остаточной процедуры. Если правильно помню, для каррированных функций проводится редукция по переданным параметрам. Не берусь утверждать, не знаком с Nemerle, но скорее всего там происходит именно замыкание, просто сделана удобная синтаксическая конструкция «а-ля частичное применение».

                                              Относительно литературы, этот вопрос мельком затронут у Душкина в «Функциональное программирование на языке Haskell», и в этой книге в списке литературы есть ссылки на иноязычную литературу в том числе по данной тематике (частичное применение). В целом это вопрос из области лямбда-исчисления и сложно порекомендовать что-то конкретно по данному вопросу.

                                              Каррирование в Haskell делается очень просто. Для функции двух аргументов каррирование определяется так:

                                              carry :: ( (a, b) -> c ) -> (a -> b -> c)
                                              carry f = \ x y -> f (x, y)
                                              

                                              Главное, не считайте что раз происходит вызов исходной некаррированной функции, то это тоже замыкание. В данном случае приводится описание функции, а не способ ее вычисления. Способ выводит компилятор.
                                                0
                                                С вами полностью согласен, но переименовывать статью в «Эмуляция каррирования и частичного применения» не буду=)
                                                Сейчас очень часто приходиться слышать, как некоторые вещи называют не теми именами, но что поделаешь, эти названия модные, и они привлекают к себе внимание. Спекуляция поделила их значения на две группы: точные (те, например, что привели вы) и популярные (те, которыми оперировал я).
                                                Мне самому это не нравиться, но мы как-то не вольно в это втягиваемся — что же, будем бороться.
                                                з.ы. за объяснения огромное спасибо, никогда над этим не задумывался.
                                                  0
                                                  Не надо менять название статьи :). Но если Вы используете такую трактовку определений, уберите из текста ссылки на Haskell или хотя бы сделайте акцент на то, что это относится только к синтаксису, но не к семантике языка, потому что применительно к Haskell такая трактовка ошибочна. Иначе у читателя создается впечатление, что Haskell в этом отношении ущербен и не чета Nemerle.

                                                  P.S.: Статья хорошая и решение я считаю весьма элегантным, с учетом вполне точного замечания относительно интуитивности использования, сформулированного товарищем Wyrd в комментарии.
                                                    0
                                                    Извините, но и ссылки на Haskell я убрать не могу, поскольку то, что о нем упоминается, относиться к «спору», о котором я говорил в начале статьи. То есть это, по сути, цитата.
                                                      0
                                                      Это решается упоминанием о некорректности подобного заявления участником спора. Или примечанием в конце статьи.
                                                      Я, конечно, ни в коем случае не настаиваю. Это на правах рекомендации. Все-таки не очень приятно видеть ошибки в хороших статьях.
                                      +1
                                      Все это достаточно интересно, но мне кажется Вы немного не с той стороны подошли к написанию кода. Попробуйте себе задать вопрос не «что оно должно делать», а «как я его буду использовать». Тогда, скорее всего, получился бы boost::bind.

                                      Почему?

                                      1. Очевидный синтаксис:

                                      boost::bind( f, _2, _1 );
                                      выглядит понятнее чем
                                      permute< 10 >( f );

                                      2. Возможность частичного применения сразу нескольких аргументов, да еще и с перестановкой аргументов:
                                      boost::bind( f, _2, "Arg1", _1, "Arg2" );

                                      против
                                      permute< 10 >( PartApply< 2 >( PartApply< 1 >( f, "Arg1" )"Arg2" ) );

                                      Чего стоит только посчитать в уме индексы аргументов.
                                      Кстати, ни в PartApply, ни в permute не очевидно, что индексы нумеруются с нуля.

                                      3. boost::bind умеет работать с функциями-членами:

                                      boost::bind( &CMyClass::fthis );
                                      std::shared_ptr< CMyClass > sp = ...;
                                      boost::bind( &CMyClass::f, sp );

                                      И их тоже можно биндить:
                                      boost::bind( &CMyClass::f, _ );

                                      4. Если подумать, то переменное количество аргументов нафик не надо. Почему? Потому что вы хотите чтобы было ясно видно, когда вы занимаетсь биндингом, а когда вычислением:

                                      // Тут без контекста совершенно непонятно, где произойдет вызов f.
                                      int f( intintintint );
                                      // много кода
                                      auto x = carry( f )( 12 );
                                      // много кода
                                      auto y = x( 3 );
                                      // много кода
                                      auto z = y( 2 );

                                      Кроме того, у Вас, совершенно невозможно создать функцию без аргументов:
                                      int f( intintintint );
                                      auto g = boost::bind( f, 1234 ); // здесь мы f еще не вызывали
                                      g(); // а вот тут уже вызвали.


                                      Теперь камень в огород boost::bind. Полезная штука, да… был.
                                      С приходом С++0х он как-то сам по себе вытесняется лямбдами:

                                      1. Его невозможно отлаживать (точнее можно, но неудобно, по Step-In мы попадаем в дебри)

                                      2. Лямбда выражение хотя и длиннее немного, но нагляднее, когда код приходится читать, сравните:

                                      void DrawCircle( double x, double y, double R );
                                       
                                      // И к чему мы тут прибиндили 1.??
                                      // Надо смотреть определение DrawCircle, иначе не понятно.
                                      auto f = boost::bind( &DrawCircle, 1., _, _ );
                                       
                                      // Тут все очевидно
                                      auto f = 
                                          []( double y, double R )
                                          {
                                              return DrawCircle( 1., y, R );
                                          };


                                      Единственный минус лямбд — нельзя сделать типы, которые передаются в лямбду шаблонными или «авто», может оно и к лучшему, но иногда очень лень писать тип какого-нибудь контейнера, когда и так (по названию переменной) понятно, что это контейнер.

                                      p.s.

                                      Ах да, boost::bind еще позволяет захватывать по ссылке (см. boost::ref).
                                        0
                                        Собственно, идея предыдущей «простыни»:

                                        Когда пишите что-то шаблонное думайте в первую очередь о том, как им будут пользоваться. Надо искать такой «интерфейс взаимодействия», чтоб потом количество граблей из-за случайного/неосознанного использования сводилось к минимуму. Шаблонные параметры не должны трактоваться двусмысленно даже без взгляда на реализвацию
                                          0
                                          И, да, за статью устный плюсик, т.к. «период голосования истек»
                                            0
                                            Спасибо, но по-моему вы невнимательно читали. Из того, что вы назвали действительно только:
                                            1. нумерация с нуля — легко исправить;
                                            2. ненаглядный синтаксис — имеет свое преимущество: пользуясь, вы как бы говорите:
                                            auto f1 = partApply<3, 0>(f)(x, y); // передаю третий аргумент, а потом нулевой

                                            3. и работа с членами — я нарочно специализировал шаблоны только для std::function — так было проще. Это возможно, действительно, минус, но он тоже легко устраняется дополнительной [тривиальной] специализацией.
                                              0
                                              1.

                                              Тут Вы меня не так поняли, не важно откуда начинать нумерацию (с 0 или с 1), важно что это не очевидно по названию функции (permute/partApply), т.е. при их использовании может понадобиться искать определение. Впрочем, с этим можно мирится.

                                              2.

                                              Это холивар. На вкус и цвет все фломастеры разные (красный — самый вкусный).
                                              Насколько я понимаю, можно сделать врапперы как для boost::bind, так и для Вашего кода, которые эти самые фломастеры будут менять.

                                              3.

                                              Это в принципе можно дописать.

                                              Сюда же стоит добавить что-то вроде boost::ref:
                                              auto std::function< bool( intint ) > f = []( int x, int y ){ return x > y; };
                                              int y = 3;
                                              auto f1 = partApply< 1 >( f )( ref( y ) ); // y сохранить по ссылке
                                              assert( f1( 4 )() == true );
                                              = 4;
                                              assert( f1( 4 )() == false );


                                              Кроме того, рекомендую задуматься над следующим кодом:
                                              #include <iostream>
                                              #include <boost/bind.hpp>
                                               
                                              using namespace boost;
                                               
                                              int f(){ std::cout << "f" << std::endl; return 3; };
                                               
                                              int b( int x, int y ){ return x > y; };
                                               
                                              int _tmain( int, TCHAR*[] )
                                              {
                                                  // Подставить в качестве второго аргумента отложенный вызов f
                                                  auto F = bind( b, _1, bind( f ) );
                                               
                                                  std::cout << F( 2 ) << std::endl;
                                                  std::cout << F( 4 ) << std::endl;
                                               
                                                  return 0;
                                              }


                                              Результат выполнения:
                                              f
                                              0
                                              f
                                              1

                                              f тут вызывается два раза, опять же за счет специализации (если в качестве аргумента передают bind выражение, то оно его каждый раз вычисляет перед передачей в функцию)

                                              >> Из того, что вы назвали действительно только
                                              Пересмотрел Ваш код, согласен, однако основная «претензия» все-таки осталась.
                                              Я ее сформировал в более простом виде:
                                              «Когда я вижу код auto g = f( 5 );, я думаю что это вызов функции/функтора f, но в вашем случае — это подстановка аргумента, а вызов выглядел бы (при условии, что f каррированный вариант функции одного аргумента) так: auto g = f( 5 )();. Лично у меня это вызывает „разрыв шаблона“ в голове.»
                                                0
                                                Я вообще-то не собирался соревноваться с «boost::bind», но про отложенный вызов мне понравилось. Спасибо.
                                                0
                                                auto f1 = partApply<30>(f)(x, y); // передаю третий аргумент, а потом нулевой

                                                Это, кстати, не то же самое, что и:

                                                boost::bind( f, _2, y, _1, x );

                                                То же самое выглядело бы так:

                                                auto f1 = permute< 10 >( partApply<30>(f)(x, y) );
                                                  0
                                                  Перестаньте искать сложные пути! Почему бы просто не скачать код и не попробовать? А то о чем вы пишете, делается так:
                                                  auto f1 = partApp<3, 1, 2, 0>(f)(a1, a2);

                                                  з.ы. вы точно не внимательно читали — об этом было написано.
                                                    0
                                                    у меня студия, там нету variadic templates еще :(
                                              • НЛО прилетело и опубликовало эту надпись здесь

                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                          Самое читаемое