Замыкание (компьютерное программирование)
В языках программирования замыкание с , а также лексическое замыкание или замыкание функции , представляет собой метод реализации лексической областью действия привязки имени на языке с первоклассными функциями . С функциональной точки зрения замыкание — это запись, хранящая функцию. [а] вместе с окружающей средой. [1] Среда — это сопоставление, связывающее каждую свободную переменную функции (переменные, которые используются локально, но определены в охватывающей области) со значением или ссылкой , к которой было привязано имя при создании замыкания. [б] В отличие от простой функции, замыкание позволяет функции получать доступ к этим захваченным переменным через копии их значений или ссылок, даже если функция вызывается вне их области действия.
История и этимология [ править ]
Концепция замыканий была разработана в 1960-х годах для механической оценки выражений в λ-исчислении и впервые была полностью реализована в 1970 году как языковая функция языка программирования PAL для поддержки первоклассных функций с лексической областью видимости . [2]
Питер Ландин определил термин «замыкание» в 1964 году как наличие части среды и части управления , используемых его машиной SECD для оценки выражений. [3] Джоэл Мозес приписывает Ландину введение термина « закрытие» для обозначения лямбда-выражения с открытыми привязками (свободными переменными), которые были закрыты (или связаны) лексической средой, что привело к закрытому выражению или замыканию. [4] [5] Это использование впоследствии было принято Сассманом и Стилом , когда они определили схему в 1975 году: [6] вариант Lisp с лексической областью действия и получил широкое распространение.
Сассман и Абельсон также использовали термин «замыкание» в 1980-х годах со вторым, несвязанным значением: свойство оператора, который добавляет данные в структуру данных , также иметь возможность добавлять вложенные структуры данных. Такое использование термина происходит от использования математики , а не от предыдущего использования в информатике. Такое совпадение в терминологии авторы считают «неудачным». [7]
Анонимные функции [ править ]
Термин «замыкание» часто используется как синоним анонимной функции , хотя, строго говоря, анонимная функция — это литерал функции без имени, тогда как замыкание — это экземпляр функции, значение , чьи нелокальные переменные были привязаны либо к значения или места хранения (в зависимости от языка; см. раздел «Лексическое окружение» ниже).
Например, в следующем коде Python :
def f ( x ):
def g ( y ):
return x + y
return g # Возвращает замыкание.
def h ( x ):
return лямбда y : x + y # Вернуть замыкание.
# Назначение конкретных замыканий переменным.
a = f ( 1 )
b = h ( 1 )
# Использование замыканий, хранящихся в переменных.
Assert a ( 5 ) == 6
Assert b ( 5 ) == 6
# Использование замыканий без предварительной привязки их к переменным.
утверждать f ( 1 )( 5 ) == 6 # f(1) — замыкание.
утверждать, что h ( 1 )( 5 ) == 6 # h(1) — замыкание.
ценности a
и b
являются замыканиями, в обоих случаях создаваемыми путем возврата вложенной функции со свободной переменной из включающей функции, так что свободная переменная привязывается к значению параметра x
объемлющей функции. Замыкания в a
и b
функционально идентичны. Единственное отличие в реализации состоит в том, что в первом случае мы использовали вложенную функцию с именем g
, а во втором случае мы использовали анонимную вложенную функцию (используя ключевое слово Python lambda
для создания анонимной функции). Исходное имя, если таковое имеется, использованное при их определении, не имеет значения.
Замыкание — это такое же значение, как и любое другое значение. Его не обязательно присваивать переменной, его можно использовать напрямую, как показано в последних двух строках примера. Такое использование можно считать «анонимным закрытием».
Определения вложенных функций сами по себе не являются замыканиями: у них есть свободная переменная, которая еще не связана. Только после того, как включающая функция оценена со значением параметра, свободная переменная вложенной функции привязывается, создавая замыкание, которое затем возвращается из включающей функции.
Наконец, замыкание отличается от функции со свободными переменными только тогда, когда оно находится вне области действия нелокальных переменных, в противном случае среда определения и среда выполнения совпадают, и их нечего различать (статическую и динамическую привязку нельзя различить, поскольку имена разрешаются к одним и тем же значениям). Например, в приведенной ниже программе функции со свободной переменной x
(привязано к нелокальной переменной x
с глобальной областью действия) выполняются в той же среде, где x
определено, поэтому не имеет значения, действительно ли это замыкания:
x = 1
nums = [ 1 , 2 , 3 ]
def f ( y ):
вернуть x + y
map ( f , nums )
map ( лямбда y : x + y , nums )
Чаще всего это достигается путем возврата функции, поскольку функция должна быть определена в области действия нелокальных переменных, и в этом случае ее собственная область действия обычно будет меньше.
Этого также можно достичь с помощью затенения переменных (что уменьшает область действия нелокальной переменной), хотя на практике это менее распространено, поскольку оно менее полезно и затенение не рекомендуется. В этом примере f
можно рассматривать как закрытие, потому что x
в теле f
привязан к x
в глобальном пространстве имен, а не в x
местный для g
:
x = 0
def f ( y ):
return x + y
def g ( z ):
x = 1 # локальный x затеняет глобальный x
return f ( z )
g ( 1 ) # оценивается как 1, а не 2
Приложения [ править ]
Использование замыканий связано с языками, где функции являются объектами первого класса , в которых функции могут быть возвращены как результаты функций более высокого порядка или переданы в качестве аргументов для вызовов других функций; если функции со свободными переменными являются первоклассными, то возврат одной из них создает замыкание. Сюда входят языки функционального программирования , такие как Lisp и ML , а также многие современные мультипарадигмальные языки, такие как Julia , Python и Rust . Замыкания также часто используются с обратными вызовами , особенно для обработчиков событий , например, в JavaScript , где они используются для взаимодействия с динамической веб-страницей .
Замыкания также можно использовать в стиле передачи продолжения, чтобы скрыть состояние . Таким образом, такие конструкции, как объекты и структуры управления , можно реализовать с помощью замыканий. В некоторых языках замыкание может произойти, когда функция определена внутри другой функции, а внутренняя функция ссылается на локальные переменные внешней функции. Во время выполнения , когда выполняется внешняя функция, формируется замыкание, состоящее из кода внутренней функции и ссылок (upvalues) на любые переменные внешней функции, требуемые замыканием.
Первоклассные функции [ править ]
Замыкания обычно появляются в языках с функциями первого класса — другими словами, такие языки позволяют передавать функции в качестве аргументов, возвращать их из вызовов функций, привязывать к именам переменных и т. д., точно так же, как и более простые типы, такие как строки и целые числа. Например, рассмотрим следующую функцию Scheme :
; Верните список всех книг, проданных хотя бы ПОРОГОВЫХ экземпляров.
( определить ( самых продаваемых книг порог )
( фильтр
( лямбда ( книга )
( >= ( книга продаж книг )) порог ))
список книг ))
В этом примере лямбда-выражение (lambda (book) (>= (book-sales book) threshold))
появляется внутри функции best-selling-books
. Когда вычисляется лямбда-выражение, Scheme создает замыкание, состоящее из кода лямбда-выражения и ссылки на threshold
переменная, которая является свободной переменной внутри лямбда-выражения.
Закрытие затем передается в filter
функция, которая вызывает ее несколько раз, чтобы определить, какие книги следует добавить в список результатов, а какие следует удалить. Поскольку замыкание имеет ссылку на threshold
, он может использовать эту переменную каждый раз filter
называет это. Функция filter
может быть определен в отдельном файле.
Вот тот же пример, переписанный на JavaScript , другом популярном языке с поддержкой замыканий:
// Возвращаем список всех книг, проданных хотя бы «пороговых» копий.
функция bestSellingBooks ( порог ) {
return bookList . фильтр ( книга => книга . продажи >= порог );
}
Оператор стрелки =>
используется для определения выражения стрелочной функции , а Array.filter
метод [8] вместо глобального filter
функция, но в остальном структура и эффект кода такие же.
Функция может создать замыкание и вернуть его, как в этом примере:
// Возвращаем функцию, которая аппроксимирует производную f
// с использованием интервала dx, который должен быть соответствующим образом малым.
функции производная ( f , dx ) {
return x => ( f ( x + dx ) - f ( x )) / dx ;
}
Поскольку замыкание в этом случае переживает выполнение создавшей его функции, переменные f
и dx
жить дальше после функции derivative
возвращает, даже если выполнение вышло из области действия и они больше не видны. В языках без замыканий время жизни автоматической локальной переменной совпадает с выполнением фрейма стека, в котором эта переменная объявлена. В языках с замыканиями переменные должны продолжать существовать до тех пор, пока любые существующие замыкания имеют на них ссылки. Чаще всего это реализуется с помощью той или иной формы сборки мусора .
Государственное представительство [ править ]
Замыкание можно использовать для связи функции с набором « частных » переменных, которые сохраняются при нескольких вызовах функции. Область действия переменной охватывает только закрытую функцию, поэтому к ней нельзя получить доступ из другого программного кода. Они аналогичны частным переменным в объектно-ориентированном программировании , и фактически замыкания подобны объектам функций с состоянием (или функторам) с одним методом оператора вызова.
Таким образом, в языках с сохранением состояния замыкания могут использоваться для реализации парадигм представления состояния и сокрытия информации , поскольку upvalue замыкания (его закрытые переменные) имеют неопределенный размер , поэтому значение, установленное в одном вызове, остается доступным в следующем. Замыкания, используемые таким образом, больше не имеют ссылочной прозрачности и, таким образом, больше не являются чистыми функциями ; тем не менее, они обычно используются в нечистых функциональных языках, таких как Scheme .
Другое использование [ править ]
Замыкания имеют множество применений:
- Поскольку замыкания задерживают вычисление (т. е. они ничего не «делают» до тех пор, пока их не вызовут), их можно использовать для определения управляющих структур. Например, все стандартные структуры управления Smalltalk , включая ветви (if/then/else) и циклы ( while и for ), определяются с использованием объектов, методы которых допускают замыкания. Пользователи также могут легко определять свои собственные структуры управления.
- В языках, реализующих присваивание, можно создавать несколько функций, расположенных близко к одной и той же среде, что позволяет им взаимодействовать конфиденциально, изменяя эту среду. В схеме:
( define foo #f )
( define bar #f )
( let (( secret-message "none" ))
( set! foo ( lambda ( msg ) ( set! secret-message msg )))
( set! bar ( лямбда ( ) секретное сообщение )))
( дисплей ( бар ) ) ; печатает «none»
( newline )
( foo «встретимся у доков в полночь» )
( display ( bar )) ; печатает «встретимся в доках в полночь»
Примечание. Некоторые ораторы называют любую структуру данных, которая связывает лексическое окружение, замыканием, но этот термин обычно относится конкретно к функциям.
Реализация и теория [ править ]
Замыкания обычно реализуются с помощью специальной структуры данных , которая содержит указатель на код функции , а также представление лексического окружения функции (т. е. набора доступных переменных) на момент создания замыкания. Ссылающаяся среда связывает нелокальные имена с соответствующими переменными в лексической среде во время создания замыкания, дополнительно продлевая их срок службы, по крайней мере, до времени существования замыкания. Когда замыкание вводится позже, возможно, в другом лексическом окружении, функция выполняется с нелокальными переменными, ссылающимися на те, которые были захвачены замыканием, а не на текущую среду.
Реализация языка не может легко поддерживать полные замыкания, если ее модель памяти времени выполнения распределяет все автоматические переменные в линейном стеке . В таких языках автоматические локальные переменные функции освобождаются при завершении функции. Однако замыкание требует, чтобы свободные переменные, на которые оно ссылается, пережили выполнение включающей функции. Следовательно, эти переменные должны быть выделены так, чтобы они сохранялись до тех пор, пока они больше не понадобятся, обычно через распределение в куче , а не в стеке, и их время жизни должно управляться так, чтобы они сохранялись до тех пор, пока все замыкания, ссылающиеся на них, больше не будут использоваться.
Это объясняет, почему обычно языки, которые изначально поддерживают замыкания, также используют сбор мусора . Альтернативой является ручное управление памятью нелокальных переменных (явное выделение памяти в куче и освобождение по завершении) или, при использовании выделения стека, принятие языком того, что определенные варианты использования приведут к неопределенному поведению из-за висячих указателей на освобождены автоматические переменные, как в лямбда-выражениях в C++11. [10] или вложенные функции в GNU C. [11] ( Проблема funarg или проблема «функционального аргумента») описывает сложность реализации функций как объектов первого класса в языке программирования на основе стека, таком как C или C++. Аналогично в версии D 1 предполагается, что программист знает, что делать с делегатами и автоматическими локальными переменными, поскольку их ссылки будут недействительны после возврата из области определения (автоматические локальные переменные находятся в стеке) – это по-прежнему позволяет использовать много полезных функциональные шаблоны, но для сложных случаев требуется явное выделение кучи для переменных. Версия D 2 решила эту проблему, определяя, какие переменные должны храниться в куче, и выполняла автоматическое распределение. Поскольку D использует сбор мусора, в обеих версиях нет необходимости отслеживать использование переменных по мере их передачи.
В строгих функциональных языках с неизменяемыми данными ( например, Erlang ) очень легко реализовать автоматическое управление памятью (сборку мусора), поскольку в ссылках на переменные невозможны циклы. Например, в Erlang все аргументы и переменные размещаются в куче, но ссылки на них дополнительно хранятся в стеке. После возврата функции ссылки по-прежнему действительны. Очистка кучи выполняется инкрементальным сборщиком мусора.
В ML локальные переменные имеют лексическую область видимости и, следовательно, определяют стековую модель, но поскольку они привязаны к значениям, а не к объектам, реализация может свободно копировать эти значения в структуру данных замыкания способом, который невидим для пользователя. программист.
Scheme , который имеет ALGOL -подобную систему лексической области видимости с динамическими переменными и сборкой мусора, не имеет модели стекового программирования и не страдает от ограничений языков, основанных на стеке. Замыкания естественным образом выражаются в Scheme. Лямбда-форма заключает в себе код, а свободные переменные ее среды сохраняются в программе до тех пор, пока к ним возможен доступ, и поэтому их можно использовать так же свободно, как и любое другое выражение схемы. [ нужна цитата ]
Замыкания тесно связаны с Актерами в модели параллельных вычислений Actor , где значения в лексическом окружении функции называются знакомыми . Важным вопросом для замыканий в параллельных языках программирования является возможность обновления переменных в замыкании и, если да, то как эти обновления можно синхронизировать. Актеры предлагают одно решение. [12]
Замыкания тесно связаны с функциональными объектами ; переход от первого ко второму известен как дефункционализация или лямбда-лифтинг ; см. также преобразование замыкания . [ нужна цитата ]
Различия в семантике [ править ]
Лексическое окружение [ править ]
Поскольку в разных языках не всегда имеется общее определение лексической среды, их определения замыкания также могут различаться. Общепринятое минималистское определение лексического окружения определяет его как набор всех привязок переменных в области видимости, и это также то, что замыкания в любом языке должны фиксировать. Однако значение привязки переменной также различается. В императивных языках переменные привязываются к относительным местам в памяти, в которых могут храниться значения. Хотя относительное расположение привязки не меняется во время выполнения, значение в привязанном местоположении может измениться. В таких языках, поскольку замыкание фиксирует привязку, любая операция над переменной, независимо от того, выполняется ли оно из замыкания или нет, выполняется в одной и той же относительной ячейке памяти. Это часто называют захватом переменной «по ссылке». Вот пример, иллюстрирующий концепцию ECMAScript , который является одним из таких языков:
// Javascript
var f , g ;
функция foo () {
var x ;
е = функция () { возвращение ++ х ; };
г = функция () { возвращение -- х ; };
х = 1 ;
alert ( 'внутри foo, вызовите f(): ' + f ());
}
Фу (); // 2
оповещения ( 'вызов g(): ' + g ()); // 1 (--x)
оповещение ( 'вызов g(): ' + g ()); // 0 (--x)
alert ( 'вызов f(): ' + f ()); // 1 (++x)
оповещение ( 'вызов f(): ' + f ()); // 2 (++x)
Функция foo
и замыкания, на которые ссылаются переменные f
и g
все используют одну и ту же относительную ячейку памяти, обозначенную локальной переменной x
.
В некоторых случаях описанное выше поведение может быть нежелательным, и необходимо связать другое лексическое замыкание. Опять же, в ECMAScript это можно сделать с помощью Function.bind()
.
Пример 1: Ссылка на несвязанную переменную [ править ]
var модуль = {
x : 42 ,
getX : function () { верните это . Икс ; }
}
вар unboundGetX = модуль . получитьХ ;
консоль . журнал ( unboundGetX ()); // Функция вызывается в глобальной области
// выдает неопределенное значение, поскольку 'x' не указан в глобальной области.
варboundGetX = unboundGetX . привязать ( модуль ); закрытия
// указываем объектный модуль в качестве консоли . журнал ( boundGetX ()); // выдает 42
Пример 2: Случайная ссылка на связанную переменную [ править ]
В этом примере ожидаемое поведение будет заключаться в том, что каждая ссылка должна выдавать свой идентификатор при нажатии; но поскольку переменная «e» привязана к указанной выше области и лениво оценивается при щелчке, на самом деле происходит то, что каждое событие щелчка выдает идентификатор последнего элемента в «элементах», привязанный в конце цикла for. [14]
вар элементы = документ . getElementsByTagName ( 'a' );
// Неверно: e привязано к функции, содержащей цикл for, а не к замыканию "дескриптора"
for ( var e of elements ) {
e . onclick = функции дескриптор () {
alert ( e . id );
}
}
Опять здесь переменная e
должно быть связано с областью действия блока, используя handle.bind(this)
или let
ключевое слово.
С другой стороны, многие функциональные языки, такие как ML , привязывают переменные непосредственно к значениям. В этом случае, поскольку нет возможности изменить значение переменной после ее привязки, нет необходимости разделять состояние между замыканиями — они просто используют одни и те же значения. Это часто называют захватом переменной «по значению». Локальные и анонимные классы Java также попадают в эту категорию — они требуют, чтобы захваченные локальные переменные были final
, что также означает, что нет необходимости делиться состоянием.
Некоторые языки позволяют выбирать между сохранением значения переменной или ее местоположения. Например, в C++11 захваченные переменные либо объявляются с помощью [&]
, что означает захват по ссылке или с помощью [=]
, что означает захват по значению.
Еще одно подмножество — ленивые функциональные языки, такие как Haskell , привязывают переменные к результатам будущих вычислений, а не к значениям. Рассмотрим этот пример в Haskell:
-- Haskell
foo :: Дробное a => a -> a -> ( a -> a )
foo x y = ( \ z -> z + r )
где r = x / y
f :: Дробное a => a - > a
f = foo 1 0
main = print ( f 123 )
Связывание r
захватывается замыканием, определенным внутри функции foo
относится к вычислениям (x / y)
— что в данном случае приводит к делению на ноль. Однако, поскольку фиксируется именно вычисление, а не значение, ошибка проявляется только при вызове замыкания, а затем при попытке использовать захваченную привязку.
Закрытие, уход [ править ]
Еще больше различий проявляется в поведении других конструкций с лексической областью видимости, таких как return
, break
и continue
заявления. Такие конструкции, в общем, можно рассматривать с точки зрения вызова escape-продолжения , установленного включающим оператором управления (в случае break
и continue
, такая интерпретация требует, чтобы циклические конструкции рассматривались с точки зрения рекурсивных вызовов функций). В некоторых языках, например ECMAScript, return
относится к продолжению, установленному замыканием, лексически наиболее внутренним по отношению к утверждению - таким образом, return
внутри замыкания передает управление вызвавшему его коду. Однако в Smalltalk внешне похожий оператор ^
вызывает escape-продолжение, установленное для вызова метода, игнорируя escape-продолжения любых промежуточных вложенных замыканий. Продолжение escape конкретного замыкания может быть вызвано в Smalltalk только неявно, достигнув конца кода замыкания. Эти примеры в ECMAScript и Smalltalk подчеркивают разницу:
"Светская беседа"
фу
| хз |
хз := #( 1 2 3 4 ) .
хз делаю: [ : х | ^ х ] .
^ 0
бар
Показ расшифровки : ( self foo printString ) "печатает 1"
ECMAScript
// Функция foo () {
var xs = [ 1 , 2 , 3 , 4 ];
хз . forEach ( функция ( х ) { возврат х ; });
вернуть 0 ;
}
Оповещение ( фу ()); // печатает 0
Приведенные выше фрагменты кода будут вести себя по-другому, поскольку Smalltalk ^
оператор и JavaScript return
оператор не является аналогом. В примере ECMAScript return x
покинет внутреннее замыкание, чтобы начать новую итерацию forEach
цикл, тогда как в примере Smalltalk ^x
прервет цикл и вернется из метода foo
.
Common Lisp предоставляет конструкцию, которая может выражать любое из вышеперечисленных действий: Lisp (return-from foo x)
ведет себя как Smalltalk ^x
, а Лисп (return-from nil x)
ведет себя как JavaScript return x
. Следовательно, Smalltalk позволяет захваченному escape-продолжению пережить тот период, в котором оно может быть успешно вызвано. Учитывать:
«Светская беседа»
foo
^ [ : x | ^ х ]
бар
| ж |
f := сам фу .
f значение : 123 «ошибка!»
Когда замыкание возвращается методом foo
вызывается, он пытается вернуть значение из вызова foo
это создало замыкание. Поскольку этот вызов уже вернулся, а модель вызова метода Smalltalk не соответствует дисциплине стека спагетти для облегчения множественных возвратов, эта операция приводит к ошибке.
Некоторые языки, такие как Ruby , позволяют программисту выбирать путь return
захвачен. Пример в Ruby:
# Ruby
# Замыкание с использованием Proc
def foo
f = Proc . new { return «возврат из foo изнутри процедуры» }
f . call # управление оставляет здесь foo
return "return from foo"
end
# Замыкание с использованием лямбды
def bar
f = лямбда { return "return from лямбда" }
f . call # управление здесь не покидает бар
return "возврат из бара"
end
puts foo # печатает "возврат из foo изнутри процедуры"
puts bar # печатает "возврат из бара"
Оба Proc.new
и lambda
в этом примере представлены способы создания замыкания, но семантика созданных таким образом замыканий различна по отношению к return
заявление.
В схеме дано определение и объем return
оператор управления является явным (и только для примера условно назван «return»). Ниже приведен прямой перевод примера Ruby.
; Схема
( define call/cc call-with-current-continuation )
( define ( foo )
( call/cc
( лямбда ( return )
( define ( f ) ( return "возврат из foo изнутри proc" ))
( f ) ; control оставляет здесь foo
( return "return from foo" ))))
( define ( bar )
( call/cc
( лямбда ( return )
( define ( f ) ( call/cc ( лямбда ( return ) ( return " return from лямбда " ) )))
( f ) ; здесь управление не покидает бар
( return "возврат из бара" ))))
( display ( foo ) ) ; печатает «возврат из foo изнутри proc»
( новая строка )
( display ( bar )) ; печатает «возврат из бара»
Замыкающие конструкции [ править ]
В некоторых языках есть функции, имитирующие поведение замыканий. В таких языках, как C++ , C# , D , Java , Objective-C и Visual Basic (.NET) (VB.NET), эти функции являются результатом объектно-ориентированной парадигмы языка.
Обратные вызовы (C) [ править ]
Некоторые библиотеки C поддерживают обратные вызовы . Иногда это реализуется путем предоставления двух значений при регистрации обратного вызова в библиотеке: указатель на функцию и отдельный void*
указатель на произвольные данные по выбору пользователя. Когда библиотека выполняет функцию обратного вызова, она передает указатель данных. Это позволяет обратному вызову сохранять состояние и ссылаться на информацию, полученную на момент его регистрации в библиотеке. Эта идиома похожа на замыкания по функциональности, но не по синтаксису. void*
Указатель не является типобезопасным, поэтому эта идиома C отличается от типобезопасных замыканий в C#, Haskell или ML.
Обратные вызовы широко используются в графического пользовательского интерфейса (GUI) наборах инструментов виджетов для реализации событийно-ориентированного программирования путем связывания общих функций графических виджетов (меню, кнопки, флажки, ползунки, счетчики и т. д.) с функциями, специфичными для приложения, реализующими конкретные желаемые действия. поведение приложения.
Вложенная функция и указатель функции (C) [ править ]
С расширением GNU Compiler Collection (GCC) вложенная функция [15] можно использовать, а указатель функции может эмулировать замыкания при условии, что функция не выходит из содержащейся области. Следующий пример неверен, потому что adder
является определением верхнего уровня (в зависимости от версии компилятора оно может дать правильный результат, если скомпилировано без оптимизации, т. е. при -O0
):
#include <stdio.h>
typedef int ( * fn_int_to_int )( int ); // тип функции int->int
fn_int_to_int adder ( int number ) {
int add ( int value ) { возвращаемое значение + число ; }
Вернуться и добавить ; // Оператор & здесь необязателен, поскольку имя функции в C — это указатель, указывающий на саму себя
}
int main ( void ) {
fn_int_to_int add10 = adder ( 10 );
printf ( "%d \n " , add10 ( 1 ));
вернуть 0 ;
}
Но переезд adder
(и, по желанию, typedef
) в main
делает его действительным:
#include <stdio.h>
int main ( void ) {
typedef int ( * fn_int_to_int )( int ); // тип функции int->int
fn_int_to_int adder ( int number ) {
int add ( int value ) { возвращаемое значение + число ; }
Вернуться добавить ;
}
fn_int_to_int add10 = сумматор ( 10 );
printf ( "%d \n " , add10 ( 1 ));
вернуть 0 ;
}
Если это выполнено, теперь печатается 11
как и ожидалось.
Локальные классы и лямбда-функции (Java) [ править ]
Java позволяет классы определять внутри методов . Они называются локальными классами . Если такие классы не названы, они называются анонимными классами (или анонимными внутренними классами). Локальный класс (именованный или анонимный) может ссылаться на имена в лексически включающих классах или на переменные, доступные только для чтения (помеченные как final
) в лексически включающем методе.
класс CalculationWindow расширяет JFrame {
частный изменчивый int результат ;
// ...
public void CalcultInSeparateThread ( final URI uri ) {
// Выражение "new Runnable() { ... }" — это анонимный класс, реализующий интерфейс Runnable.
new Thread (
new Runnable () {
void run () {
// Он может читать конечные локальные переменные:
Calculate ( uri );
// Он может получить доступ к частным полям включающего класса:
result = result + 10 ;
}
}
). начинать ();
}
}
Захват final
переменные позволяют захватывать переменные по значению. Даже если переменная для захвата не является final
, его всегда можно скопировать во временный final
переменная непосредственно перед классом.
Захват переменных по ссылке можно эмулировать с помощью final
ссылка на изменяемый контейнер, например, одноэлементный массив. Локальный класс не сможет изменить значение ссылки на контейнер, но сможет изменить содержимое контейнера.
С появлением лямбда-выражений Java 8, [16] закрытие приводит к выполнению приведенного выше кода как:
класс CalculationWindow расширяет JFrame {
частный изменчивый int результат ;
// ...
public void CalculateInSeparateThread ( final URI uri ) {
// Код () -> { /* code */ } является замыканием.
новый поток (() -> {
вычислить ( uri );
результат = результат + 10 ;
}). начинать ();
}
}
Локальные классы — это один из типов внутренних классов , которые объявляются в теле метода. Java также поддерживает внутренние классы, объявленные как нестатические члены включающего класса. [17] Их обычно называют просто «внутренними классами». [18] Они определены в теле включающего класса и имеют полный доступ к переменным экземпляра включающего класса. Из-за их привязки к этим переменным экземпляра экземпляр внутреннего класса может быть создан только с явной привязкой к экземпляру включающего класса с использованием специального синтаксиса. [19]
public class EnclosingClass {
/* Определить внутренний класс */
public class InnerClass {
public int ignoreAndReturnCounter () {
return counter ++ ;
}
}
частный int счетчик ;
{
счетчик = 0 ;
}
Public int getCounter () {
возврат счетчика ;
}
Public static void main ( String [] args ) {
EnclosingClass enclosingClassInstance = новый EnclosingClass ();
/* Создать экземпляр внутреннего класса с привязкой к экземпляру */
EnclosingClass . InnerClass InternalClassInstance =
EnclosingClassInstance . новый ВнутреннийКласс ();
for ( int i = enclosingClassInstance . getCounter ();
( i = InternalClassInstance . инкрементАнкретурнкаунтер ()) < 10 ;
/* шаг приращения опущен */ ) {
System . вне . печатьln ( я );
}
}
}
При выполнении будут выведены целые числа от 0 до 9. Будьте осторожны, чтобы не путать этот тип класса с вложенным классом, который объявляется таким же образом с сопутствующим использованием модификатора «static»; они не дают желаемого эффекта, а представляют собой просто классы без специальной привязки, определенной во включающем классе.
Начиная с Java 8 , Java поддерживает функции как объекты первого класса. Лямбда-выражения такого вида считаются типа Function<T,U>
где T — это домен, а U — тип изображения. Выражение можно вызвать с помощью его .apply(T t)
метод, но не с помощью стандартного вызова метода.
public static void main ( String [] args ) {
Function < String , Integer > length = s -> s . длина ();
Система . вне . println ( length . apply ( "Привет, мир!" ) ); // Напечатаем 13.
}
Блоки (C, C++, Objective-C 2.0) [ править ]
Apple представила блоки , форму замыкания, как нестандартное расширение в C , C++ , Objective-C 2.0 и в Mac OS X 10.6 «Snow Leopard» и iOS 4.0 . Apple сделала свою реализацию доступной для компиляторов GCC и clang.
Указатели на блоки и блочные литералы отмечены значком ^
. Обычные локальные переменные фиксируются по значению при создании блока и доступны только для чтения внутри блока. Переменные, которые необходимо захватить по ссылке, отмечены значком __block
. Блоки, которые должны сохраняться за пределами области, в которой они созданы, возможно, придется скопировать. [20] [21]
typedef int ( ^ IntBlock )();
IntBlock downCounter ( int start ) {
__block int i = start ;
return [[ ^ int () {
return i -- ;
} копировать ] автовыпуск ];
}
IntBlock f = downCounter ( 5 );
NSLog ( @"%d" , f ());
NSLog ( @"%d" , f ());
NSLog ( @"%d" , f ());
Делегаты (C#, VB.NET, D) [ править ]
Анонимные методы C# и лямбда-выражения поддерживают замыкание:
вар данные = новый [] { 1 , 2 , 3 , 4 };
вар множитель = 2 ;
вар результат = данные . Выберите ( x => x * множитель );
Visual Basic .NET , который имеет множество языковых функций, аналогичных функциям C#, также поддерживает лямбда-выражения с замыканиями:
Тусклые данные = { 1 , 2 , 3 , 4 }
Тусклый множитель = 2
Тусклый результат = данные . Выберите ( Функция ( x ) x * множитель )
В D замыкания реализуются делегатами, указателем функции в паре с указателем контекста (например, экземпляром класса или кадром стека в куче в случае замыканий).
автоматический тест1 () {
int a = 7 ;
вернуть делегата ) { вернуть + 3 ; ( }; // конструкция анонимного делегата
}
auto test2 () {
int a = 20 ;
int foo () { return a + 5 ; } // внутренняя функция
return & foo ; // другой способ создания делегата
}
void bar () {
auto dg = test1 ();
дг (); // =10 // ок, test1.a находится в замыкании и все еще существует
dg = test2 ();
дг (); // =25 // ок, test2.a закрыт и все еще существует
}
Версия D 1 имеет ограниченную поддержку замыканий. Например, приведенный выше код не будет работать корректно, поскольку переменная a находится в стеке, и после возврата из test() ее больше нельзя использовать (вероятнее всего, вызов foo через dg() вернет ' случайное целое число). Эту проблему можно решить, явно выделив переменную «a» в куче или используя структуры или классы для хранения всех необходимых закрытых переменных и создав делегат из метода, реализующего тот же код. Замыкания можно передавать другим функциям, если они используются только тогда, когда значения, на которые они ссылаются, действительны (например, вызов другой функции с замыканием в качестве параметра обратного вызова) и полезны для написания общего кода обработки данных, поэтому это ограничение На практике зачастую это не является проблемой.
Это ограничение было исправлено в версии D 2 — переменная «a» будет автоматически выделена в куче, поскольку она используется во внутренней функции, и делегат этой функции может выйти из текущей области видимости (через присвоение dg или возврат). Любые другие локальные переменные (или аргументы), на которые не ссылаются делегаты или на которые ссылаются только делегаты, не выходящие за пределы текущей области, остаются в стеке, что проще и быстрее, чем выделение кучи. То же самое верно и для методов класса Internal, которые ссылаются на переменные функции.
Функциональные объекты (C++) [ править ]
C++ позволяет определять объекты функций путем перегрузки. operator()
. Эти объекты ведут себя как функции функционального языка программирования. Они могут создаваться во время выполнения и могут содержать состояние, но они не фиксируют локальные переменные неявно, как это делают замыкания. Начиная с версии 2011 года , язык C++ также поддерживает замыкания, которые представляют собой тип функционального объекта, автоматически создаваемого из специальной языковой конструкции, называемой лямбда-выражением . Замыкание C++ может захватывать свой контекст либо путем сохранения копий переменных, к которым осуществляется доступ, как членов объекта замыкания, либо по ссылке. В последнем случае, если объект замыкания выходит за пределы объекта, на который указывает ссылка, вызывая его operator()
вызывает неопределенное поведение, поскольку замыкания C++ не продлевают время жизни их контекста.
void foo ( string myname ) {
int y ;
вектор < строка > n ;
// ...
auto i = std :: find_if ( n.begin { n.end (), ( [ // ),
это лямбда-выражение:
& ] ( const string & s ) = return s ! myname && s . размер () > y } )
;
// 'i' теперь является либо 'n.end()', либо указывает на первую строку в 'n'
// которая не равна 'myname' и длина которой больше 'y'
}
Встроенные агенты (Eiffel) [ править ]
Eiffel включает в себя встроенные агенты, определяющие замыкания. Встроенный агент — это объект, представляющий процедуру, определяемый путем указания встроенного кода процедуры. Например, в
ок_кнопка . клик_событие . подписаться (
агент ( x , y : INTEGER ) сделать
карту . Country_at_coordinates ( x , y ). Показать
конец
)
аргумент в пользу subscribe
— агент, представляющий процедуру с двумя аргументами; процедура находит страну по соответствующим координатам и отображает ее. Весь агент «подписан» на тип события. click_event
для
определенную кнопку, так что всякий раз, когда на этой кнопке возникает экземпляр типа события (потому что пользователь нажал кнопку), процедура будет выполняться с координатами мыши, передаваемыми в качестве аргументов для x
и y
.
Основное ограничение агентов Eiffel, которое отличает их от замыканий в других языках, заключается в том, что они не могут ссылаться на локальные переменные из охватывающей области. Это проектное решение помогает избежать двусмысленности при разговоре о значении локальной переменной в замыкании — должно ли это быть последнее значение переменной или значение, полученное при создании агента? Только Current
(ссылка на текущий объект, аналог this
в Java), его функции и аргументы агента доступны из тела агента. Значения внешних локальных переменных можно передать, предоставив агенту дополнительные закрытые операнды.
C++Builder __closure зарезервированное слово [ править ]
Embarcadero C++Builder предоставляет зарезервированное слово. __closure
чтобы предоставить указатель на метод с синтаксисом, аналогичным указателю на функцию. [22]
Стандарт C позволяет писать typedef для указателя на тип функции , используя следующий синтаксис:
typedef void ( * TMyFunctionPointer )( void );
Подобным образом, typedef можно объявить для указателя на метод, используя следующий синтаксис:
typedef void ( __closure * TMyMethodPointer )();
См. также [ править ]
Примечания [ править ]
- ^ Функция может храниться как ссылка на функцию, например указатель на функцию .
- ^ Эти имена обычно относятся к значениям, изменяемым переменным или функциям, но также могут быть другими объектами, такими как константы, типы, классы или метки.
Ссылки [ править ]
- ^ Сассман и Стил. «Схема: интерпретатор расширенного лямбда-исчисления». «... структура данных, содержащая лямбда-выражение, и среда, которая будет использоваться, когда это лямбда-выражение применяется к аргументам». ( Викиисточник )
- ^ Тернер, Дэвид А. (2012). «Некоторые истории языков функционального программирования» (PDF) . Международный симпозиум по тенденциям функционального программирования . Конспекты лекций по информатике. Том. 7829. Спрингер. С. 1–20 Утверждение о М-выражениях см. в § 12, § 2, примечание 8. дои : 10.1007/978-3-642-40447-4_1 . ISBN 978-3-642-40447-4 .
- ^ Ландин, П.Дж. (январь 1964 г.). «Механическая оценка выражений» (PDF) . Компьютерный журнал . 6 (4): 308–320. дои : 10.1093/comjnl/6.4.308 .
- ^
Моисей, Джоэл (июнь 1970 г.). «Функция FUNCTION в LISP, или Почему проблему FUNARG следует называть проблемой среды». Бюллетень ACM Sigsam (15): 13–27. дои : 10.1145/1093410.1093411 . hdl : 1721.1/5854 . S2CID 17514262 . Памятка AI 199.
Полезной метафорой различия между FUNCTION и QUOTE в LISP является представление QUOTE как пористого или открытого покрытия функции, поскольку свободные переменные уходят в текущую среду. ФУНКЦИЯ действует как закрытое или непористое покрытие (отсюда и термин «закрытие», используемый Ландином). Таким образом, мы говорим об «открытых» лямбда-выражениях (функции в LISP обычно являются лямбда-выражениями) и «закрытых» лямбда-выражениях. [...] Мой интерес к проблеме окружающей среды начался, когда Ландин, глубоко разбиравшийся в этой проблеме, посетил Массачусетский технологический институт в 1966–67 годах. Затем я понял соответствие между списками FUNARG, которые являются результатами оценки «закрытых» лямбда-выражений в LISP и . Lambda Closures ISWIM
- ^ Викстрем, Оке (1987). Функциональное программирование с использованием стандартного ML . Прентис Холл. ISBN 0-13-331968-7 .
Причина, по которой это называется «замыканием», заключается в том, что выражение, содержащее свободные переменные, называется «открытым» выражением, и, связывая с ним привязки его свободных переменных, вы закрываете его.
- ^ Сассман, Джеральд Джей ; Стил, Гай Л. младший (декабрь 1975 г.). Схема: интерпретатор расширенного лямбда-исчисления (отчет). Памятка AI 349.
- ^ Абельсон, Гарольд ; Сассман, Джеральд Джей ; Сассман, Джули (1996). Структура и интерпретация компьютерных программ . МТИ Пресс. стр. 98–99. ISBN 0-262-51087-1 .
- ^ "массив.фильтр" . Центр разработчиков Mozilla . 10 января 2010 г. Проверено 9 февраля 2010 г.
- ^ «Re: FP, OO и отношения. Кто-нибудь превосходит остальных?» . 29 декабря 1999 года. Архивировано из оригинала 26 декабря 2008 года . Проверено 23 декабря 2008 г.
- ^ Комитет по стандартам лямбда-выражений и замыканий C++. 29 февраля 2008 г.
- ^ «6.4 Вложенные функции» . Руководство GCC .
Если вы попытаетесь вызвать вложенную функцию по ее адресу после выхода из содержащей функции, начнется ад. Если вы попытаетесь вызвать его после выхода из содержащего уровня области видимости и если он ссылается на некоторые переменные, которые больше не входят в область видимости, вам может повезти, но рисковать неразумно. Однако если вложенная функция не ссылается ни на что, выходящее за пределы области видимости, вы можете быть в безопасности.
- ^ Основы семантики актеров будут сохраняться. Докторская диссертация по математике в Массачусетском технологическом институте. Июнь 1981 года.
- ^ «Функция.прототип.bind()» . Веб-документы MDN . Проверено 20 ноября 2018 г.
- ^ «Замыкания» . Веб-документы MDN . Проверено 20 ноября 2018 г.
- ^ «Вложенные функции» .
- ^ «Лямбда-выражения» . Учебники по Java .
- ^ «Вложенные, внутренние, члены и классы верхнего уровня» . Блог Oracle Джозефа Д. Дарси . Июль 2007 г. Архивировано из оригинала 31 августа 2016 г.
- ^ «Пример внутреннего класса» . Учебные пособия по Java: Изучение языка Java: классы и объекты .
- ^ «Вложенные классы» . Учебные пособия по Java: Изучение языка Java: классы и объекты .
- ^ «Темы программирования блоков» . Apple Inc., 8 марта 2011 г. Проверено 8 марта 2011 г.
- ^ Бенгтссон, Иоахим (7 июля 2010 г.). «Программирование с использованием блоков C на устройствах Apple» . Архивировано из оригинала 25 октября 2010 года . Проверено 18 сентября 2010 г.
- ^ Полную документацию можно найти по адресу http://docwiki.embarcadero.com/RADStudio/Rio/en/Closure.
Внешние ссылки [ править ]
- Оригинальные «Лямбда-документы» : классическая серия статей Гая Л. Стила-младшего и Джеральда Джея Сассмана, в которых, среди прочего, обсуждается универсальность замыканий в контексте Scheme (где они появляются как лямбда -выражения ).
- Гафтер, Нил (28 января 2007 г.). «Определение замыканий» .
- Браха, Гилад ; Гафтер, Нил; Гослинг, Джеймс ; фон дер Ахе, Питер. «Замыкания для языка программирования Java (v0.5)» .
- Замыкания о замыканиях в динамически типизированных императивных языках : статья Мартина Фаулера .
- Методы закрытия коллекций : пример технической области, в которой удобно использовать замыкания, Мартин Фаулер.