Из Википедии, бесплатной энциклопедии
Перейти к навигации Перейти к поиску

В языках программирования , замыкание , а также лексико замыкание или замыкание функции , представляет собой метод реализаций лексический контекстное имя связывания в языке с функциями первого класса . С функциональной точки зрения замыкание - это запись, хранящая функцию [a] вместе с окружением. [1] Среда - это отображение, связывающее каждую свободную переменную функции (переменные, которые используются локально, но определены во включающей области) со значением или ссылкой.к которому было привязано имя при создании замыкания. [b] В отличие от простой функции, замыкание позволяет функции получить доступ к этим захваченным переменным через копии замыкания их значений или ссылок, даже если функция вызывается вне их области видимости.

История и этимология [ править ]

Концепция замыканий была разработана в 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  lambda  y :  x  +  y  # Возвращает замыкание.# Назначение конкретных замыканий для переменных. а  =  f ( 1 ) b  =  h ( 1 )# Использование замыканий, хранящихся в переменных. assert  a ( 5 )  ==  6 assert  b ( 5 )  ==  6# Использование замыканий без предварительной привязки их к переменным. assert  f ( 1 ) ( 5 )  ==  6  # f (1) - это замыкание. assert  h ( 1 ) ( 5 )  ==  6  # h (1) - это закрытие.

значения aи bявляются замыканиями, в обоих случаях производимые путем возврата вложенной функции со свободной переменной из включающей функции, так что свободная переменная привязывается к значению параметра xвключающей функции. Замки в aи bфункционально идентичны. Единственное отличие в реализации состоит в том, что в первом случае мы использовали вложенную функцию с именем, gа во втором случае мы использовали анонимную вложенную функцию (используя ключевое слово Python lambdaдля создания анонимной функции). Оригинальное название, использованное при их определении, не имеет значения.

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

Определения вложенных функций сами по себе не являются замыканиями: у них есть свободная переменная, которая еще не связана. Только после того, как включающая функция оценивается со значением параметра, свободная переменная вложенной функции привязывается, создавая замыкание, которое затем возвращается из включающей функции.

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

х  =  1 НУМС  =  [ 1 ,  2 ,  3 ]def  f ( y ):  вернуть  x  +  ymap ( f ,  nums ) map ( лямбда  y :  x  +  y ,  nums )

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

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

х  =  0def  f ( y ):  вернуть  x  +  ydef  g ( z ):  x  =  1  # локальные x тени глобальные x  return  f ( z )g ( 1 )  # оценивается как 1, а не 2

Приложения [ править ]

Использование замыканий связано с языками, в которых функции являются объектами первого класса , в которых функции могут быть возвращены как результаты из функций более высокого порядка или переданы как аргументы для других вызовов функций; если функции со свободными переменными являются первоклассными, то возвращение одной создает закрытие. Сюда входят языки функционального программирования, такие как Lisp и ML , а также многие современные языки с несколькими парадигмами, такие как Python и Rust . Замыкания также часто используются с обратными вызовами , особенно для обработчиков событий , например, в JavaScript., где они используются для взаимодействия с динамической веб-страницей .

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

Первоклассные функции [ править ]

Замыкания обычно появляются в языках с функциями первого класса - другими словами, такие языки позволяют функциям передаваться как аргументы, возвращаться из вызовов функций, связываться с именами переменных и т. Д., Точно так же, как более простые типы, такие как строки и целые числа. Например, рассмотрим следующую функцию схемы :

; Верните список всех книг, по крайней мере, с ПОРОГОМ проданных копий. ( определить ( порог самых продаваемых книг  ) ( filter ( lambda ( book ) ( > = ( book-sales book ) threshold )) book-list ))      

В этом примере лямбда-выражение (lambda (book) (>= (book-sales book) threshold)) появляется внутри функции best-selling-books. Когда лямбда-выражение оценивается, Scheme создает замыкание, состоящее из кода для лямбда-выражения и ссылки на thresholdпеременную, которая является свободной переменной внутри лямбда-выражения.

Затем закрытие передается filterфункции, которая вызывает ее несколько раз, чтобы определить, какие книги должны быть добавлены в список результатов, а какие - отброшены. Поскольку само замыкание имеет ссылку на threshold, оно может использовать эту переменную каждый раз при filterего вызове. Сама функция filterможет быть определена в отдельном файле.

Вот тот же пример, переписанный на JavaScript , другом популярном языке с поддержкой замыканий:

// Возвращаем список всех книг с как минимум «пороговым» проданным количеством копий. функция  bestSellingBooks ( порог )  {  return  bookList . filter (  function  ( book )  {  return  book . sales  > =  threshold ;  }  ); }

functionКлючевое слово здесь вместо используется lambda, и Array.filterметод [8] вместо глобальной filterфункции, но в остальном структура и эффект кода являются одинаковыми.

Функция может создать замыкание и вернуть его, как в следующем примере:

// Возвращаем функцию, которая аппроксимирует производную f // с использованием интервала dx, который должен быть соответственно малым. производная функции  ( f , dx ) { return function ( x ) { return ( f ( x + dx ) - f ( x )) / dx ; }; }               

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

Государственное представительство [ править ]

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

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

Другое использование [ править ]

Замыкания имеют много применений:

  • Поскольку замыкания задерживают оценку - т. Е. Они ничего не «делают» до тех пор, пока не будут вызваны, их можно использовать для определения структур управления. Например, все стандартные управляющие структуры Smalltalk , включая ветви (if / then / else) и циклы (while и for), определяются с помощью объектов, методы которых принимают замыкания. Пользователи также могут легко определять свои собственные управляющие структуры.
  • В языках, реализующих назначение, несколько функций могут быть созданы так близко в одной и той же среде, что позволяет им общаться конфиденциально, изменяя эту среду. На схеме:
( определить foo  #f ) ( определить bar  #f )( let (( secret-message  "none" ))  ( set! foo  ( lambda ( msg )  ( set! secret-message  msg )))  ( set! bar  ( lambda ()  secret-message )))( дисплей ( полоса ))  ; выводит «none» ( новая строка ) ( foo  «встретимся у дока в полночь» ) ( display ( bar ))  ; принты "встретим меня у пристани в полночь"
  • Замыкания можно использовать для реализации объектных систем. [9]

Примечание. Некоторые ораторы называют закрытием любую структуру данных, которая связывает лексическое окружение, но этот термин обычно относится именно к функциям.

Реализация и теория [ править ]

Замыкания обычно реализуются с помощью специальной структуры данных, которая содержит указатель на код функции , а также представление лексического окружения функции (т. Е. Набора доступных переменных) на момент создания замыкания. Ссылочная среда связывает нелокальные имена с соответствующими переменными в лексической среде в момент создания замыкания, дополнительно продлевая их время жизни, по крайней мере, до времени жизни самого замыкания. Когда закрытие вводится позже, возможно, с другой лексической средой, функция выполняется со своими нелокальными переменными, относящимися к переменным, захваченным замыканием, а не текущей среде.

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

Это объясняет, почему обычно языки, которые изначально поддерживают замыкания, также используют сборку мусора . Альтернативами являются ручное управление памятью нелокальных переменных (явное выделение в куче и освобождение по завершении) или, если используется выделение стека, чтобы язык признал, что определенные варианты использования приведут к неопределенному поведению из-за висящих указателей на освобожденные автоматические переменные, как в лямбда-выражениях в C ++ 11 [10] или вложенных функциях в GNU C. [11] Проблема funarg (или проблема «функционального аргумента») описывает сложность реализации функций как объектов первого класса в стеке. язык программирования, такой как C или C ++. Аналогично в Dверсии 1 предполагается, что программист знает, что делать с делегатами и автоматическими локальными переменными, поскольку их ссылки будут недействительными после возврата из области определения (автоматические локальные переменные находятся в стеке) - это по-прежнему позволяет использовать множество полезных функциональных шаблонов, но для сложных случаев требуется явное выделение кучи для переменных. D версии 2 решила эту проблему, определив, какие переменные должны храниться в куче, и выполнила автоматическое выделение. Поскольку D использует сборку мусора, в обеих версиях нет необходимости отслеживать использование переменных по мере их передачи.

В строго функциональных языках с неизменяемыми данными ( например, Erlang ) очень легко реализовать автоматическое управление памятью (сборку мусора), поскольку нет возможных циклов в ссылках на переменные. Например, в Erlang все аргументы и переменные размещаются в куче, но ссылки на них дополнительно хранятся в стеке. После возврата из функции ссылки остаются действительными. Очистка кучи выполняется инкрементным сборщиком мусора.

В ML локальные переменные имеют лексическую область видимости и, следовательно, определяют модель, подобную стеку, но поскольку они привязаны к значениям, а не к объектам, реализация может копировать эти значения в структуру данных замыкания невидимым для программист.

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

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

Замыкания тесно связаны с объектами функций ; переход от первого ко второму известен как дефункционализация или лямбда-лифтинг ; см. также преобразование закрытия . [ необходима цитата ]

Различия в семантике [ править ]

Лексическая среда [ править ]

Поскольку разные языки не всегда имеют общее определение лексической среды, их определения замыкания также могут различаться. Общепринятое минималистское определение лексической среды определяет ее как набор всех привязок переменных в области видимости, и это также то, что должны улавливать замыкания в любом языке. Однако значение переменнойпереплет тоже отличается. В императивных языках переменные связываются с относительными участками памяти, в которых могут храниться значения. Хотя относительное расположение привязки не изменяется во время выполнения, значение в привязанном расположении может. В таких языках, поскольку замыкание фиксирует привязку, любая операция с переменной, независимо от того, выполняется ли она из замыкания или нет, выполняется в той же относительной ячейке памяти. Это часто называется захватом переменной «по ссылке». Вот пример, иллюстрирующий концепцию в ECMAScript , который является одним из таких языков:

// ECMAScript var  f ,  g ; функция  foo ()  {  var  x ;  f  =  функция ()  {  return  ++ x ;  };  г  =  функция ()  {  возврат  - х ;  };  х  =  1 ;  alert ( 'внутри foo, вызов f ():'  +  f ()); } foo ();  // 2 alert ( 'вызов g ():'  +  g ()); // 1 (--x) alert ( 'вызов g ():'  +  g ());  // 0 (--x) alert ( 'вызов f ():'  +  f ());  // 1 (++ x) alert ( 'вызов функции f ():'  +  f ());  // 2 (++ x)

Функция fooи замыкания, на которые ссылаются переменные, fи gвсе они используют одну и ту же относительную ячейку памяти, обозначенную локальной переменной x.

В некоторых случаях вышеуказанное поведение может быть нежелательным, и необходимо связать другое лексическое замыкание. Опять же в ECMAScript это можно сделать с помощью Function.bind().

Пример 1. Ссылка на несвязанную переменную [ править ]

[13]

var  module  =  {  x :  42 ,  getX :  function ()  { вернуть  это . х ;  } } var  unboundGetX  =  module . getX ; консоль . журнал ( unboundGetX ());  // Функция вызывается в глобальной области видимости // выдает undefined, поскольку 'x' не указан в глобальной области.var  boundGetX  =  unboundGetX . привязка ( модуль );  // указываем объектный модуль как консоль закрытия . журнал ( boundGetX ());  // испускает 42

Пример 2: Случайная ссылка на связанную переменную [ править ]

В этом примере ожидаемым поведением будет то, что каждая ссылка будет выдавать свой идентификатор при нажатии; но поскольку переменная 'e' привязана к указанной выше области и лениво оценивается при щелчке, на самом деле происходит то, что каждое событие щелчка генерирует идентификатор последнего элемента в 'elements', привязанном в конце цикла for. [14]

var  elements =  document . getElementsByTagName ( 'а' ); // Неправильно: e привязан к функции, содержащей цикл for, а не к закрытию "handle" for  ( var  e  of  elements ) {  e . OnClick = функция  ручка () {  оповещение ( е . идентификатор );}  }

Опять же, здесь переменная eдолжна быть привязана к области действия блока с помощью ключевого слова handle.bind(this)или let.

С другой стороны, многие функциональные языки, такие как ML , напрямую связывают переменные со значениями. В этом случае, поскольку нет возможности изменить значение переменной после ее привязки, нет необходимости разделять состояние между замыканиями - они просто используют одни и те же значения. Это часто называется захватом переменной «по значению». Локальные и анонимные классы Java также попадают в эту категорию - они требуют наличия захваченных локальных переменных final, что также означает, что нет необходимости совместно использовать состояние.

Некоторые языки позволяют вам выбирать между захватом значения переменной или ее местоположением. Например, в C ++ 11 захваченные переменные либо объявляются с помощью [&], что означает захват по ссылке, либо с помощью [=], что означает захват по значению.

Еще одно подмножество, ленивые функциональные языки, такие как Haskell , привязывают переменные к результатам будущих вычислений, а не к значениям. Рассмотрим этот пример на Haskell:

- Haskell foo  ::  Fractional  a  =>  a  ->  a  ->  ( a  ->  a ) foo  x  y  =  ( \ z  ->  z  +  r ),  где  r  =  x  /  yf  ::  дробное  a  =>  a  ->  a f  =  foo  1  0main  =  print  ( f  123 )

Привязка rзахваченного замыканием, определенным в функции, связана fooс вычислением, (x / y)что в данном случае приводит к делению на ноль. Однако, поскольку фиксируется вычисление, а не значение, ошибка проявляется только при вызове замыкания и фактически пытается использовать захваченную привязку.

Закрытие выхода [ править ]

Еще больше различий проявляется в поведении других конструкций с лексической областью видимости, таких как return, breakи continue. Такие конструкции, в общем, могут рассматриваться с точки зрения вызова escape-продолжения, установленного включающим оператором управления (в случае breakи continue, такая интерпретация требует, чтобы конструкции цикла рассматривались с точки зрения рекурсивных вызовов функций). В некоторых языках, таких как ECMAScript, returnотносится к продолжению, установленному замыканием, лексически наиболее внутренним по отношению к оператору - таким образом, returnвнутри замыкания передается управление коду, который его вызвал. Однако в Smalltalk внешне похожий оператор^вызывает escape-продолжение, установленное для вызова метода, игнорируя escape-продолжения любых промежуточных вложенных замыканий. Эскейп-продолжение конкретного замыкания может быть вызвано в Smalltalk только неявно, достигнув конца кода замыкания. Следующие примеры в ECMAScript и Smalltalk подчеркивают разницу:

"Smalltalk" foo  | хз |  хз  : =  # ( 1  2  3  4 ) .  хз  делать: [ : х  |  ^ х ] .  ^ 0 bar  Transcript  show: ( self  foo  printString ) «печатает 1»
// Функция  ECMAScript foo ()  {  var  xs  =  [ 1 ,  2 ,  3 ,  4 ];  хз . forEach ( функция  ( х )  {  возврат  х ;  });  возврат  0 ; } alert ( foo ());  // выводит 0

Приведенные выше фрагменты кода будут вести себя по-разному, потому что ^оператор Smalltalk и оператор JavaScript returnне аналогичны. В примере ECMAScript return xоставит внутреннее замыкание, чтобы начать новую итерацию forEachцикла, тогда как в примере Smalltalk ^xпрервет цикл и вернется из метода foo.

Common Lisp предоставляет конструкцию, которая может выражать любое из вышеперечисленных действий: Lisp (return-from foo x)ведет себя как Smalltalk ^x , а Lisp (return-from nil x)ведет себя как JavaScript return x . Следовательно, Smalltalk позволяет захваченному продолжению побега пережить ту степень, в которой оно может быть успешно запущено. Рассмотреть возможность:

"Smalltalk" foo  ^ [ : x  |  ^ x ] бар  |  f  |  f  : =  self  foo .  f  значение:  123  "ошибка!"

Когда fooвызывается закрытие, возвращаемое методом , оно пытается вернуть значение из вызова foo, создавшего закрытие. Поскольку этот вызов уже был возвращен, а модель вызова метода Smalltalk не следует дисциплине стека спагетти для облегчения множественных возвратов, эта операция приводит к ошибке.

Некоторые языки, такие как Ruby , позволяют программисту выбирать способ returnзахвата. Пример на Ruby:

# Рубин# Закрытие с использованием Proc def  foo  f  =  Proc . new  {  return  "return from foo from inside proc"  }  f . call  # control оставляет foo здесь  return  "return from foo" end# Закрытие с использованием лямбда def  bar  f  =  lambda  {  return  "return from lambda"  }  f . call  # control не покидает бар здесь  return  "return from bar" endпомещает  foo  # выводит "возврат из foo изнутри proc" помещает  бар  # выводит "возврат из бара"

Оба Proc.newи lambdaв этом примере являются способами создания замыкания, но семантика созданных таким образом замыканий отличается от returnоператора.

В Scheme определение и область действия returnоператора управления являются явными (и только для примера с произвольным названием «return»). Ниже приводится прямой перевод образца Ruby.

; Схема ( определение call / cc  call-with-current-continue )( define ( foo )  ( call / cc  ( lambda ( return )  ( define ( f )  ( return  «return from foo from inside proc» ))  ( f )  ; control оставляет foo здесь  ( return  «return from foo» ))))( define ( bar )  ( call / cc  ( lambda ( return )  ( define ( f )  ( call / cc ( lambda ( return )  ( return  «return from lambda ))))  ( f )  ; элемент управления не оставляет бар здесь  ( возврат  "возврат из бара" ))))( дисплей ( foo ))  ; печатает "возвращение из Foo из внутренней Proc" ( перевод строки ) ( дисплей ( бар ))  ; печатает "возврат из бара"

Конструкции, подобные замыканию [ править ]

В некоторых языках есть функции, имитирующие поведение замыканий. В таких языках, как Java, C ++, Objective-C, C #, VB.NET и D, эти функции являются результатом объектно-ориентированной парадигмы языка.

Обратные вызовы (C) [ править ]

Некоторые библиотеки C поддерживают обратные вызовы . Иногда это реализуется путем предоставления двух значений при регистрации обратного вызова в библиотеке: указателя функции и отдельного void*указателя на произвольные данные по выбору пользователя. Когда библиотека выполняет функцию обратного вызова, она передает указатель данных. Это позволяет обратному вызову поддерживать состояние и обращаться к информации, полученной во время регистрации в библиотеке. Идиома похожа на замыкание по функциональности, но не по синтаксису. void*Указатель не типобезопасен так этого C идиома отличается от замыканий типобезопасных в C #, Haskell или ML.

Обратные вызовы широко используются в наборах инструментов GUI Widget для реализации программирования, управляемого событиями, путем связывания общих функций графических виджетов (меню, кнопки, флажки, ползунки, счетчики и т. Д.) Со специфическими для приложения функциями, реализующими конкретное желаемое поведение для приложения.

Вложенная функция и указатель функции (C) [ править ]

С расширением gcc может использоваться вложенная функция , а указатель функции может имитировать замыкания, при условии, что функция не выходит из содержащей его области. Следующий пример недействителен, потому что adderэто определение верхнего уровня (в зависимости от версии компилятора он может дать правильный результат, если скомпилирован без оптимизации, то есть при -O0):

#include  <stdio.h>ЬурейеЕ  междунар  ( * fn_int_to_int ) ( INT );  // тип функции int-> intfn_int_to_int  adder ( int  number )  {  int  add  ( int  value )  {  возвращаемое  значение  +  число ;  }  return  & add ;  // Оператор & здесь необязателен, потому что имя функции в C является указателем, указывающим на себя }int  main ( void )  {  fn_int_to_int  add10  =  сумматор ( 10 );  printf ( "% d \ n " ,  add10 ( 1 ));  возврат  0 ; }

Но перемещение adder(и, необязательно typedef) mainвнутрь делает его действительным:

#include  <stdio.h>Int  основного ( пустоты )  {  ЬурейеГо  Int  ( * fn_int_to_int ) ( INT );  // тип функции int-> int  fn_int_to_int  adder ( int  number )  {  int  add  ( int  value )  {  возвращаемое  значение  +  число ;  }  return  add ;  }  fn_int_to_int  add10  =  сумматор ( 10 );  printf ( "% d \ n " ,  add10 ( 1 ));  возврат  0 ; }

Если выполнить это, теперь печатает, 11как ожидалось

Локальные классы и лямбда-функции (Java) [ править ]

Java позволяет определять классы внутри методов . Они называются локальными классами . Когда такие классы не именуются, они называются анонимными классами (или анонимными внутренними классами). Локальный класс (именованный или анонимный) может ссылаться на имена в лексически включающих классах или на переменные только для чтения (помеченные как final) в лексически включающем методе.

класс  CalculationWindow  расширяет  JFrame  {  частный  изменчивый  результат int  ; // ... public void calculateInSeparateThread ( final URI uri ) { // Выражение «new Runnable () {...}» - это анонимный класс, реализующий интерфейс Runnable. new Thread ( new Runnable () { void run () { // Он может читать конечные локальные переменные: calculate ( uri ); // Он может получить доступ к закрытым полям включающего класса:                    результат  =  результат  +  10 ;  }  }  ). start ();  } }

Захват finalпеременных позволяет захватывать переменные значения. Даже если переменная, которую вы хотите захватить, не является final, вы всегда можете скопировать ее во временную finalпеременную непосредственно перед классом.

Захват переменных по ссылке можно эмулировать, используя finalссылку на изменяемый контейнер, например, одноэлементный массив. Локальный класс не сможет изменить значение самой ссылки на контейнер, но он сможет изменить содержимое контейнера.

С появлением лямбда-выражений в Java 8 [15] закрытие заставляет приведенный выше код выполняться как:

класс  CalculationWindow  расширяет  JFrame  {  частный  изменчивый  результат int  ; // ... public void calculateInSeparateThread ( final URI uri ) { // Код () -> {/ * code * /} - это закрытие. новый поток (() -> { вычислить ( uri ); результат = результат + 10 ; }). start (); } }                    

Локальные классы - это один из типов внутреннего класса, который объявляется в теле метода. Java также поддерживает внутренние классы, объявленные как нестатические члены включающего класса. [16] Их обычно называют просто «внутренними классами». [17] Они определены в теле включающего класса и имеют полный доступ к переменным экземпляра включающего класса. Из-за их привязки к этим переменным экземпляра внутренний класс может быть создан только с явной привязкой к экземпляру включающего класса с использованием специального синтаксиса. [18]

открытый  класс  EnclosingClass  {  / * Определите внутренний класс * /  открытый  класс  InnerClass  {  public  int  incrementAndReturnCounter ()  {  return  counter ++ ;  }  } частный  счетчик int  ; { counter = 0 ; }      public  int  getCounter ()  {  счетчик возврата  ; }  public  static  void  main ( String []  args )  {  EnclosingClass  enclosingClassInstance  =  new  EnclosingClass ();  / * Создание экземпляра внутреннего класса с привязкой к экземпляру * /  EnclosingClass . Внутренний класс  innerClassInstance  =  enclosingClassInstance . новый  InnerClass (); for  ( int  i  =  enclosingClassInstance . getCounter ();  ( i  =  innerClassInstance . incrementAndReturnCounter ())  <  10 ;  / * шаг увеличения пропущен * / )  {  System . из . println ( я );  }  } }

После выполнения это напечатает целые числа от 0 до 9. Остерегайтесь путать этот тип класса с вложенным классом, который объявлен таким же образом с сопутствующим использованием модификатора «static»; они не имеют желаемого эффекта, а представляют собой просто классы без специальной привязки, определенной во включающем классе.

Начиная с Java 8 , Java поддерживает функции как объекты первого класса. Лямбда-выражения этой формы считаются типами, в Function<T,U>которых T является доменом, а U - типом изображения. Выражение можно вызвать с помощью его .apply(T t)метода, но не с помощью стандартного вызова метода.

public  static  void  main ( String []  args )  {  Функция < 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. Блоки, которые должны сохраняться за пределами области, в которой они созданы, возможно, потребуется скопировать. [19] [20]

typedef  int  ( ^ IntBlock ) ();IntBlock  downCounter ( int  start )  {  __block  int  i  =  start ;  return  [[  ^ int ()  {  return  я - ;  }  копия ]  автозапуск ]; }IntBlock  f  =  downCounter ( 5 ); NSLog ( @ "% d" ,  f ()); NSLog ( @ "% d" ,  f ()); NSLog ( @ "% d" ,  f ());

Делегаты (C #, VB.NET, D) [ править ]

Анонимные методы и лямбда-выражения C # поддерживают закрытие:

var  data  =  new []  { 1 ,  2 ,  3 ,  4 }; var  multiplier  =  2 ; var  result  =  data . Выберите ( x  =>  x  *  множитель );

Visual Basic .NET , имеющий многие языковые функции, аналогичные функциям C #, также поддерживает лямбда-выражения с замыканиями:

Тусклые  данные  =  { 1 ,  2 ,  3 ,  4 } Тусклый  множитель  =  2 Тусклый  результат  =  данные . Выберите ( Функция ( x )  x  *  множитель )

В D замыкания реализуются делегатами, указателем функции в паре с указателем контекста (например, экземпляром класса или кадром стека в куче в случае замыканий).

автоматическое  test1 ()  {  INT  = 7 ; возвращать делегат () { возвращают + 3 ; }; // конструкция анонимного делегата }           автоматическое  test2 ()  {  INT  = 20 ; INT Foo () { вернуть + 5 ; } // внутренняя функция return & foo ; // другой способ создания делегата }              пустая  панель ()  {  авто  dg  =  test1 ();  dg ();  // = 10 // хорошо, test1.a закрывается и все еще существует dg  =  test2 ();  dg ();  // = 25 // хорошо, test2.a закрывается и все еще существует }

D версии 1 имеет ограниченную поддержку закрытия. Например, приведенный выше код не будет работать правильно, потому что переменная a находится в стеке, и после возврата из test () ее больше нельзя использовать (скорее всего, вызов foo через dg () вернет ' случайное целое число). Это может быть решено путем явного выделения переменной «a» в куче или использования структур или классов для хранения всех необходимых закрытых переменных и создания делегата из метода, реализующего тот же код. Замыкания могут быть переданы другим функциям, если они используются только тогда, когда указанные значения еще действительны (например, вызов другой функции с замыканием в качестве параметра обратного вызова) и полезны для написания универсального кода обработки данных, поэтому это ограничение на практике это часто не проблема.

Это ограничение было исправлено в версии D 2 - переменная 'a' будет автоматически выделена в куче, потому что она используется во внутренней функции, и делегат этой функции может выйти из текущей области (через присвоение dg или return). Любые другие локальные переменные (или аргументы), на которые не ссылаются делегаты или на которые ссылаются только делегаты, которые не выходят за пределы текущей области, остаются в стеке, что проще и быстрее, чем распределение кучи. То же самое верно и для методов внутреннего класса, которые ссылаются на переменные функции.

Функциональные объекты (C ++) [ править ]

C ++ позволяет определять объекты функций путем перегрузки operator(). Эти объекты ведут себя как функции функционального языка программирования. Они могут быть созданы во время выполнения и могут содержать состояние, но они не захватывают неявно локальные переменные, как это делают замыкания. Начиная с версии 2011 года , язык C ++ также поддерживает замыкания, которые представляют собой тип функционального объекта, автоматически создаваемого из специальной языковой конструкции, называемой лямбда-выражением . Замыкание C ++ может захватывать свой контекст, сохраняя копии переменных, к которым осуществляется доступ, в качестве членов объекта закрытия или по ссылке. В последнем случае, если закрывающий объект выходит из области действия указанного объекта, вызывая егоoperator() вызывает неопределенное поведение, поскольку замыкания C ++ не продлевают время жизни их контекста.

void  foo ( строка  myname )  {  int  y ;  вектор < строка >  n ;  // ...  auto  i  =  std :: find_if ( n . begin (),  n . end (),  // это лямбда-выражение:  [ & ] ( const  string &  s )  {  return  s  ! =  myname  &&  s . size ()  >  y;  }  );  // 'i' теперь либо 'n.end ()', либо указывает на первую строку в 'n'  //, которая не равна 'myname' и длина которой больше 'y' }

Встроенные агенты (Eiffel) [ править ]

Eiffel включает встроенные агенты, определяющие замыкания. Встроенный агент - это объект, представляющий процедуру, определяемую путем предоставления кода процедуры встроенным. Например, в

ok_button . click_event . подписаться  ( агент  ( x ,  y :  INTEGER )  do map . country_at_coordinates  ( x ,  y ). display end )

аргумент для subscribe- это агент, представляющий процедуру с двумя аргументами; процедура находит страну по соответствующим координатам и отображает ее. Весь агент «подписан» на тип события click_eventдля определенной кнопки, так что всякий раз, когда на этой кнопке возникает экземпляр типа события - потому что пользователь щелкнул кнопку - процедура будет выполняться с координатами мыши, передаваемыми как аргументы в пользу xи y.

Основное ограничение агентов Eiffel, которое отличает их от замыканий в других языках, заключается в том, что они не могут ссылаться на локальные переменные из окружающей области. Это проектное решение помогает избежать двусмысленности при разговоре о значении локальной переменной в замыкании - должно ли это быть последнее значение переменной или значение, полученное при создании агента? Только Current(ссылка на текущий объект, аналогично thisJava), его возможности и аргументы самого агента могут быть доступны из тела агента. Значения внешних локальных переменных можно передать, предоставив агенту дополнительные закрытые операнды.

C ++ Builder __closure зарезервированное слово [ править ]

Embarcadero C ++ Builder предоставляет резервное слово __closure, чтобы предоставить указатель на метод с синтаксисом, аналогичным указателю на функцию. [21]

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

typedef  void  ( * TMyFunctionPointer ) (  пустота  );

Аналогичным образом вы можете объявить typedef для указателя на метод, используя следующий синтаксис:

typedef  void  ( __closure  * TMyMethodPointer ) ();

См. Также [ править ]

  • Анонимная функция
  • Блоки (расширение языка C)
  • Шаблон команды
  • Продолжение
  • Каррирование
  • Проблема Funarg
  • Лямбда-исчисление
  • Ленивая оценка
  • Частичное применение
  • Стек спагетти
  • Синтаксическое закрытие
  • Программирование на уровне ценности

Заметки [ править ]

  1. ^ Функция может быть сохранена как ссылка на функцию, например указатель на функцию .
  2. ^ Эти имена чаще всего относятся к значениям, изменяемым переменным или функциям, но также могут быть другими сущностями, такими как константы, типы, классы или метки.

Ссылки [ править ]

  1. ^ Сассман и Стил. «Схема: интерпретатор расширенного лямбда-исчисления». «... структура данных, содержащая лямбда-выражение, и среда, которая будет использоваться, когда это лямбда-выражение применяется к аргументам». ( Wikisource )
  2. ^ Дэвид А. Тернер (2012). «Немного истории языков функционального программирования» . Тенденции функционального программирования '12. Раздел 2, примечание 8 содержит утверждение о M-выражениях.
  3. ^ PJ Landin (1964), Механическая оценка выражений
  4. Джоэл Мозес (июнь 1970 г.), Функция FUNCTION в LISP, или Почему проблема FUNARG должна называться проблемой окружающей среды , hdl : 1721.1 / 5854 , AI Memo 199,Полезная метафора для различия между FUNCTION и QUOTE в LISP - рассматривать QUOTE как пористое или открытое покрытие функции, поскольку свободные переменные ускользают в текущее окружение. ФУНКЦИЯ действует как закрытое или непористое покрытие (отсюда термин «закрытие», используемый Ландином). Таким образом, мы говорим об «открытых» лямбда-выражениях (функции в LISP обычно являются лямбда-выражениями) и «закрытых» лямбда-выражениях. [...] Мой интерес к проблеме окружающей среды возник, когда Ландин, глубоко понимавший эту проблему, посетил Массачусетский технологический институт в 1966–67. Затем я понял соответствие между списками FUNARG, которые являются результатами оценки «закрытых» лямбда-выражений в LISP, и лямбда-замыканиями ISWIM .
  5. ^ Åke Wikström (1987). Функциональное программирование с использованием стандартного машинного обучения . ISBN 0-13-331968-7. Причина, по которой это называется «закрытием», заключается в том, что выражение, содержащее свободные переменные, называется «открытым» выражением, и, связав с ним привязки его свободных переменных, вы его закрываете.
  6. ^ Джеральд Джей Сассман и Гай Л. Стил младший (декабрь 1975), Схема: Интерпретатор для расширенного лямбда-исчисления , AI Memo 349
  7. ^ Абельсон, Гарольд; Сассман, Джеральд Джей; Суссман, Джули (1996). Структура и интерпретация компьютерных программ . Кембридж, Массачусетс: MIT Press. п. 98–99. ISBN 0262510871.
  8. ^ "array.filter" . Центр разработчиков Mozilla . 10 января 2010 . Проверено 9 февраля 2010 года .
  9. ^ "Re: FP, OO и отношения. Кто-нибудь превосходит других?" . 29 декабря 1999 года Архивировано из оригинала 26 декабря 2008 года . Проверен 23 декабрь 2 008 .
  10. ^ Комитет по стандартам C ++ по лямбда-выражениям и замыканиям . 29 февраля 2008 г.
  11. ^ Руководство по GCC, 6.4 Вложенные функции , «Если вы попытаетесь вызвать вложенную функцию через ее адрес после выхода из содержащей функцию, все развалится. Если вы попытаетесь вызвать ее после выхода из содержащего уровня области видимости, и если она относится к какому-то переменных, которые больше не входят в область видимости, вам может повезти, но рисковать неразумно. Если же вложенная функция не ссылается ни на что, что вышло за пределы области видимости, вы должны быть в безопасности ».
  12. ^ Основы актерской семантики Уилл Клингер. Докторская диссертация по математике Массачусетского технологического института. Июнь 1981 г.
  13. ^ "Function.prototype.bind ()" . Веб-документы MDN . Проверено 20 ноября 2018 года .
  14. ^ «Замыкания» . Веб-документы MDN . Проверено 20 ноября 2018 года .
  15. ^ «Лямбда-выражения (Учебники по Java)» .
  16. ^ «Вложенные, внутренние, членские классы и классы верхнего уровня» .
  17. ^ «Пример внутреннего класса (Учебники по Java> Изучение языка Java> Классы и объекты)» .
  18. ^ «Вложенные классы (Учебники по Java> Изучение языка Java> Классы и объекты)» .
  19. ^ Apple Inc. "Темы программирования блоков" . Проверено 8 марта 2011 года .
  20. Иоахим Бенгтссон (7 июля 2010 г.). «Программирование с помощью C-блоков на устройствах Apple» . Архивировано из оригинального 25 октября 2010 года . Проверено 18 сентября 2010 года .
  21. ^ Полную документацию можно найти на http://docwiki.embarcadero.com/RADStudio/Rio/en/Closure

Внешние ссылки [ править ]

  • Оригинальные «Лямбда-документы» : классическая серия статей Гая Стила и Джеральда Сассмана, в которых , среди прочего, обсуждается универсальность замыканий в контексте Scheme (где они представлены как лямбда- выражения ).
  • Нил Гафтер (28 января 2007 г.). «Определение замыканий» .
  • Гилад Браха , Нил Гафтер , Джеймс Гослинг , Питер фон дер Ае . «Замыкания для языка программирования Java (v0.5)» .CS1 maint: несколько имен: список авторов ( ссылка )
  • Замыкания : статья Мартина Фаулера о замыканиях в императивных языках с динамической типизацией .
  • Методы закрытия коллекций : пример технической области, в которой удобно использовать замыкания, автор Мартин Фаулер.