Не уверен в правильности выбора раздела, но все же.
Есть четкое определение отличительны свойств лямбды и замыкания?
Или то и другое просто анонимные функции? В чем их ключевые различия?
Что может одна и не может другая конструкция?
Здравствуйте, vaa, Вы писали:
vaa>Не уверен в правильности выбора раздела, но все же. vaa>Есть четкое определение отличительны свойств лямбды и замыкания? vaa>Или то и другое просто анонимные функции? В чем их ключевые различия? vaa>Что может одна и не может другая конструкция?
Здравствуйте, vaa, Вы писали:
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 параметров, тривиальный вырожденный случай — но для обычного разговора это неинтересно).
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 добавляется сильная громоздкость этому процессу).
Здравствуйте, vaa, Вы писали:
vaa>Есть четкое определение отличительны свойств лямбды и замыкания?
Есть. Лямбда — это функция без имени. В классике с одним параметром. Множество образовывалось вложенностью a => b => c => ...
Замыкание — это захват переменных из внешней области. Может быть не только у функции, но и у класса или локальной функции. Например, в Яве локальные классы могут заыкаться на тех в которые они вложены. Для этого у локальных классов добавляется невидимо поле в которое кладется ссылка на внешний объект.
vaa>Или то и другое просто анонимные функции? В чем их ключевые различия?
Это разные концепции. Замыканием могут быть именованные функции, лямбды (безымянные функции), классы и анонимные классы.
vaa>Что может одна и не может другая конструкция?
Это все равно что спросить что может автомобиль и не может авторучка. По сути класс — это замыкание метода на экземпляр объекта. Просто в ООП никто не глядел на это под таким углом.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Да, в голове маленьком прояснилось, спасибо, я имел ввиду что вот в емакслиспе обе эти конструкции
имеют четкое различие в семантике:
(setq a 10)
(setq f1 (lambda (b)
(+ a b)))
(setq lexical-binding nil);; => (lambda (b) (+ a b))
(funcall f1 1) ;; => 11
(let ((a 20))
(funcall f1 1));; => 21
(setq lexical-binding t);; => (closure (t) (b) (+ a b))
(funcall f1 1) ;; => 11
(let ((a 20))
(funcall f1 1));; => 11
А если взять тот же C# то там замыкание как в питоне неотъемлемое свойство любого вложенного метода/функции.
Т.е. очень сильно размыты понятия
согласно справочника Албахари, там такая градация:
1. лямбда-выражения, которые могут быть либо экзмепляром делегата(Func/Action и т.п.) либо деревом выражений Expression<TDelegate>
последний вычисляется в рантайме.
и там написано, что лямбда-выражение захватившее внешнюю переменную, называется замыканием(по сути компилятор смотрит куда поместить переменную/когда ее можно уничтожить).
(оказывается вот так просто).
2. анонимные методы которые записываются в форме делегатов,
delegate (int x) {return x;};
отличается от 1-го только тем, что можно опускать параметры, но обязательно указывать тип, и обязательно скобки для тела, вместо стрелки.
Вообщем, по мне довольно много лишних сущностей.
в лиспе проще ориентироваться. видно прям какой тип создан. а в C# когда смотришь в дизассемблер, то все превращается в анонимные методы.
Здравствуйте, VladD2, Вы писали:
VD>Это все равно что спросить что может автомобиль и не может авторучка. По сути класс — это замыкание метода на экземпляр объекта. Просто в ООП никто не глядел на это под таким углом.
Про "никто не глядел" — "отучаемся говорить за всех" (tm). ООП разный это большой мир:
1) В Питоне подобный принцип вообще-то явно формулируется, и любой не переопределённый в конкретном объекте метод доступен и как Class.foo(obj, *args), и как obj.foo(*args).
2) Аналогично для boost::bind, можно задать объект и имя метода, а можно класс, имя метода и указатель на объект.
3) Классика JavaScript до введения явных объектов — создание функции, локальные переменные которой используются как поля объекта, типа:
Здравствуйте, vaa, Вы писали:
vaa>Здравствуйте, netch80, Вы писали:
vaa>Да, в голове маленьком прояснилось, спасибо, я имел ввиду что вот в емакслиспе обе эти конструкции vaa>имеют четкое различие в семантике:
Ну по этому примеру я понял, что отличается то, откуда может быть произведён захват переменной. Причём, формально "лямбда" тут одинаковая, но при lexical-binding false — формируется и тут же выполняется замыкание с учётом локального let, а при true — без него. Больше выглядит как странность языка, а не как осмысленная задача, и lexical-binding выглядит странноватым костылём.
Ценность замыкания не видна на этом примере. Чтобы что-то увидеть, надо сформировать замыкание как готовый функциональный объект и потом передать его куда-то в другое место, за пределами текущего контекста. Попробуйте пример с функцией, которая возвращает созданную лямбду. Ну пусть тот же make_adder, для простоты и общности. Выполнилось что-то сложное, в результате у вас в какой-то переменной просто функция. Вы её вызываете — что будет?
Если lexical-binding включено, будет ли оно искать это `a` в текущем контексте, а не в контексте, в котором замыкание было создано?
vaa>А если взять тот же C# то там замыкание как в питоне неотъемлемое свойство любого вложенного метода/функции.
Не обязательно любого. Если тело функции не требует захвата значений из контекста, то наверняка это оптимизировано в сторону упрощения.
Но ещё в C# 1 "делегатам" можно было назначать методы конкретных объектов, это уже были замыкания, хоть и не в удобном виде. Вообще, любое замыкание в почти всех ОО языках определено как объект (обычно сгенерированного на ходу) специального (параметризованного) типа, у которого есть сохранённые значения и фиксированный метод для вызова с подстановкой параметров.
vaa>Т.е. очень сильно размыты понятия
Да. Ну это уже последствие того, что ЯП очень разные.
vaa>согласно справочника Албахари, там такая градация: vaa>1. лямбда-выражения, которые могут быть либо экзмепляром делегата(Func/Action и т.п.) либо деревом выражений Expression<TDelegate> vaa>последний вычисляется в рантайме. vaa>и там написано, что лямбда-выражение захватившее внешнюю переменную, называется замыканием(по сути компилятор смотрит куда поместить переменную/когда ее можно уничтожить).
Да. Но не каждое замыкание — лямбда-выражение, которое что-то захватило. Если вы делаете
class Roo {
void run1(int x) { что-то делаем }
void run2(int x) { ещё что-то делаем }
};
delegate void Moo(int);
Moo moo;
Roo roo1, roo2;
moo += roo1.run1;
moo += roo2.run2;
это тоже замыкание: в список вызываемого внутри moo попадает, что у объекта roo1 надо вызвать run1(), а у roo2 — run2().
vaa>(оказывается вот так просто). vaa>2. анонимные методы которые записываются в форме делегатов, vaa>
vaa>delegate (int x) {return x;};
vaa>
И это тоже (появилось позже, суть та же).
vaa>отличается от 1-го только тем, что можно опускать параметры, но обязательно указывать тип, и обязательно скобки для тела, вместо стрелки. vaa>Вообщем, по мне довольно много лишних сущностей.
По большей части не сущностей (внутри всё примерно одинаково), а синтаксисов. Сначала было громоздко с ручным писанием всего, потом анонимные делегаты, потом добавили автогенерацию классов замыканий и удобный (наверно) синтаксис для лямбд через стрелку,
vaa>в лиспе проще ориентироваться. видно прям какой тип создан.
И как этот тип там называется?
vaa> а в C# когда смотришь в дизассемблер, то все превращается в анонимные методы.
Ну если оно расшифровывает сигнатуру и показывает, из какой части кода вызвана эта генерация, то должно читаться.
Здравствуйте, vaa, Вы писали:
vaa>А если взять тот же C# то там замыкание как в питоне неотъемлемое свойство любого вложенного метода/функции.
Это не так. Не каждая вложенная функция захватывает переменные. И если исходить из того, что замыкание — это техника захвата, то отсюда следует что не любая функция должна порождать замыкание. На практике так и есть, т.е. "неотъемлемое свойство любого" не выполняется.
vaa>Т.е. очень сильно размыты понятия vaa>согласно справочника Албахари, там такая градация: vaa>1. лямбда-выражения, которые могут быть либо экзмепляром делегата(Func/Action и т.п.) либо деревом выражений Expression<TDelegate> vaa>последний вычисляется в рантайме. vaa>и там написано, что лямбда-выражение захватившее внешнюю переменную, называется замыканием(по сути компилятор смотрит куда поместить переменную/когда ее можно уничтожить).
Проблема этой писанины в том, что не только лишь лямбда-выражение может захватывать переменную. И что с этим делать? Считать что локальная функция, захватывающая переменную и использующая технику замыкания не является при этом замыканием? vaa>(оказывается вот так просто).
Просто, но неверно.
vaa>2. анонимные методы которые записываются в форме делегатов, vaa>
vaa>delegate (int x) {return x;};
vaa>
vaa>отличается от 1-го только тем, что можно опускать параметры, но обязательно указывать тип, и обязательно скобки для тела, вместо стрелки. vaa>Вообщем, по мне довольно много лишних сущностей.
Слишком. Наличие имени у функции или то, как она описана (лямбдой ли или делегатом) — вообще не имеет отношения к факту захвата переменной. Не синтаксисом определяется то, будет ли захвачена переменная. vaa>в лиспе проще ориентироваться. видно прям какой тип создан. а в C# когда смотришь в дизассемблер, то все превращается в анонимные методы.
Не анонимные методы указывают на замыкание, а вот такие штуки при обращении к локальным переменным определяющей замыкание функции, т.е. функции, чьи переменные захватываются.
Здравствуйте, VladD2, Вы писали:
VD>... По сути класс — это замыкание метода на экземпляр объекта. Просто в ООП никто не глядел на это под таким углом.
Первоначально псевдоООП в джаваскрипте был построен именно по этому принципу.