Покажчики функцій, замикання та лямбда


86

Я тільки зараз дізнаюся про покажчики функцій, і, коли я читав розділ K&R на цю тему, перше, що мене вразило, було: "Гей, це як би закриття". Я знав, що це припущення якимось чином неправильно, і після пошуку в Інтернеті я не знайшов насправді жодного аналізу цього порівняння.

То чому вказівники на функції у стилі С принципово відрізняються від закриття чи лямбда? Наскільки я можу сказати, це пов’язано з тим, що покажчик функції все ще вказує на визначену (іменовану) функцію, на відміну від практики анонімного визначення функції.

Чому передача функції функції розглядається як потужніша у другому випадку, коли вона не називається, ніж у першій, де це просто звичайна, повсякденна функція, яка передається?

Скажіть, будь ласка, як і чому я помиляюся, так порівнюючи ці два.

Дякую.

Відповіді:


108

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

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

Я використовував анонімний делегат там як закриття (це синтаксис трохи чіткіший і ближчий до C, ніж лямбда-еквівалент), який захопив у закритті меншеThan (змінну стека). Коли оцінюється закриття, посилання буде менше, ніж (кадр стеку якого може бути знищений). Якщо я змінюю менше, ніж тоді, то я змінюю порівняння:

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

Але тепер я повинен передати два аргументи, коли я це оцінюю. Якби я хотів передати цей вказівник на функцію іншій функції, де меншеThan не було в обсязі, мені довелося б або тримати його в ручному режимі, передаючи його кожній функції в ланцюжку, або просуваючи його до глобальної.

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

Короткий зміст: закриття - це комбінація покажчика функції + захоплених змінних.


спасибі, ти справді загнав додому ідею інших людей, куди намагаються потрапити.
Немає

Ви, напевно, використовували стару версію C, коли писали це, або не пам’ятали переадресувати функцію, але я не спостерігаю тієї самої поведінки, яку ви згадали під час тестування. ideone.com/JsDVBK
smac89

@ smac89 - ви зробили змінну lessThan глобальною - я прямо зазначив це як альтернативу.
Марк Брекетт

42

Як хтось, хто написав компілятори для мов як із “реальними” закриттями, так і без них, я з повагою не погоджуюся з деякими з наведених вище відповідей. Закриття Lisp, Scheme, ML або Haskell не створює нову функцію динамічно . Натомість він повторно використовує існуючу функцію, але робить це з новими вільними змінними . Набір вільних змінних часто називають середовищем , принаймні теоретиками мови програмування.

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

Ви можете моделювати все це в C, але це біль у дупі. Популярні дві техніки:

  1. Передайте вказівник на функцію (код) та окремий вказівник на вільні змінні, щоб закриття було розділене на дві змінні C.

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

Техніка №1 ідеальна, коли ви намагаєтеся імітувати якийсь поліморфізм в C, і ви не хочете розкривати тип середовища - ви використовуєте покажчик void * для представлення середовища. Для прикладів подивіться на Інтерфейси та реалізації Дейва Хенсона . Техніка №2, яка більше нагадує те, що відбувається в компіляторах власного коду для функціональних мов, також нагадує іншу звичну техніку ... Об'єкти C ++ з функціями віртуальних членів. Реалізації майже однакові.

Це спостереження призвело до мудрості Генрі Бейкера:

Люди у світі Алгол / Фортран роками скаржились, що не розуміють, яке можливе закриття функцій може мати ефективне програмування майбутнього. Потім сталася революція в "об'єктно-орієнтованому програмуванні", і тепер усі програми, що використовують закриття функцій, за винятком того, що вони все ще відмовляються називати їх так.


1
+1 для пояснення та цитата про те, що ООП насправді закривається - повторно використовує існуючу функцію, але робить це з новими вільними змінними - функціями (методами), які приймають середовище (вказівник структури на дані екземпляра об'єкта, що є нічим іншим, як новими станами) оперувати.
legends2k

8

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

Найпростішими словами, покажчики функцій не мають пов'язаної з ними області дії (якщо не враховувати загальну область дії), тоді як закриття включають область застосування методу, який їх визначає. З лямбдами ви можете написати метод, який пише метод. Закриття дозволяють зв’язати "деякі аргументи з функцією і в результаті отримати функцію нижчого рівня". (взято з коментаря Томаса). Ви не можете цього зробити в C.

РЕДАКТУВАТИ: Додавання прикладу (я буду використовувати синтаксис 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); });
}

Функція, яку ви передаєте process (), вже не є якоюсь статично визначеною функцією. Він генерується динамічно і може включати посилання на змінні, які були в області дії, коли метод був визначений. Отже, він може отримати доступ до 'o' та 'objectProcessor', навіть якщо вони не входять до загальної області.

Сподіваюсь, це мало сенс.


Я змінив свою відповідь на основі вашого коментаря. Я все ще не на 100% зрозумілий у специфіці термінів, тому я просто цитував вас безпосередньо. :)
Гермс

Вбудована здатність анонімних функцій є деталлю реалізації (більшості?) Основних мов програмування - це не є вимогою до закриття.
Mark Brackett,

6

Закриття = логіка + середовище.

Наприклад, розглянемо цей метод C # 3:

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

Лямбда-вираз не лише інкапсулює логіку ("порівняти ім'я"), але й середовище, включаючи параметр (тобто локальну змінну) "ім'я".

Детальніше про це погляньте на мою статтю про закриття, яка проведе вас через C # 1, 2 та 3, де показано, як закриття полегшує ситуацію.


розгляньте можливість заміни void на IEnumerable <Person>
Amy B

1
@ David B: Вітаємо, готово. @edg: Я думаю, що це більше, ніж просто стан, тому що це мінливий стан. Іншими словами, якщо ви виконуєте закриття, яке змінює локальну змінну (поки все ще знаходиться в методі), ця локальна змінна теж змінюється. «Навколишнє середовище», здається, передає мені це краще, але воно вовняне.
Джон Скіт,

Я ціную відповідь, але це насправді нічого не зрозуміло для мене, схоже, що люди - це просто об’єкт, і ви викликаєте на ньому метод. Можливо, це просто я не знаю C #.
Немає

Так, він викликає метод на ньому, але параметр, який він передає, - це закриття.
Джон Скіт,

4

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

Що це за об'єкт - вказівник на вкладену функцію? Це не може бути просто адреса коду, тому що якщо ви викликаєте його, як він отримує доступ до змінних зовнішньої функції? (Пам'ятайте, що через рекурсію одночасно може бути кілька різних викликів зовнішньої функції.) Це називається проблемою фунарга , і є дві підзадачі: проблема фунаргів вниз і проблема фунаргів вгору.

Проблема зниження funargs, тобто надсилання покажчика на функцію "вниз по стеку" як аргументу функції, яку ви викликаєте, насправді не є несумісною з C, і GCC підтримує вкладені функції як низхідні funargs. У GCC, коли ви створюєте вказівник на вкладену функцію, ви дійсно отримуєте вказівник на батут , динамічно побудований фрагмент коду, який встановлює статичний вказівник посилання, а потім викликає реальну функцію, яка використовує статичний вказівник посилання для доступу змінні зовнішньої функції.

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

Лямбди ( анонімні функції ) насправді є окремою проблемою, але зазвичай мова, яка дозволяє визначати анонімні функції на льоту, також дозволяє повертати їх як значення функцій, тому вони в кінцевому підсумку закриваються.


3

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

(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захоплюється анонімною функцією та модифікується внутрішньо, як показано в наступному прикладі:

[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]> 

2

Закриття означає, що якась змінна з точки зору визначення функції пов'язана разом з логікою функції, як можливість оголосити міні-об'єкт на льоту.

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

Мені не комфортно ототожнювати лямбду з закриттям, тому що я не впевнений, що лямбди у всіх мовах є закриттями, часом мені здається, що лямбди були лише локально визначеними анонімними функціями без прив’язки змінних (Python pre 2.1?).


2

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

Звичайно, використання цієї техніки усуває можливість роботи вашої програми з іншими компіляторами і, мабуть, є "невизначеною" поведінкою, тому YMMV.


2

Закриття захоплює вільні змінні в середовищі . Середовище все ще існуватиме, навіть якщо оточуючий код може бути неактивним.

Приклад у 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функція показує, що об’єкти функції для обох закриттів однакові, але оточення відрізняється.

Common Lisp робить як закриття, так і чисті функціональні об'єкти (ті, що не мають середовища) і функціями, і можна викликати обидва однаково, використовуючи тут FUNCALL.


1

Основна відмінність виникає через відсутність лексичного обсягу в C.

Покажчик функції - це саме це, вказівник на блок коду. Будь-яка не стекова змінна, на яку вона посилається, є глобальною, статичною або подібною.

Закриття, OTOH, має свій власний стан у вигляді „зовнішніх змінних”, або „вищих значень”. вони можуть бути настільки приватними або спільними, як ви хочете, використовуючи лексичний обсяг. Ви можете створити багато закриттів з однаковим кодом функції, але різними екземплярами змінних.

Кілька закриттів можуть містити деякі змінні, і таким чином може бути інтерфейс об'єкта (у сенсі ООП). щоб зробити так, що в C вам потрібно пов'язати структуру з таблицею покажчиків на функції (це те, що робить C ++, з класом vtable).

коротше, замикання - це покажчик функції ПЛЮС деякого стану. це конструкція вищого рівня


2
WTF? C безумовно має лексичний обсяг.
Луїс Олівейра

1
він має "статичний масштаб". наскільки я розумію, лексичний масштаб є більш складною функцією для підтримки подібної семантики в мові, яка має динамічно створені функції, які потім називаються закриттями.
Хав'єр

1

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

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

Закриття - це середовище, яке визначає $countзмінну. Він доступний лише для incrementпідпрограми і зберігається між дзвінками.


0

У C покажчик функції - це вказівник, який буде викликати функцію, коли ви її визначите, закриття - це значення, яке містить логіку функції та середовище (змінні та значення, до яких вони прив'язані), а лямбда-значення зазвичай посилається на значення, яке насправді є неназваною функцією. У C функція не є першокласним значенням, тому її не можна передавати, тому замість цього потрібно передати на неї вказівник, однак у функціональних мовах (наприклад, Scheme) ви можете передавати функції таким же чином, як і будь-яке інше значення

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.