Pull to refresh

Comments 62

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

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

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

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

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

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

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

Данный же подход позволяет использовать лишь функцию. А при передачи ей нужных аргументов происходит то самое конфигурирование callback'а.
Позвольте, а чем обычные функторы не подходят в случае callback'ов?
Если нужно передавать только 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); // конфигурируем
boost::bind(&MyClass::method, this, x, y) чем плохо?
Не знал, почитал, спасибо: это, действительно, то же. Тогда пост можно рассматривать как объяснение реализации или, возможно, другой подход.
Это не то, у boost::bind нет произвольного кол-ва аргументов.
то есть функтор, получаемый с помощью boost::bind, имеет точное кол-во аргументов.
И что? Функция получаемая в результате каррирования или частичного применения по определению имеет определённое количество аргументов.
Если нужно, то можно к ней ещё раз применить каррирование или частичного применение.
это соответствующий проект для CodeBlocks
Пример интересен как пруф-оф-консепт или в блог ненормальное программирование :-)
Применение каррирование и прочих функциональных штук в строго-типизированном языке — это извращение, на мой взгляд.
типизация Haskell'а намного «строже» C++ной
Вы переизобрели std::bind. Он есть в стандартной библиотеке c++11.
У него более универсальный синтаксис. Можно сделать:

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

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

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

int res1= f2(1); //res1=11
int res2 = v15(); //res2=15
спрошу тогда так: вы сможете с помощью bind создать такой функтор f1, что будет верно следующее:
f1(5, 5)(5) == f1(5)(5, 5) == f1(5, 5, 5) == f1(5)(5)(5)
Я понимаю чем код написанные автором отличается, от реализации 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: Знак == в выражении выше это не оператор сравнение С++, а знак тождественного равенства функций.
Я не понимаю, что вам всем не так?
1) человек сделал новую и интересную вещь
2) продемонстрировал новые возможности c++11
Довольно полезная статья для тех, кто хочет глубже изучить c++.
Все вроде согласны на счет (1) и (2), просто есть сомнения на счет практической применимости.
Так весь буст в таком стиле написан, или про него тоже сомнения?
Буст очень разный и неоднородный, вы про какую часть сейчас говорите?
Автор, уточните пожалуйста, что Вы понимаете под каррированием и частичным применением функции. Чем оправданы фразы «Каррирование является частным случаем частичного применения» и «или достаточно иметь только каррирование (как, например, в Haskell)»?
Конечно, пожалуйста.
Каррирование — это представление функции от нескольких аргументов в виде функции, что принимает первый аргумент и возвращает функцию, которая, в свою очередь, принимает второй аргумент и так далее до результата.
Частичное применение — это передача функции части аргументов, получая при этом функцию, что принимает остальные аргументы.
По-этому, если рассматривать данные возможности с точки зрения их одноразового применения, то есть, если имеются две функции, первая из которых каррированна, а с другой можно проводить частичное применение, то в случае с каррированной функцией можно зафиксировать только первый аргумент, а в случае с частично применяемой — любой или любые. То есть в этом смысле каррирование есть частный случай частичного применения.
Что касается «или достаточно иметь только каррирование (как, например, в Haskell)» — это мысль не моя и на нее я уже ответил в топике.
Благодарю. С определениями спорить не буду. Но Вы несколько неверно их трактуете. Каррирование — это операция приведения некаррированной функции к каррированному виду. Само по себе каррирование не может считаться частным случаем частичного применения функции, это всего лишь математическое преобразование функции. Для случая каррированных функций (не каррирования, а каррированных функций) задача частичного применения решаема (это доказано и использовано в Haskell). В общем случае эта задача НЕ решаема, т.е. встроенной реализации частичного применения нет ни в одном языке, то, что есть — означивание параметров и конструирование функций, вызывающих исходную с заданным значением определенного параметра — называется замыканием. Поэтому неверно говорить, что каррирование есть частный случай частичного применения.
Но я же указал, в каком смысле сравнивал эти два понятия.
Нащет уровня встроености чего-то в язык, каюсь, что настолько строго данный вопрос не рассматривал, но нету ведь особой разницы: компилятор этим занимаеться или библиотечная функция. Я наведу пример з Nemerle:
извините, нечаянно нажал…

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

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

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

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

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

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

P.S.: Статья хорошая и решение я считаю весьма элегантным, с учетом вполне точного замечания относительно интуитивности использования, сформулированного товарищем Wyrd в комментарии.
Извините, но и ссылки на Haskell я убрать не могу, поскольку то, что о нем упоминается, относиться к «спору», о котором я говорил в начале статьи. То есть это, по сути, цитата.
Это решается упоминанием о некорректности подобного заявления участником спора. Или примечанием в конце статьи.
Я, конечно, ни в коем случае не настаиваю. Это на правах рекомендации. Все-таки не очень приятно видеть ошибки в хороших статьях.
Все это достаточно интересно, но мне кажется Вы немного не с той стороны подошли к написанию кода. Попробуйте себе задать вопрос не «что оно должно делать», а «как я его буду использовать». Тогда, скорее всего, получился бы 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).
Собственно, идея предыдущей «простыни»:

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

3. и работа с членами — я нарочно специализировал шаблоны только для std::function — так было проще. Это возможно, действительно, минус, но он тоже легко устраняется дополнительной [тривиальной] специализацией.
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 )();. Лично у меня это вызывает „разрыв шаблона“ в голове.»
Я вообще-то не собирался соревноваться с «boost::bind», но про отложенный вызов мне понравилось. Спасибо.
auto f1 = partApply<30>(f)(x, y); // передаю третий аргумент, а потом нулевой

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

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

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

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

з.ы. вы точно не внимательно читали — об этом было написано.
у меня студия, там нету variadic templates еще :(
UFO just landed and posted this here
Sign up to leave a comment.

Articles