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

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

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

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

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

Спасибо.

Ответ 1

Lambda (или closure) инкапсулирует как указатель на функцию, так и переменные. Вот почему в С# вы можете:

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

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

int lessThan = 100;
Func<int, bool> 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 не был в области видимости, мне пришлось бы либо вручную сохранить ее, передав ее каждой функции в цепочке, либо продвинув ее к глобальной.

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

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

Ответ 2

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

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

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

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

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

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

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

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

Ответ 3

В 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", даже если они не входят в глобальную область.

Я надеюсь, что это имело смысл.

Ответ 4

Закрытие = логическая + среда.

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

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

Лямбда-выражение не только инкапсулирует логику ( "сравнивает имя" ), но также среду, включая параметр (то есть локальную переменную) "имя".

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

Ответ 5

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

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

Проблема с пучками вниз, т.е. отправка указателя функции "вниз по стеку" в качестве аргумента для вызываемой функции, на самом деле несовместима с C и GCC поддерживает вложенные функции как downward funargs. В GCC, когда вы создаете указатель на вложенную функцию, вы действительно получаете указатель на trampoline, динамически построенный фрагмент кода который устанавливает статический указатель ссылки, а затем вызывает реальную функцию, которая использует статический указатель ссылки для доступа к переменным внешней функции.

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

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

Ответ 6

Лямбда - анонимная, динамически определенная функция. Вы просто не можете сделать это в 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]> 

Ответ 7

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

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

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

Ответ 8

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

#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;
             }));

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

Ответ 9

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

Пример в 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 
#<Closure 1 subfunction of MAKE-ADDER 4060001ED4> is a CLOSURE
Function         #<Function 1 subfunction of MAKE-ADDER 4060001CAC>
Environment      #(60 10)
#<Closure 1 subfunction of MAKE-ADDER 4060001EFC> is a CLOSURE
Function         #<Function 1 subfunction of MAKE-ADDER 4060001CAC>
Environment      #(77 20)

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

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

Ответ 10

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

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

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

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

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

Ответ 11

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

Ответ 12

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

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

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