Проблемы с реализацией замыканий в нефункциональных настройках

18

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

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

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

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

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

Почему замыкания так трудно (понять и понять)? Это слишком широкий и расплывчатый вопрос, поэтому позвольте мне сосредоточиться на следующих взаимосвязанных вопросах:

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

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

Рафаэль
источник
Хороший вопрос. Замыкания были реализованы в Scala, и Мартин Одерский написал компилятор Java 1.5, поэтому не ясно, почему они не в Java 7. В C # они есть. (Я постараюсь написать лучший ответ позже.)
Дейв Кларк
4
Нечистые функциональные языки, такие как Lisp и ML, прекрасно приспособлены к замыканиям, поэтому для них не может быть внутренней семантической причины проблем.
Жиль "ТАК - перестань быть злым"
Я включил этот пункт, потому что я изо всех сил пытался представить, как семантическая ступенька может выглядеть для замыканий. Вполне может быть, что замыкания сами по себе не являются проблемой, но включить их в язык, который не предназначен для них, сложно.
Рафаэль
1
Взгляните на pdfs.semanticscholar.org/73a2/… - авторы Lua сделали это очень умно и обсудят также общие проблемы реализации замыканий
Булат

Ответы:

10

Могу ли я направить вас на страницу Википедии о проблеме Фунарга ? По крайней мере, так люди из компилятора ссылались на проблему реализации замыкания.

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

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

  • Локальные переменные в функциях никогда не используются после возврата функции.
  • Локальные переменные могут использоваться после возврата функции.

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

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


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

Я также могу думать о других вещах, которые делают функции первого класса менее тривиальными для реализации, такие как решение, что делать с «магическими» переменными, такими как this , self или super, и как взаимодействовать с существующими операторами потока управления, такими как break и return (мы хотим разрешить нелокальные возвраты или нет?). Но, в конце концов, недавняя популярность первоклассных функций, кажется, указывает на то, что языки, в которых их нет, в основном делают это по историческим причинам или из-за какого-то значительного конструктивного решения на ранних этапах.

hugomg
источник
1
Знаете ли вы о каких-либо языках, которые различают случаи с повышением и понижением? В языках .NET универсальный метод, который ожидал получить функцию только для нисходящего потока, мог бы получать структуру универсального типа вместе с делегатом, который получал бы такую ​​структуру как byref (в C #, « refпараметр»). Если вызывающая сторона инкапсулирует все переменные, представляющие интерес в структуре, делегат может быть полностью статичным, избегая необходимости выделения кучи. Компиляторы не предлагают никакой приятной синтаксической справки для таких конструкций, но Framework может их поддерживать.
суперкат
2
@supercat: Rust имеет несколько типов замыканий, которые позволяют вам применять во время компиляции, если внутренняя функция должна будет использовать кучу. Однако это не означает, что реализация не может попытаться избежать выделения кучи, не заставляя вас заботиться обо всех этих дополнительных типах. Компилятор может попытаться определить время жизни функции или использовать проверки времени выполнения, чтобы лениво сохранять переменные в кучу только в случае крайней необходимости (см. Раздел «лексическая область действия» в подробности см. В Evolution of Lua )
hugomg
5

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

Рассмотрим следующее псевдо-C # (я вырезал немного C #-специфичных вещей):

int x = 1;
function f = function() { x++; };
for (int i = 1; i < 10; i++) {
    f();
}
print x; // Should print 9

Компилятор превращает это во что-то вроде этого:

class FunctionStuff {
   int x;
   void theFunction() {
       x++;
   }
}

FunctionStuff theClosureObject = new FunctionStuff();
theClosureObject.x = 1;
for (int i = 1; i < 10; i++) {
    theClosureObject.theFunction();
}
print theClosureObject.x; // Should print 9

(на самом деле, переменная f все равно будет создана, где f - это «делегат» (= указатель на функцию), но этот делегат по-прежнему связан с объектом theClosureObject - я оставил эту часть для ясности для тех, кто не знаком с C #)

Это преобразование является довольно масштабным и сложным: рассмотрим замыкания внутри замыканий и взаимодействие замыканий с остальными возможностями языка C #. Я могу себе представить, что эта функция была отодвинута назад для Java, поскольку в Java 7 уже появилось довольно много новых функций.

Алекс тен Бринк
источник
Я могу видеть, куда это идет; наличие нескольких замыканий и доступ к основной области действия одной и той же переменной будет беспорядочным.
Рафаэль
Честно говоря, это больше связано с использованием существующей ОО-инфраструктуры для реализации замыканий, чем с любой реальной проблемой с ними. Другие языки просто размещают переменные в отдельной структуре, не содержащей методов, а затем позволяют нескольким замыканиям делиться ими, если они хотят.
hugomg
@ Рафаэль: как вы относитесь к замыканиям внутри замыканий? Погоди, позволь мне добавить это.
Алекс тен Бринк
5

Чтобы ответить на часть вашего вопроса. Формализм, описанный Моррисеттом и Харпером, охватывает семантику больших и малых шагов полиморфных языков высшего порядка, содержащих замыкания. До этого есть документы, в которых указана та семантика, которую вы ищете. Посмотрите, например, на машину SECD . Добавить изменяемые ссылки или изменяемые локальные объекты в эту семантику просто. Я не вижу каких-либо технических проблем в предоставлении такой семантики.

Дэйв Кларк
источник
Спасибо за ссылку! Кажется, это не облегчает чтение, но этого, вероятно, следует ожидать от семантической статьи.
Рафаэль
1
@ Рафаэль: Есть, наверное, более простые. Я постараюсь найти что-то и вернусь к вам. В любом случае, рисунок 8 имеет семантику, которую вы ищете.
Дейв Кларк
Может быть, вы можете дать приблизительный обзор соотв. центральные идеи в вашем ответе?
Рафаэль
2
@Raphael. Возможно, я мог бы отослать вас к своим лекционным заметкам, которые я использую для курса языков программирования, который дает вам краткое введение. Пожалуйста, посмотрите раздаточные материалы 8 и 9.
Uday Reddy
1
Эта ссылка отображается либо мертвой, либо за невидимой аутентификацией. ( cs.cmu.edu/afs/cs/user/rwh/public/www/home/papers/gcpoly/tr.pdf ). Я получаю 403 запрещено.
Бен Флетчер