Отличительные свойства лямбды и замыкания
От: vaa  
Дата: 08.02.22 02:30
Оценка:
Не уверен в правильности выбора раздела, но все же.
Есть четкое определение отличительны свойств лямбды и замыкания?
Или то и другое просто анонимные функции? В чем их ключевые различия?
Что может одна и не может другая конструкция?
☭ ✊ В мире нет ничего, кроме движущейся материи.
Отредактировано 08.02.2022 2:32 Разраб . Предыдущая версия .
Re: Отличительные свойства лямбды и замыкания
От: samius Япония http://sams-tricks.blogspot.com
Дата: 08.02.22 02:45
Оценка: 6 (1)
Здравствуйте, vaa, Вы писали:

vaa>Не уверен в правильности выбора раздела, но все же.

vaa>Есть четкое определение отличительны свойств лямбды и замыкания?
vaa>Или то и другое просто анонимные функции? В чем их ключевые различия?
vaa>Что может одна и не может другая конструкция?

http://rsdn.org/forum/dotnet/8174124.1
Автор: samius
Дата: 18.01.22
Re: Отличительные свойства лямбды и замыкания
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 08.02.22 08:12
Оценка: 9 (1)
Здравствуйте, 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 параметров, тривиальный вырожденный случай — но для обычного разговора это неинтересно).

Порождаем замыкание на 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 добавляется сильная громоздкость этому процессу).
The God is real, unless declared integer.
Отредактировано 10.02.2022 19:47 netch80 . Предыдущая версия . Еще …
Отредактировано 09.02.2022 6:59 netch80 . Предыдущая версия .
Re: Отличительные свойства лямбды и замыкания
От: VladD2 Российская Империя www.nemerle.org
Дата: 09.02.22 01:35
Оценка: 86 (2)
Здравствуйте, vaa, Вы писали:

vaa>Есть четкое определение отличительны свойств лямбды и замыкания?


Есть. Лямбда — это функция без имени. В классике с одним параметром. Множество образовывалось вложенностью a => b => c => ...
Замыкание — это захват переменных из внешней области. Может быть не только у функции, но и у класса или локальной функции. Например, в Яве локальные классы могут заыкаться на тех в которые они вложены. Для этого у локальных классов добавляется невидимо поле в которое кладется ссылка на внешний объект.

vaa>Или то и другое просто анонимные функции? В чем их ключевые различия?


Это разные концепции. Замыканием могут быть именованные функции, лямбды (безымянные функции), классы и анонимные классы.

vaa>Что может одна и не может другая конструкция?


Это все равно что спросить что может автомобиль и не может авторучка. По сути класс — это замыкание метода на экземпляр объекта. Просто в ООП никто не глядел на это под таким углом.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[2]: Отличительные свойства лямбды и замыкания
От: vaa  
Дата: 09.02.22 01:38
Оценка:
Здравствуйте, netch80, Вы писали:

Да, в голове маленьком прояснилось, спасибо, я имел ввиду что вот в емакслиспе обе эти конструкции
имеют четкое различие в семантике:

(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# когда смотришь в дизассемблер, то все превращается в анонимные методы.
☭ ✊ В мире нет ничего, кроме движущейся материи.
Re[2]: Отличительные свойства лямбды и замыкания
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 09.02.22 06:49
Оценка: 20 (1)
Здравствуйте, VladD2, Вы писали:

VD>Это все равно что спросить что может автомобиль и не может авторучка. По сути класс — это замыкание метода на экземпляр объекта. Просто в ООП никто не глядел на это под таким углом.


Про "никто не глядел" — "отучаемся говорить за всех" (tm). ООП разный это большой мир:

1) В Питоне подобный принцип вообще-то явно формулируется, и любой не переопределённый в конкретном объекте метод доступен и как Class.foo(obj, *args), и как obj.foo(*args).

2) Аналогично для boost::bind, можно задать объект и имя метода, а можно класс, имя метода и указатель на объект.

3) Классика JavaScript до введения явных объектов — создание функции, локальные переменные которой используются как поля объекта, типа:

myclass = function() {
  foo = 1;
  function getFoo() { return foo; }
  function setFoo(x) { foo = x+1; }
  return { 'getFoo': getFoo, 'setFoo': setFoo };
};
obj1 = myclass();
y = obj1.getFoo();


тогда объектом является фрейм переменных функции.

Это только три варианта с ходу, а их наверняка больше.
The God is real, unless declared integer.
Re[3]: Отличительные свойства лямбды и замыкания
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 09.02.22 07:24
Оценка: 8 (2)
Здравствуйте, 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# когда смотришь в дизассемблер, то все превращается в анонимные методы.


Ну если оно расшифровывает сигнатуру и показывает, из какой части кода вызвана эта генерация, то должно читаться.
The God is real, unless declared integer.
Re[3]: Отличительные свойства лямбды и замыкания
От: samius Япония http://sams-tricks.blogspot.com
Дата: 09.02.22 09:02
Оценка: 8 (2)
Здравствуйте, 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# когда смотришь в дизассемблер, то все превращается в анонимные методы.
Не анонимные методы указывают на замыкание, а вот такие штуки при обращении к локальным переменным определяющей замыкание функции, т.е. функции, чьи переменные захватываются.
IL_0000:  newobj      UserQuery+<>c__DisplayClass0_0..ctor
IL_0005:  stloc.0     // CS$<>8__locals0
IL_0006:  nop         
IL_0007:  ldloc.0     // CS$<>8__locals0
IL_0008:  ldc.i4.5    
IL_0009:  stfld       UserQuery+<>c__DisplayClass0_0.v
Re[2]: Отличительные свойства лямбды и замыкания
От: YuriV  
Дата: 11.02.22 10:14
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>... По сути класс — это замыкание метода на экземпляр объекта. Просто в ООП никто не глядел на это под таким углом.


Первоначально псевдоООП в джаваскрипте был построен именно по этому принципу.
Re[3]: Отличительные свойства лямбды и замыкания
От: VladD2 Российская Империя www.nemerle.org
Дата: 11.02.22 18:23
Оценка:
Здравствуйте, YuriV, Вы писали:

YV>Первоначально псевдоООП в джаваскрипте был построен именно по этому принципу.


Да и замыкания в Шарпе через классы реализуются во многих случаях.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.