Сообщение Re: Отличительные свойства лямбды и замыкания от 08.02.2022 8:12
Изменено 10.02.2022 19:47 netch80
Re: Отличительные свойства лямбды и замыкания
Здравствуйте, vaa, Вы писали:
vaa>Не уверен в правильности выбора раздела, но все же.
vaa>Есть четкое определение отличительны свойств лямбды и замыкания?
vaa>Или то и другое просто анонимные функции? В чем их ключевые различия?
vaa>Что может одна и не может другая конструкция?
Нужно различать, как минимум:
1) лямбду как концепцию теории лямбда-исчисления.
2) лямбду как языковую конструкцию.
3) замыкание как технологию в программировании.
Может быть, ещё и замыкание в исходном смысле имени Haskell Curry, но я тут не в курсе.
Скорее всего, у тебя вопрос про соотношение (2) и (3). (1) мало смысла учитывать, потому что при его учёте можно любую функцию считать лямбдой, это неэффективно.
В среднем по наиболее ходовым языкам на данный момент: лямбда может ничего не замыкать, а замыкание может не быть оформлено как лямбда. Возьмём Питон:
(простите за изъезженность примера с make_adder, но он универсальный и таки понятный)
Первое — лямбда в смысле собственно языка, второе — именованная функция. Делают одинаковые вещи. В обоих случаях это функция первого класса, но не замыкание (или, математически, можно назвать замыканием на 0 параметров, тривиальный вырожденный случай — но для обычного разговора это неинтересно).
Порождаем замыкание на 1 параметр:
make_adder(3), например, создаст функцию с 1 аргументом, которая возвращает этот аргумент, увеличенный на 3.
Такие же две функции внутри, которые стали замыканиями из-за использования параметра — переменной из внешнего контекста, и две снаружи, которые порождают такие замыкания. (Из-за свойства Python, что замыкание тянет за собой весь внешний фрейм по ссылке, лучше генерацию замыкания выносить в отдельную функцию с минимальным количеством локальных переменных. Или я тут отстал и это уже не актуально?)
Теперь C++. Именованных вложенных функций там нет, всё делается через синтаксис, который называется Lambda expressions. Но есть большая разница, созданная им вложенная функция будет хранить какой-то контекст или нет (компилятор может найти причины соптимизировать это).
Например:
Тут просто выделение части кода в отдельный блок (иногда удобно чтобы не распространять локальный контекст). И снова это то тривиальное замыкание на 0 параметров, которое простой компилятор может создать полноценную функцию первого класса в виде std::function<сигнатура> (или аналога, дальше не уточняю). Более умный наверняка имеет средства отбросить подобное усложнение (для GCC, Clang это точно так).
Если foo() принимает коллбэк, это объект типа std::function<>, но он ничего не сохранил из контекста, внутри объекта только адрес для вызова.
С одной стороны, x запомнено по значению, с другой, компилятор может не создавать полноценный std::function, а иначе сделать — неважно как, лишь бы x передавался в f. Оно расскажет про 100% и 200%.
А тут без контекста (хранящего адрес f) не обойтись. В foo() уходит объект контекста с одним дополнительно запомненным параметром — &x (и, естественно, кого вызывать с этим параметром).
Точно так же, вызвать — расскажет про 100% и 200%.
Тут будет 0% в обоих случаях, запомнено будет значение x в момент создания замыкания, а не его адрес. Это уже особенности C++.
С третьей стороны, ничто не мешает сделать замыкание и не через лямбду языка. Например, libjit — популярный вариант до появления готового аналога C++11, или и сейчас в C без C++. Лямбды C++ переложили заботу о генерации кода для всех платформ на компилятор, а тут — авторы библиотеки.
Вдогонку: вспомнил, как мы ещё в 90-х творили подобные штуки на Фортране С соглашениями вызова, принятых в x86-{16,32}, параметры передаются на стеке. Генератор "замыкания" аллоцировал память под кусочек кода, который писал по байтам, типа такого:
У внутренней функции первым параметром было cookie, которое она дальше разбирала (обычно — указатель на структуру).
Современные аналоги делают почти то же самое 1:1 (только с регистровыми соглашениями вызова для x86-64 добавляется сильная громоздкость этому процессу).
vaa>Не уверен в правильности выбора раздела, но все же.
vaa>Есть четкое определение отличительны свойств лямбды и замыкания?
vaa>Или то и другое просто анонимные функции? В чем их ключевые различия?
vaa>Что может одна и не может другая конструкция?
Нужно различать, как минимум:
1) лямбду как концепцию теории лямбда-исчисления.
2) лямбду как языковую конструкцию.
3) замыкание как технологию в программировании.
Может быть, ещё и замыкание в исходном смысле имени Haskell Curry, но я тут не в курсе.
Скорее всего, у тебя вопрос про соотношение (2) и (3). (1) мало смысла учитывать, потому что при его учёте можно любую функцию считать лямбдой, это неэффективно.
В среднем по наиболее ходовым языкам на данный момент: лямбда может ничего не замыкать, а замыкание может не быть оформлено как лямбда. Возьмём Питон:
(простите за изъезженность примера с make_adder, но он универсальный и таки понятный)
func1 = lambda x: x+1
def func2(x):
return x+1
Первое — лямбда в смысле собственно языка, второе — именованная функция. Делают одинаковые вещи. В обоих случаях это функция первого класса, но не замыкание (или, математически, можно назвать замыканием на 0 параметров, тривиальный вырожденный случай — но для обычного разговора это неинтересно).
Порождаем замыкание на 1 параметр:
def make_adder_1(y):
return lambda x: x+y
def make_adder_2(y):
def adder(x):
return x+y
return adder
make_adder(3), например, создаст функцию с 1 аргументом, которая возвращает этот аргумент, увеличенный на 3.
Такие же две функции внутри, которые стали замыканиями из-за использования параметра — переменной из внешнего контекста, и две снаружи, которые порождают такие замыкания. (Из-за свойства Python, что замыкание тянет за собой весь внешний фрейм по ссылке, лучше генерацию замыкания выносить в отдельную функцию с минимальным количеством локальных переменных. Или я тут отстал и это уже не актуально?)
Теперь C++. Именованных вложенных функций там нет, всё делается через синтаксис, который называется Lambda expressions. Но есть большая разница, созданная им вложенная функция будет хранить какой-то контекст или нет (компилятор может найти причины соптимизировать это).
Например:
auto f = [] {
printf("Hello from lambda\n");
};
f();
Тут просто выделение части кода в отдельный блок (иногда удобно чтобы не распространять локальный контекст). И снова это то тривиальное замыкание на 0 параметров, которое простой компилятор может создать полноценную функцию первого класса в виде std::function<сигнатура> (или аналога, дальше не уточняю). Более умный наверняка имеет средства отбросить подобное усложнение (для GCC, Clang это точно так).
auto f = [] {
printf("Hello from lambda\n");
};
foo(f);
Если foo() принимает коллбэк, это объект типа std::function<>, но он ничего не сохранил из контекста, внутри объекта только адрес для вызова.
int x;
auto f = [&] {
printf("Hello %d%% from lambda\n", x);
};
x = 100;
f();
x = 200;
f();
С одной стороны, x запомнено по значению, с другой, компилятор может не создавать полноценный std::function, а иначе сделать — неважно как, лишь бы x передавался в f. Оно расскажет про 100% и 200%.
int x;
auto f = [&] {
printf("Hello %d%% from lambda\n", x);
};
x = 100;
foo(f);
x = 200;
foo(f);
А тут без контекста (хранящего адрес f) не обойтись. В foo() уходит объект контекста с одним дополнительно запомненным параметром — &x (и, естественно, кого вызывать с этим параметром).
Точно так же, вызвать — расскажет про 100% и 200%.
int x = 0;
auto f = [=] {
printf("Hello %d%% from lambda\n", x);
};
x = 100;
foo(f);
x = 200;
foo(f);
Тут будет 0% в обоих случаях, запомнено будет значение x в момент создания замыкания, а не его адрес. Это уже особенности C++.
С третьей стороны, ничто не мешает сделать замыкание и не через лямбду языка. Например, libjit — популярный вариант до появления готового аналога C++11, или и сейчас в C без C++. Лямбды C++ переложили заботу о генерации кода для всех платформ на компилятор, а тут — авторы библиотеки.
Вдогонку: вспомнил, как мы ещё в 90-х творили подобные штуки на Фортране С соглашениями вызова, принятых в x86-{16,32}, параметры передаются на стеке. Генератор "замыкания" аллоцировал память под кусочек кода, который писал по байтам, типа такого:
pop cx ; адрес возврата. не помню, какой регистр был свободен, пусть это cx
push <константа>
push cx
jmp <функция вокруг которой делалось замыкание>
У внутренней функции первым параметром было cookie, которое она дальше разбирала (обычно — указатель на структуру).
Современные аналоги делают почти то же самое 1:1 (только с регистровыми соглашениями вызова для x86-64 добавляется сильная громоздкость этому процессу).
Re: Отличительные свойства лямбды и замыкания
Здравствуйте, vaa, Вы писали:
vaa>Не уверен в правильности выбора раздела, но все же.
vaa>Есть четкое определение отличительны свойств лямбды и замыкания?
vaa>Или то и другое просто анонимные функции? В чем их ключевые различия?
vaa>Что может одна и не может другая конструкция?
Нужно различать, как минимум:
1) лямбду как концепцию теории лямбда-исчисления.
2) лямбду как языковую конструкцию.
3) замыкание как технологию в программировании.
Может быть, ещё и замыкание в исходном смысле имени Haskell Curry, но я тут не в курсе.
Скорее всего, у тебя вопрос про соотношение (2) и (3). (1) мало смысла учитывать, потому что при его учёте можно любую функцию считать лямбдой, это неэффективно.
В среднем по наиболее ходовым языкам на данный момент: лямбда может ничего не замыкать, а замыкание может не быть оформлено как лямбда. Возьмём Питон:
(простите за изъезженность примера с make_adder, но он универсальный и таки понятный)
Первое — лямбда в смысле собственно языка, второе — именованная функция. Делают одинаковые вещи. В обоих случаях это функция первого класса, но не замыкание (или, математически, можно назвать замыканием на 0 параметров, тривиальный вырожденный случай — но для обычного разговора это неинтересно).
Порождаем замыкание на 1 параметр:
make_adder(3), например, создаст функцию с 1 аргументом, которая возвращает этот аргумент, увеличенный на 3.
Такие же две функции внутри, которые стали замыканиями из-за использования параметра — переменной из внешнего контекста, и две снаружи, которые порождают такие замыкания. (Из-за свойства Python, что замыкание тянет за собой весь внешний фрейм по ссылке, лучше генерацию замыкания выносить в отдельную функцию с минимальным количеством локальных переменных. Или я тут отстал и это уже не актуально?)
Теперь C++. Именованных вложенных функций там нет, всё делается через синтаксис, который называется Lambda expressions. Но есть большая разница, созданная им вложенная функция будет хранить какой-то контекст или нет (компилятор может найти причины соптимизировать это).
Например:
Тут просто выделение части кода в отдельный блок (иногда удобно чтобы не распространять локальный контекст). И снова это то тривиальное замыкание на 0 параметров, которое простой компилятор может создать полноценную функцию первого класса в виде std::function<сигнатура> (или аналога, дальше не уточняю). Более умный наверняка имеет средства отбросить подобное усложнение (для GCC, Clang это точно так).
Если foo() принимает коллбэк, это объект типа std::function<>, но он ничего не сохранил из контекста, внутри объекта только адрес для вызова.
С одной стороны, x запомнено по значению, с другой, компилятор может не создавать полноценный std::function, а иначе сделать — неважно как, лишь бы x передавался в f. Оно расскажет про 100% и 200%.
А тут без контекста (хранящего адрес x) не обойтись. В foo() уходит объект контекста с одним дополнительно запомненным параметром — &x (и, естественно, кого вызывать с этим параметром).
Точно так же, вызвать — расскажет про 100% и 200%.
Тут будет 0% в обоих случаях, запомнено будет значение x в момент создания замыкания, а не его адрес. Это уже особенности C++.
С третьей стороны, ничто не мешает сделать замыкание и не через лямбду языка. Например, libjit — популярный вариант до появления готового аналога C++11, или и сейчас в C без C++. Лямбды C++ переложили заботу о генерации кода для всех платформ на компилятор, а тут — авторы библиотеки.
Вдогонку: вспомнил, как мы ещё в 90-х творили подобные штуки на Фортране С соглашениями вызова, принятых в x86-{16,32}, параметры передаются на стеке. Генератор "замыкания" аллоцировал память под кусочек кода, который писал по байтам, типа такого:
У внутренней функции первым параметром было cookie, которое она дальше разбирала (обычно — указатель на структуру).
Современные аналоги делают почти то же самое 1:1 (только с регистровыми соглашениями вызова для x86-64 добавляется сильная громоздкость этому процессу).
vaa>Не уверен в правильности выбора раздела, но все же.
vaa>Есть четкое определение отличительны свойств лямбды и замыкания?
vaa>Или то и другое просто анонимные функции? В чем их ключевые различия?
vaa>Что может одна и не может другая конструкция?
Нужно различать, как минимум:
1) лямбду как концепцию теории лямбда-исчисления.
2) лямбду как языковую конструкцию.
3) замыкание как технологию в программировании.
Может быть, ещё и замыкание в исходном смысле имени Haskell Curry, но я тут не в курсе.
Скорее всего, у тебя вопрос про соотношение (2) и (3). (1) мало смысла учитывать, потому что при его учёте можно любую функцию считать лямбдой, это неэффективно.
В среднем по наиболее ходовым языкам на данный момент: лямбда может ничего не замыкать, а замыкание может не быть оформлено как лямбда. Возьмём Питон:
(простите за изъезженность примера с make_adder, но он универсальный и таки понятный)
func1 = lambda x: x+1
def func2(x):
return x+1
Первое — лямбда в смысле собственно языка, второе — именованная функция. Делают одинаковые вещи. В обоих случаях это функция первого класса, но не замыкание (или, математически, можно назвать замыканием на 0 параметров, тривиальный вырожденный случай — но для обычного разговора это неинтересно).
Порождаем замыкание на 1 параметр:
def make_adder_1(y):
return lambda x: x+y
def make_adder_2(y):
def adder(x):
return x+y
return adder
make_adder(3), например, создаст функцию с 1 аргументом, которая возвращает этот аргумент, увеличенный на 3.
Такие же две функции внутри, которые стали замыканиями из-за использования параметра — переменной из внешнего контекста, и две снаружи, которые порождают такие замыкания. (Из-за свойства Python, что замыкание тянет за собой весь внешний фрейм по ссылке, лучше генерацию замыкания выносить в отдельную функцию с минимальным количеством локальных переменных. Или я тут отстал и это уже не актуально?)
Теперь C++. Именованных вложенных функций там нет, всё делается через синтаксис, который называется Lambda expressions. Но есть большая разница, созданная им вложенная функция будет хранить какой-то контекст или нет (компилятор может найти причины соптимизировать это).
Например:
auto f = [] {
printf("Hello from lambda\n");
};
f();
Тут просто выделение части кода в отдельный блок (иногда удобно чтобы не распространять локальный контекст). И снова это то тривиальное замыкание на 0 параметров, которое простой компилятор может создать полноценную функцию первого класса в виде std::function<сигнатура> (или аналога, дальше не уточняю). Более умный наверняка имеет средства отбросить подобное усложнение (для GCC, Clang это точно так).
auto f = [] {
printf("Hello from lambda\n");
};
foo(f);
Если foo() принимает коллбэк, это объект типа std::function<>, но он ничего не сохранил из контекста, внутри объекта только адрес для вызова.
int x;
auto f = [&] {
printf("Hello %d%% from lambda\n", x);
};
x = 100;
f();
x = 200;
f();
С одной стороны, x запомнено по значению, с другой, компилятор может не создавать полноценный std::function, а иначе сделать — неважно как, лишь бы x передавался в f. Оно расскажет про 100% и 200%.
int x;
auto f = [&] {
printf("Hello %d%% from lambda\n", x);
};
x = 100;
foo(f);
x = 200;
foo(f);
А тут без контекста (хранящего адрес x) не обойтись. В foo() уходит объект контекста с одним дополнительно запомненным параметром — &x (и, естественно, кого вызывать с этим параметром).
Точно так же, вызвать — расскажет про 100% и 200%.
int x = 0;
auto f = [=] {
printf("Hello %d%% from lambda\n", x);
};
x = 100;
foo(f);
x = 200;
foo(f);
Тут будет 0% в обоих случаях, запомнено будет значение x в момент создания замыкания, а не его адрес. Это уже особенности C++.
С третьей стороны, ничто не мешает сделать замыкание и не через лямбду языка. Например, libjit — популярный вариант до появления готового аналога C++11, или и сейчас в C без C++. Лямбды C++ переложили заботу о генерации кода для всех платформ на компилятор, а тут — авторы библиотеки.
Вдогонку: вспомнил, как мы ещё в 90-х творили подобные штуки на Фортране С соглашениями вызова, принятых в x86-{16,32}, параметры передаются на стеке. Генератор "замыкания" аллоцировал память под кусочек кода, который писал по байтам, типа такого:
pop cx ; адрес возврата. не помню, какой регистр был свободен, пусть это cx
push <константа>
push cx
jmp <функция вокруг которой делалось замыкание>
У внутренней функции первым параметром было cookie, которое она дальше разбирала (обычно — указатель на структуру).
Современные аналоги делают почти то же самое 1:1 (только с регистровыми соглашениями вызова для x86-64 добавляется сильная громоздкость этому процессу).