Существует много путаницы вокруг лямбд и закрытий, даже в ответах на этот вопрос StackOverflow здесь. Вместо того, чтобы спрашивать случайных программистов, которые узнали о замыканиях на практике с определенными языками программирования или другими невежественными программистами, отправляйтесь в путь к источнику (где все это началось). А поскольку лямбды и затворы взяты из Лямбда-исчисления , изобретенного Алонзо Черчем еще в 30-х годах, еще до того, как появились первые электронные компьютеры, это источник Я говорю о
Лямбда-исчисление - самый простой язык программирования в мире. Единственное, что в нем можно сделать: ►
- APPLICATION: Применение одного выражения к другому, обозначается
f x
.
(Думайте об этом как о вызове функции , где f
- функция, а x
- ее единственный параметр )
- ABSTRACTION: привязывает символ, встречающийся в выражении, для обозначения того, что этот символ является просто «слотом», пустым полем, ожидающим заполнения значением, как бы «переменной». Это делается путем добавления греческой буквы
λ
(лямбда), затем символического имени (например, x
), затем точки .
перед выражением. Затем он преобразует выражение в функцию , ожидающую один параметр .
Например: λx.x+2
принимает выражение x+2
и сообщает, что символ x
в этом выражение - это связанная переменная - его можно заменить значением, указанным в качестве параметра.
Обратите внимание, что функция, определенная таким образом, является анонимной - у нее нет имени, поэтому вы еще не можете обратиться к ней, но вы можете немедленно вызвать ее (помните приложение? ), предоставив ему ожидаемый параметр, например: (λx.x+2) 7
. Затем выражение (в данном случае буквальное значение) 7
подставляется как x
в подвыражении x+2
примененной лямбды, так что вы получаете 7+2
, который затем уменьшается до 9
по общим правилам арифметики.
Итак, мы решили одну из загадок:
лямбда - это анонимная функция из приведенного выше примера λx.x+2
.
<Ч />
В разных языках программирования синтаксис функциональной абстракции (лямбда) может различаться. Например, в JavaScript это выглядит так:
function(x) { return x+2; }
и вы можете сразу применить его к какому-либо параметру:
(function(x) { return x+2; })(7)
или вы можете сохранить эту анонимную функцию (лямбду) в некоторой переменной:
var f = function(x) { return x+2; }
, который фактически дает ему имя f
, что позволяет вам обращаться к нему и вызывать его несколько раз позже, например ::
alert( f(7) + f(10) ); // should print 21 in the message box
Но вам не нужно было называть это. Вы можете позвонить сразу:
alert( function(x) { return x+2; } (7) ); // should print 9 in the message box
В LISP лямбды сделаны так:
(lambda (x) (+ x 2))
и вы можете вызвать такую лямбду, сразу применив ее к параметру:
( (lambda (x) (+ x 2)) 7 )
<ч />
Хорошо, теперь пришло время разгадать другую загадку: что такое замыкание .
Для этого давайте поговорим о символах ( переменных ) в лямбда-выражениях.
Как я уже сказал, лямбда-абстракция связывает символ в своем подвыражении, так что он становится заменяемым параметром . Такой символ называется bound . Но что, если в выражении есть другие символы? Например: λx.x/y+2
. В этом выражении символ x
связан лямбда-абстракцией λx.
, предшествующей ему. Но другой символ, y
, не связан - он свободен . Мы не знаем, что это такое и откуда оно берется, поэтому мы не знаем, что это означает и какое значение оно представляет, и поэтому мы не можем оценить это выражение до выяснить, что означает y
.
Фактически, то же самое относится и к двум другим символам, 2
и +
. Просто мы настолько знакомы с этими двумя символами, что обычно забываем, что компьютер их не знает, и нам нужно сказать, что они означают, определив их где-то, например. в библиотеке или на самом языке.
YoВы можете думать о свободных символах, как они определены где-то еще, вне выражения, в его «окружающем контексте», который называется его окружением . Окружение может быть большим выражением, частью которого является это выражение (как сказал Квай-Гон Джинн: «всегда есть большая рыба»;)), или в какой-то библиотеке, или в самом языке (как примитив ) ).
Это позволяет разделить лямбда-выражения на две категории:
- ЗАКРЫТЫЕ выражения: каждый символ, встречающийся в этих выражениях, связан некоторой лямбда-абстракцией. Другими словами, они автономны ; они не требуют какого-либо окружающего контекста для оценки. Их также называют комбинаторами .
- ОТКРЫТЫЕ выражения: некоторые символы в этих выражениях не связаны - то есть некоторые символы, встречающиеся в них, свободны и требуют некоторой внешней информации, и поэтому они не могут оцениваться до тех пор, пока вы не предоставите определения этих символов.
Вы можете ЗАКРЫТЬ лямбда-выражение open , предоставив среду , которая определяет все эти свободные символы, связывая их с некоторыми значениями (которые могут быть числами, строками, анонимными функциями, или лямбды, что угодно ...).
И вот идет закрытие часть:
замыкание лямбда-выражения - это конкретный набор символов, определенных во внешнем контексте (среде), которые дают значения свободных символов в этом выражении, делая они несвободны больше. Он превращает открытое лямбда-выражение, которое все еще содержит некоторые «неопределенные» свободные символы, в закрытое , которое больше не имеет свободных символов.
Например, если у вас есть следующее лямбда-выражение: λx.x/y+2
, символ x
связан, а символ y
свободен, поэтому выражение open
и не может быть оценено, если вы не скажете, что y
означает (и то же самое с +
и 2
, которые также бесплатны). Но предположим, что у вас также есть окружение , подобное этому:
{ y: 3,
+: [built-in addition],
2: [built-in number],
q: 42,
w: 5 }
Эта среда предоставляет определения для всех "неопределенных" (свободных) символов из нашего лямбда-выражения (y
, +
, 2
) и нескольких дополнительных символов (q
, w
). Символы, которые нам нужно определить, являются этим подмножеством среды:
{ y: 3,
+: [built-in addition],
2: [built-in number] }
и это как раз замыкание нашего лямбда-выражения:>
Другими словами, закрывает открытое лямбда-выражение. Отсюда и название closure , и именно поэтому ответы многих людей в этой теме не совсем верны: P
<Ч />
Так почему они ошибаются? Почему многие из них говорят, что замыкания - это некоторые структуры данных в памяти или некоторые особенности языков, которые они используют, или почему они путают замыкания с лямбдами? : P
Ну, виноваты корпоративные маркетоиды Sun / Oracle, Microsoft, Google и т. Д., Потому что именно так они называли эти конструкции на своих языках (Java, C #, Go и т. Д.). Они часто называют «замыканиями», которые должны быть просто лямбдами. Или они называют «замыкания» конкретным методом, который они использовали для реализации лексической области видимости, то есть тот факт, что функция может получить доступ к переменным, которые были определены во внешней области во время ее определения. Они часто говорят, что функция «заключает» эти переменные, то есть записывает их в некоторую структуру данных, чтобы спасти их от уничтожения после завершения выполнения внешней функции. Но это всего лишь выдуманная post factum «фольклорная этимология» и маркетинг, который только делает вещи более запутанными, потому что каждый поставщик языков использует свою собственную терминологию.
И это еще хуже из-за того факта, что в их словах всегда есть доля правды, которая не позволяет вам легко отклонить это как ложное: P Позвольте мне объяснить:
Если вы хотите реализовать язык, который использует лямбды в качестве первоклассных граждан, вам нужно разрешить им использовать символы, определенные в их окружающем контексте (то есть использовать свободные переменные в ваших лямбдах). И эти символы должны быть там, даже когда окружающая функция возвращается. Проблема в том, что эти символы привязаны к некоторому локальному хранилищу функции (обычно в стеке вызовов), которого больше не будет, когда функция вернется. Следовательно, чтобы лямбда работала так, как вы ожидаете, вам нужно каким-то образом «захватить» все эти свободные переменные из внешнего контекста и сохранить их на потом, даже когда внешний контекст исчезнет. То есть вам нужно найти замыкание вашей лямбды (все эти внешние переменные, которые она использует) и сохранить ее где-то еще (либо путем создания копии, либо путем подготовки пространства для них заранее, где-то еще, чем на стек). Фактический метод, который вы используете для достижения этой цели, является «подробностью реализации» вашего языка. Здесь важно замыкание , которое представляет собой набор свободных переменных из окружения вашей лямбды, которые нужно где-то сохранить.
Людям не потребовалось слишком много времени, чтобы начать называть фактическую структуру данных, которую они используют в реализациях своего языка, для реализации замыкания как самого замыкания. Структура обычно выглядит примерно так:
Closure {
[pointer to the lambda function's machine code],
[pointer to the lambda function's environment]
}
и эти структуры данных передаются в качестве параметров другим функциям, возвращаются из функций и сохраняются в переменных для представления лямбда-выражений и предоставления им доступа к окружающей их среде, а также к машинному коду для выполнения в этом контексте. Но это просто (один из многих) способ реализовать закрытие , а не само закрытие .
Как я объяснил выше, закрытие лямбда-выражения - это подмножество определений в его среде, которые дают значения свободным переменным, содержащимся в этом лямбда-выражении, фактически закрывая выражение (поворачивая open лямбда-выражение, которое еще не может быть оценено, в закрытое лямбда-выражение, которое затем может быть оценено, поскольку все символы, содержащиеся в нем, теперь определены).
Все остальное - это просто "культ груза" и "магия voo-doo" программистов и поставщиков языков, не подозревающих о реальных корнях этих понятий.
Надеюсь, это ответит на ваши вопросы. Но если у вас есть дополнительные вопросы, не стесняйтесь задавать их в комментариях, и я постараюсь объяснить их лучше.