Всем известно, что функциональное программирование распространяется с быстротой огня по современным языкам программирования. Недавние примеры — Java 8 и C++, оба из которых теперь поддерживают лямбда-функции.
Итак, начнём (и да прибудет с нами веселье). Этот текст также доступен в виде слайдов на Slideshare. На написание этой статьи автор был вдохновлён создателем JSON Дугласом Крокфордом.
Функция Identity, которая принимает аргумент и возвращает тот же самый аргумент:
auto Identity = [](auto x) {
return x;
};
Identity(3); // 3
Примечание переводчика: Новой по сравнению с C++11 является возможность не указывать названия типов.
Функции add, sub и mul, которые принимают по два аргумента и возвращают их сумму, разность и произведение, соответственно:
auto add = [](auto x, auto y) {
return x + y;
};
auto sub = [](auto x, auto y) {
return x - y;
};
auto mul = [](auto x, auto y) {
return x * y;
};
Функция identityf, которая принимает аргумент и возвращает экземпляр внутреннего класса, при вызове которого будет возвращён исходный аргумент:
auto identityf = [](auto x) {
class Inner {
int x;
public: Inner(int i): x(i) {}
int operator() () { return x; }
};
return Inner(x);
};
identityf(5)(); // 5
Другая реализация identityf, возвращающая не объект, а функцию (да, теперь можно возвращать функции):
auto identityf = [](auto x) {
return [=]() { return x; };
};
identityf(5)(); // 5
Замечание: лямбда-функции ≠ замыкания:
- Лямбда — это просто анонимная функция.
- Замыкание — это функция, которая использует объекты из окружения, в котором она была объявлена. Во второй строчке предыдущего примера знак равенства обозначает «захват контекста».
- Не все лямбды являются замыканиями, и не все замыкания являются лямбдами.
- Замыкания в C++ являются обычными объектами, которые можно вызывать.
- Замыкания не продлевают жизнь объектам, которые они используют (для этого надо использовать shared_ptr).
Функция, которая возвращает функцию-генератор, возвращающую числа из заданного интервала:
auto fromto = [](auto start, auto finish) {
return [=]() mutable {
if(start < finish)
return start++;
else
throw std::runtime_error(“Complete");
};
};
auto range = fromto(0, 10);
range(); // 0
range(); // 1
Функция, принимающая числа по одному и складывающая их:
auto addf = [](auto x) {
return [=](auto y) {
return x+y;
};
};
addf(5)(4); // 9
Функция, меняющая местами аргументы другой функции:
auto swap =[](auto binary) {
return [=](auto x, auto y) {
return binary(y, x);
};
};
swap(sub)(3, 2); // -1
Функция twice, которая принимает бинарную функцию и возвращает унарную функцию, которая передаёт аргумент в бинарную два раза:
auto twice =[](auto binary) {
return [=](auto x) {
return binary(x, x);
};
};
twice(add)(11); // 22
Функция, которая принимает бинарную функцию и возвращает функцию, принимающую два аргумента по очереди:
uto applyf = [](auto binary) {
return [=](auto x) {
return [=](auto y) {
return binary(x, y);
};
};
};
applyf(mul)(3)(4); // 12
Функция каррирования, которая принимает бинарную функцию и аргумент и возвращает функцию, принимающую второй аргумент:
auto curry = [](auto binary, auto x) {
return [=](auto y) {
return binary(x, y);
};
};
curry(mul, 3)(4); // 12
Замечание: Каррирование (currying, schönfinkeling) — преобразование функции, получающей несколько аргументов, в цепочку функций, принимающих по одному аргументу.
- В λ-анализе все функции принимают только по одному аргументу.
- Вам нужно понять, как работает каррирование, чтобы выучить Haskell.
- Каррирование ≠ частичное применение функции.
Частичное применение функции:
auto addFour = [](auto a, auto b,
auto c, auto d) {
return a+b+c+d;
};
auto partial = [](auto func, auto a, auto b) {
return [=](auto c, auto d) {
return func(a, b, c, d);
};
};
partial(addFour,1,2)(3,4); //10
Три варианта, как без создания новой функции получить функцию, прибавляющую к аргументу единицу:
auto inc = curry(add, 1);
auto inc = addf(1);
auto inc = applyf(add)(1);
Реализация композиции функций:
auto composeu =[](auto f1, auto f2) {
return [=](auto x) {
return f2(f1(x));
};
};
composeu(inc1, curry(mul, 5))(3) // (3 + 1) * 5 = 20
Функция, принимающая бинарную функцию и модифицирующая её так, чтобы её можно было вызвать только один раз:
auto once = [](auto binary) {
bool done = false;
return [=](auto x, auto y) mutable {
if(!done) {
done = true;
return binary(x, y);
}
else
throw std::runtime_error("once!");
};
};
once(add)(3,4); // 7
Функция, которая принимает бинарную функцию и возвращает функцию, принимающую два аргумента и callback:
auto binaryc = [](auto binary) {
return [=](auto x, auto y, auto callbk) {
return callbk(binary(x,y));
};
};
binaryc(mul)(5, 6, inc) // 31
binaryc(mul)(5, 6, [](int a) { return a+1; }); // то же самое
Наконец, напишем следующие три функции:
- unit — то же, что identityf;
- stringify — превращает свой аргумент в строку и применяет к нему unit;
- bind — берёт результат unit и возвращает функцию, которая принимает callback и возвращает результат его применения к результату unit.
auto unit = [](auto x) {
return [=]() { return x; };
};
auto stringify = [](auto x) {
std::stringstream ss;
ss << x;
return unit(ss.str());
};
auto bind = [](auto u) {
return [=](auto callback) {
return callback(u());
};
};
Теперь убедимся, что всё работает:
std::cout << "Left Identity "
<< stringify(15)()
<< "=="
<< bind(unit(15))(stringify)()
<< std::endl;
std::cout << "Right Identity "
<< stringify(5)()
<< "=="
<< bind(stringify(5))(unit)()
<< std::endl;
Что же такого интересного в функциях unit и bind? Дело в том, что это — монады.
Читать второй пост из серии в блоге автора.
Автор: mitya57