Pull to refresh

Comments 45

Вместо реализации для двух-трёх параметров лучше было бы написать реализацию на шаблонах с переменным числом параметров. Или студия в этом плане слоупочит?
Там вообще все плохо. Про Variadic Templates в Visual Studio можно даже не мечтать…
Тогда не избавиться от указания шаблонных типов при вызове curry. Мы от них избавились благодаря typedef'ам, которые при помощи того же Variadic Templates генерировать не получится из-за их именования (first_argument_type, second_argument_type, etc).
Видимо я чего-то не понимаю (в полтретьего ночи не исключено), но меня интересует только один вопрос: зачем?
Зачем нужны все эти хитрости, которые уменьшают читабельность кода для человека не в теме, но вроде как не приносят особой пользы? Возможно я не встречал ситуаций, где это сильно упрощает жизнь, если это так, приведите такой пример пожалуйста.
С некоей общефилософской точки зрения традиционно в языках вроде C и Pascal функции не были полноценными элементами языка (т.е. first-class citizens). Вы можете написать 5 + 10, но почему-то не можете аналогичным образом «сложить» функции, получая новую функцию.

Представьте себе, каким неудобным было бы программирование, если бы приходилось писать что-то вроде этого: А = 10; А *= B; A += 5; вместо A = 5+10*B;

Мы, воспитанные на структурных языках, и не замечаем подобных ограничений, когда дело касается функций. Мы к ним попросту привыкли, хотя по сути ничего хорошего в них нет. Свобода лучше, чем несвобода, как мы помним :)

Конечно, когда в традиционные языки добавляют лямбды и тому подобное, первые попытки выглядят уродливо. К ним не привыкли мы, к ним не приспособлены сами языки. Но ситуация потихоньку меняется к лучшему. Тот же Boost.Lambda — явный шаг вперёд. Разумеется, это не значит, что его надо использовать на каждом углу.
Ну, буст лямбда вообще противопоказано использовать для продакшен кода, если целью конечно не стоит показать коллегам свою «крутость», а потом заставить их ночью с тонной матов отлаживать такой код или рефакторить его при добавлению какого-нибудь нового параметра в функцию.

Конечно, это все круто выглядит, но какие преимущества это дает? Вот вы приводите абстрактный пример с А = 5 + 10 * В, ну он настолько абстрактен, что не показывает ровным счетом ничего. Было бы гораздо лучше если бы вы привели в пример реализацию чего-либо стоящего с помощью каррирования, где были бы видны его преимущества по сравнению с обычными подходами. Я лично ничего не вижу ни одного преимущества в каррировании для С++, только существенный недостаток — усложнение кода.
Честно говоря я вообще не понял, в чем смысл превращать это:
cout << foo("Кока-кола", 2, 9.95) << endl;  // => Кока-кола: 2 л. = 9.95$

в это:
 cout << mega::curry(foo)("Кока-кола")(2)(9.95) << endl;  // => Кока-кола: 2 л. = 9.95$


Ну, а биндинг параметров с помощью лямбд делается вообще очень просто в С++0х:
string foo(string s, int i, double d);

auto fooCola = [](int i, double d) { return foo("Coca-cola", i, d); };
auto fooCola2Litra = [&](double d) { return fooCola(2, d); };
fooCola2Litra(9.95);

Выглядит на мой взгляд намного проще и не требует какого-либо дополнительно кода для реализации.
По поводу буст.лябмда. Если не даёте ей права на продакшн, воспринимайте её просто как некий тестовый движок для новых идей. Вот как раз по поводу «очень просто в C++0x»: так если бы не было Boost.Lambda, не было бы и этих идей для нового стандарта. В конце концов, стандарт двигают те же люди, что и библиотеки буста.

Смысла каррировать в вашем примере, конечно, нет никакого. По поводу практических примеров — ну вот у меня был такой кусок в коде (написано давно):

for_each(begin(), end(), bind2nd(mem_fun(Unit::setSelected), false)); // deselect everything
(У меня есть класс Unit, в котором имеется функция-член void setSelected(bool s))

Вообще я должен сказать, что одно здесь влечёт за собой другое. Если в архитектуре программы много классов с функциями от двух и более аргументов, возникает соблазн применить их в for_each и подобных алгоритмах, немного «обработав» их bind'ом. Если таких функций нет, то и соблазна нет, естественно. А добавлять в интерфейс новую функцию ради одного-единственного вызова алгоритма как-то глупо.

Ну или вот:

std::vector<SDL_Event>::iterator it =
find_if(EventQueue.begin(), EventQueue.end(), bind(&SDL_Event::type, _1) == EventType);

В классе SDL_Event есть член type. Мне нужно извлечь из очереди все евенты, у которых type = EventType.

Короче говоря, если есть готовые «кирпичи», из них хочется что-то построить. Если их нет, разумеется, их и создавать незачем.
* да, во втором примере я извлекаю только первый евент, конечно же.
Функция map — это аналог std::for_each, т.е. она принимает какую-то функцию и вызывает её для каждого элемента списка.

Ну не со всем… map, помимо всего, возвращает результат применения. Я думаю в этом плане std::transform несколько ближе к map, нежели std::for_each. :)
Простите меня за глупый вопрос и пример без темплейтов, но перегрузка методов или функций разве не даст тотже результат сделав код даже темплейта более понятным и менее витиеватым ???

#include <stdio.h>

int f(int a){
	return a+42;
}

int f(int a, int b){
	return a+b;
}

int main(){
	printf("%d\n",f(10));
	printf("%d\n",f(10,11));
	return 0;
}
UFO just landed and posted this here
Согласен, нет предела совершенству, но как мне кажется возвращение функции это излишний вызов функции а значит минус производительность ибо f(a) на ассемблере это как минимум два push и и один call которые добавятся к операциям в f(a,b).
А если я захочу потом частично применить не только 42, но и 13? Как перегрузить метод?
Если вдруг я не ясно выразился: мне нужно две версии функции — одна с зафиксированным значением 42, а другая с зафиксированным значением 13.
Если у вас есть функция f(a, b), то чтобы получить функцию g(a, 42), вам придётся создавать новый объект (функцию g). А вот если у вас есть числа 5 и 10, чтобы получить 15, нет необходимости создавать объект. Вы просто пишете 5 + 10.

Вся эта история из топика как раз о том, как сделать функции полноценными членами арифметических выражений, чтобы не плодить их безмерно.
А разве это не «плодить объекты»

auto curried_sum = curry_<int,int,int>(sum);

Даже с точки зрения создания «полноценных членов арифметических выражений», нужно указать тип данных и константное значение 42. А значит, так или иначе плодить, плодить и опять плодить. Может быть оно и сократит текст объявления но все равно задача будет ограничена a+b, а значит для задач другого рода, придется плодить шаблоны.

Хотя отмечу, что с точки зрения зарядки для ума ваш пример неплох.
ну это, видимо, не очень удачная конструкция, в идеале вы как раз должны создавать лямбда-выражения, т.е. арифметические конструкции из функций, без явного объявления новых объектов.
Идея была в том, чтобы найти золотую середину между читабельностью кода (избежать использования уродливых bind'ов) и внесением дополнительных модификаций (не писать каждый раз ещё одну перегруженную версию). Считаю, что этого удалось достичь, пусть и в академических целях, а не для использования в реальном коде.
UFO just landed and posted this here
Не совсем. std::bind — средство для создания «частичной специализации».
В то время как пример автора является «каррированием». Многие считают, что это одно и тоже, но это не так. :)
Верно. Каррирование — лишь один из способов реализовать механизм частичного применения.
В Visual Studio 2010, который используется автором статьи, std::bind есть.
Ох не завидую тому, кому придется разбираться в таком коде. Шаблонная магия во всей красе.
Но еще больше не завидую тем кому придется учить C++ с нуля, лет так через 30.
ну, тут магия довольно изолирована.

в реализации функций из std:: же практически никто не лазит и не разбирается? так и здесь. достаточно задокументировать результат выполнения функции curry.
Не переживайте, в стандартную библиотеку это не попадёт ;)
Будут проблемы с копированием строк и вообще типов поддерживающих операцию сложения.
Я не совсем понял, о чём Вы, поэтому отвечу: возможно. Можно пример кода?
curry_<int,int,int>(sum) замените на std::string, придётся писать ряд реализаций для const std::string&, а также специализацию для const char*. Строки тоже можно складывать, так что сумма должна уметь считаться и от них.
Ещё есть сумма N-мерных векторов, матриц MxN, сумма комплексных чисел. Каждый из этих объектов нуждается в передаче по ссылке, а не по значению. Сохранять куда бы то ни было по значению, значит копировать = потеря по времени для функции, которая претендует на всеобщее употребление, т.е. критична к производительности.
Я написал это как proof of concept. Естественно, что при частичном применении первого параметра, он скопируется. Тем не менее, это работает.

int main()
{
    auto concat = plus<string>();
    cout << mega::curry(concat)("One string")(" Another string") << endl;

    return EXIT_SUCCESS;
}

А теперь представим, что мы набираем с помощью Вашей функции HTML-страничку, состоящую из длинного списка (результат простого SELECT например). Например первый аргумент фиксированный, и он и будет результатом. Нужно пройти по результату запроса и набрать результат. Насколько эффективно будет использовать std::string вместо const std::string? Учитывая, что мы подразумеваем список элементов или просто параграфов , то было бы неплохо ещё складывать с const char*. Опять же тут мы снова получаем промежуточное преобразование в std::string с копированием. C++ такой язык, где либо получаем максимум эффективности, либо у начальства рано или поздно возникают вопросы «А почему на скриптовом языке получается быстрее?»
Имеется в виду, что аргументы было бы выгоднее сделать const T&, от сохранения видимо никуда не денешься, однако было бы здорово сохранять ссылку на протяжении всей цепочки вызовов, а не копировать всё содержимое объекта. Использование такой конструкции рано или поздно приведёт к рекурсивному/цепочному использованию подобных функций и обобщению их в библиотеку общего пользования. Поэтому было бы здорово чуть-чуть доработать рабочий пример.
от сохранения денешься, нужно вводить механизм описания того, как передавать данные — по ссылке, значению и т.д. (тот же boost::ref, например).

кроме того, нужна поддержка move конструкторов
Про move-конструкторы я думал, и это, пожалуй, приоритет номер один на данный момент. Но описание способа передачи данных — это спорно очень, ибо сильно снижает читабельность. Хотя может можно как-то извратиться приемлемо. В любом случае, я не предлагаю использовать то, что я написал, в требовательных к ресурсам системах. Я вообще не предлагаю это использовать :)
По хорошему, у вас все еще не каррирование. Но уже почти.

Проблема: вы не выводите новую функцию каррированием, как это делают функциональные языки. Вы возвращаете некий композитный объект, который последовательно применяет нужный аргументы. Это обламывает оптимизацию на корню.

auto f = curry(max)(10);
f( 2 );
В вашем текущем варианте в момент f( 2 ) будет выполнен виртульный operator() внутри f, которому передадут 2, затем будет еще один виртуальный вызов, в который передадут (10, 2), и только потом будет вызвана max с нужными параметрами.
Если бы не виртуальные вызовы, это все бы счастье заинлайнилось и был бы выполнен обычный ассемблерный cmp.

Предложение: возвращайте некий комлекный тип вместо std::function, что угодно, вплоть до лямбд, но чтоб там не было рантайм полиморфизма. Кому он нужен, запихнет ваш тип внутрь std::function.

Кроме того, введение такого типа, позволит вам перегрузить у вашего типа operator() так, что его можно будет вызывать не только с одним аргументом:

int f( int,int,int );
auto unary_1 = curry(f)(1,2);
auto unary_2 = curry(f)(1)(2);

в общем, мне кажется, все это можно допилить вплоть до «уровня хаскеля»
Дельно. Я попытаюсь над этим подумать, спасибо.
А вообще по-хорошему и лямбды в С++ не лямбды.
Ну, это понятно, но все-таки это и не совсем функциональные объекты.

В функциональные объекты захваченные по ссылке параметры надо «по одному» передавать, а лямбды захватывают ссылку на стек фрейм «родительской» функции. По крайней мере в MSVC так.
Думаю, это особенность реализации. Вряд ли так написано в стандарте.
Блин, случайно ентер нажал, да еще и в не той раскладке :(
Совсем необязательно захватывать весь стек. Можно захватывать отдельные переменные — в VS 2010 это вполне работает
Захватывается не стек, а ссылка на него.

Даже если вы захватили по ссылке лямбдой 100500 параметров, все равно внутрь лямбда объекта передается только указатель на стек фрейм материнской функции. Студия автоматически будет добавлять к нему смещения для доступа к конкретной переменной так же, как она это делает внутри обычной функции (используя ebp в качестве указателя, или «что попало» если отключены стек фреймы).

Захвата по значению это все, естественно не касается.
А что произойдет в этом коде, после того, как мы выйдем из функции getFunction?
std::function<int()> getFunction()
{
    int n = 10;
    std::function<int()> result = [&n]()
    {
        return n;
    }
    return result;
}

Ссылка на стек становится невалидной. А что произойдет с лямбдой и захваченной переменной?
В студии лямбда вернула ноль. А вообще всё забываю по этому поводу почитать стандарт.
Sign up to leave a comment.

Articles