Указатели функций, Закрытие и Лямбда

Я только сейчас узнаю о указателях функций, и, когда я читал главу K & R по этому вопросу, первое, что ударило меня, было: «Эй, это вроде как закрытие». Я знал, что это предположение в корне неверно, и после поиска в Интернете я не нашел никакого анализа этого сравнения.

Итак, почему указатели функций C-стиля принципиально отличаются от замыканий или lambda? Насколько я могу судить, это связано с тем, что указатель функции по-прежнему указывает на определенную (названную) функцию, а не на анонимное определение функции.

Почему передача функции функции считается более мощной во втором случае, где она неназванная, чем первая, где это просто обычная повседневная функция, которая передается?

Пожалуйста, скажите мне, как и почему я ошибаюсь, чтобы сравнить их так близко.

Благодарю.

Лямбда (или замыкание ) инкапсулирует как указатель функции, так и переменные. Вот почему в C # вы можете сделать:

int lessThan = 100; Func lessThanTest = delegate(int i) { return i < lessThan; }; 

Я использовал анонимный делегат там как закрытие (его синтаксис немного яснее и ближе к C, чем lambda-эквивалент), который захватывал lessThan (переменную стека) в закрытие. Когда вычисление закрывается, lessThan (чей стек стека может быть уничтожен) будет продолжать ссылаться. Если я изменю lessThan, то я изменил сравнение:

 int lessThan = 100; Func lessThanTest = delegate(int i) { return i < lessThan; }; lessThanTest(99); // returns true lessThan = 10; lessThanTest(99); // returns false 

В C это было бы незаконным:

 BOOL (*lessThanTest)(int); int lessThan = 100; lessThanTest = &LessThan; BOOL LessThan(int i) { return i < lessThan; // compile error - lessThan is not in scope } 

хотя я мог бы определить указатель функции, который принимает 2 аргумента:

 int lessThan = 100; BOOL (*lessThanTest)(int, int); lessThanTest = &LessThan; lessThanTest(99, lessThan); // returns true lessThan = 10; lessThanTest(100, lessThan); // returns false BOOL LessThan(int i, int lessThan) { return i < lessThan; } 

Но теперь я должен передать 2 аргумента, когда я его оцениваю. Если бы я хотел передать эту функцию указателю на другую функцию, где lessThan не был в области видимости, мне пришлось бы либо вручную сохранить ее, передав ее каждой функции в цепочке, либо продвинув ее в глобальную.

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

Реферат: закрытие представляет собой комбинацию указателя функции + захваченных переменных.

Как кто-то, кто написал компиляторы для языков как с «настоящими» закрытиями, так и без них, я почтительно не согласен с некоторыми из приведенных выше ответов. Закрытие Lisp, Scheme, ML или Haskell не создает новую функцию динамически . Вместо этого он повторно использует существующую функцию, но делает это с новыми свободными переменными . Сбор свободных переменных часто называют средой , по крайней мере теоретиками-программистами.

Закрытие – это всего лишь совокупность, содержащая функцию и среду. В стандартном ML-компиляторе Нью-Джерси мы представили его как запись; одно поле содержало указатель на код, а остальные поля содержали значения свободных переменных. Компилятор динамически создавал новую функцию (не функцию) , выделяя новую запись, содержащую указатель на тот же код, но с разными значениями для свободных переменных.

Вы можете имитировать все это на C, но это боль в заднице. Популярны два метода:

  1. Передайте указатель на функцию (код) и отдельный указатель на свободные переменные, чтобы замыкание было разделено на две переменные C.

  2. Передайте указатель на структуру, где struct содержит значения свободных переменных, а также указатель на код.

Техника № 1 идеальна, когда вы пытаетесь моделировать какой-то polymorphism в C, и вы не хотите раскрывать тип среды – вы используете указатель void * для представления среды. Например, посмотрите на интерфейсы и реализации Dave Hanson. Техника № 2, более близкая к тому, что происходит в компиляторах родного кода для функциональных языков, также напоминает другой знакомый метод … Объекты C ++ с виртуальными функциями-членами. Реализации практически идентичны.

Это наблюдение привело к тому, что Генри Бейкер сделал это:

Люди в мире Алгол / Фортран жаловались годами, что они не понимают, какие возможные закрытия функций функций будут иметь в эффективном программировании будущего. Затем произошла революция «объектно-ориентированного программирования», и теперь все программы используют закрытие функций, за исключением того, что они по-прежнему отказываются их называть.

В C вы не можете определить функцию inline, поэтому вы не можете создать закрытие. Все, что вы делаете, это передача ссылки на некоторый предопределенный метод. В языках, поддерживающих анонимные методы / закрытие, определение методов намного более гибкое.

В простейших терминах указатели на функции не имеют связанной с ними области (если вы не подсчитываете глобальную область), тогда как закрытие включает область действия метода, который их определяет. С lambdas вы можете написать метод, который пишет метод. Закрытие позволяет вам привязать «некоторые аргументы к функции и получить в результате более низкую функцию». (взято из комментария Томаса). Вы не можете сделать это в C.

EDIT: добавление примера (я собираюсь использовать ActionScript-ish синтаксис, потому что это то, что сейчас на уме):

Скажем, у вас есть какой-то метод, который принимает другой метод в качестве аргумента, но не предоставляет способ передать какие-либо параметры этому методу при его вызове? Как, скажем, какой-то метод, который вызывает задержку перед запуском метода, которым вы его передали (глупый пример, но я хочу сохранить его простым).

 function runLater(f:Function):Void { sleep(100); f(); } 

Теперь скажите, что вы хотите, чтобы пользователь runLater () задерживал некоторую обработку объекта:

 function objectProcessor(o:Object):Void { /* Do something cool with the object! */ } function process(o:Object):Void { runLater(function() { objectProcessor(o); }); } 

Функция, которую вы передаете процессу (), больше не является частью статической функции. Он динамически генерируется и может включать ссылки на переменные, которые были в области, когда метод был определен. Таким образом, он может получить доступ к «o» и «objectProcessor», даже если они не находятся в глобальной области.

Надеюсь, это имело смысл.

Закрытие = логика + окружающая среда.

Например, рассмотрите этот метод C # 3:

 public Person FindPerson(IEnumerable people, string name) { return people.Where(person => person.Name == name); } 

Выражение lambda не только инкапсулирует логику («сравнивает имя»), но также среду, включая параметр (то есть локальную переменную) «имя».

Более подробно об этом, посмотрите мою статью о закрытии, которая проведет вас через C # 1, 2 и 3, показывая, как замыкания облегчают ситуацию.

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

Какой объект является указателем на вложенную функцию? Это не просто адрес кода, потому что, если вы его вызываете, как он обращается к переменным внешней функции? (Помните, что из-за рекурсии одновременно может быть несколько разных вызовов внешней функции.) Это называется проблемой funarg , и есть две подзадачи: проблема с нисходящими лугами и проблема с лучшими проблемами.

Проблема с пучками вниз, т. Е. Отправка указателя функции «вниз по стеку» в качестве аргумента функции, которую вы вызываете, на самом деле несовместима с C, а GCC поддерживает вложенные функции как нисходящие funargs. В GCC, когда вы создаете указатель на вложенную функцию, вы действительно получаете указатель на батут , динамически построенный fragment кода, который устанавливает статический указатель ссылки, а затем вызывает реальную функцию, которая использует статический указатель ссылки для доступа переменные внешней функции.

Проблема с восходящими лугами сложнее. GCC не мешает вам указывать указатель батута после того, как внешняя функция больше не активна (не имеет записи в стеке вызовов), а затем статический указатель ссылки может указывать на мусор. Записи активации больше не могут быть выделены в стеке. Обычным решением является выделение их в куче, и пусть объект функции, представляющий вложенную функцию, просто указывает на запись активации внешней функции. Такой объект называется замыканием . Тогда язык, как правило, должен поддерживать garbage collection, чтобы записи могли быть освобождены, когда больше нет указателей, указывающих на них.

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

Лямбда – анонимная, динамически определенная функция. Вы просто не можете сделать это в C … как для закрытий (или для конвекции двух), типичный пример lisp будет выглядеть примерно так:

 (defun get-counter (n-start +-number) "Returns a function that returns a number incremented by +-number every time it is called" (lambda () (setf n-start (+ +-number n-start)))) 

В терминах C вы можете сказать, что лексическая среда (стек) get-counter захватывается анонимной функцией и внутренне изменена, как показано в следующем примере:

 [1]> (defun get-counter (n-start +-number) "Returns a function that returns a number incremented by +-number every time it is called" (lambda () (setf n-start (+ +-number n-start)))) GET-COUNTER [2]> (defvar x (get-counter 2 3)) X [3]> (funcall x) 5 [4]> (funcall x) 8 [5]> (funcall x) 11 [6]> (funcall x) 14 [7]> (funcall x) 17 [8]> (funcall x) 20 [9]> 

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

Одной из важных проблем с C и замыканиями является то, что переменные, выделенные в стеке, будут уничтожены при выходе из текущей области, независимо от того, указывала ли их закрытие. Это приведет к тому, что люди получат ошибки, когда они небрежно возвращают указатели на локальные переменные. Закрытие в основном подразумевает, что все релевантные переменные пересчитываются или собираются с помощью мусора в кучу.

Мне неудобно приравнивать lambda к закрытию, потому что я не уверен, что лямбды на всех языках являются закрытиями, иногда я думаю, что лямбды только что были локально определены анонимные функции без привязки переменных (Python pre 2.1?).

В GCC можно моделировать lambda-функции, используя следующий макрос:

 #define lambda(l_ret_type, l_arguments, l_body) \ ({ \ l_ret_type l_anonymous_functions_name l_arguments \ l_body \ &l_anonymous_functions_name; \ }) 

Пример из источника :

 qsort (array, sizeof (array) / sizeof (array[0]), sizeof (array[0]), lambda (int, (const void *a, const void *b), { dump (); printf ("Comparison %d: %d and %d\n", ++ comparison, *(const int *) a, *(const int *) b); return *(const int *) a - *(const int *) b; })); 

Использование этой методики, конечно, устраняет возможность использования вашего приложения с другими компиляторами и, по-видимому, является «неопределенным» поведением, поэтому YMMV.

Закрытие фиксирует свободные переменные в среде . Окружающая среда будет по-прежнему существовать, даже если окружающий код больше не может быть активным.

Пример в Common Lisp, где MAKE-ADDER возвращает новое закрытие.

 CL-USER 53 > (defun make-adder (start delta) (lambda () (incf start delta))) MAKE-ADDER CL-USER 54 > (compile *) MAKE-ADDER NIL NIL 

Используя приведенную выше функцию:

 CL-USER 55 > (let ((adder1 (make-adder 0 10)) (adder2 (make-adder 17 20))) (print (funcall adder1)) (print (funcall adder1)) (print (funcall adder1)) (print (funcall adder1)) (print (funcall adder2)) (print (funcall adder2)) (print (funcall adder2)) (print (funcall adder1)) (print (funcall adder1)) (describe adder1) (describe adder2) (values)) 10 20 30 40 37 57 77 50 60 # is a CLOSURE Function # Environment #(60 10) # is a CLOSURE Function # Environment #(77 20) 

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

Common Lisp делает оба замыкания и объекты чистой функции (те, у кого нет среды), как функции, и можно назвать оба таким же образом, используя FUNCALL .

Основное различие возникает из-за отсутствия лексического охвата в C.

Указатель функции – это просто указатель на блок кода. Любая переменная, отличная от стека, которая ссылается на нее, является глобальной, статической или аналогичной.

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

Несколько замыканий могут делиться некоторыми переменными, и поэтому может быть интерфейсом объекта (в смысле ООП). чтобы сделать это на C, вам нужно связать структуру с таблицей указателей функций (это то, что делает C ++, с classом vtable).

Короче говоря, замыкание является указателем функции PLUS в некотором состоянии. это конструкция более высокого уровня

Большинство ответов указывают на то, что закрытие требует указателей на функции, возможно, для анонимных функций, но, поскольку Mark пишет, что закрытие может существовать с именованными функциями. Вот пример в Perl:

 { my $count; sub increment { return $count++ } } 

Закрытие – это среда, которая определяет переменную $count . Он доступен только для подпрограммы increment и сохраняется между вызовами.

В C указатель функции – это указатель, который будет вызывать функцию при ее разыменовании, замыкание – это значение, которое содержит логику функции и среду (переменные и значения, к которым они привязаны), а lambda обычно относится к значению, которое фактически является неназванной функцией. В C функция не является первым значением classа, поэтому она не может быть передана, поэтому вам нужно передать указатель на нее, однако на функциональных языках (например, Scheme) вы можете передавать функции таким же образом, как и любое другое значение

Давайте будем гением компьютера.