Больше объяснений по поводу лексической привязки в замыканиях? - PullRequest
8 голосов
/ 15 декабря 2009

Есть много сообщений SO, связанных с этим, но я спрашиваю это снова с другой целью

Я пытаюсь понять, почему замыкания важны и полезны. Одна из вещей, которые я читал в других сообщениях SO, связанных с этим, заключается в том, что когда вы передаете переменную для закрытия, закрытие начинает запоминать это значение с тех пор. Это весь Технический аспект этого или есть что-то еще, что там происходит.

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

На языке Clojure я могу сделать следующее: Но поскольку ценность есть неизменна, эта проблема не возникает. Как насчет других языков и каково правильное техническое определение замыкания?

(defn make-greeter [greeting-prefix]
    (fn [username] (str greeting-prefix ", " username)))

((make-greeter "Hello") "World")

Ответы [ 8 ]

9 голосов
/ 17 декабря 2009

Это не тот ответ, который, похоже, вызывает здесь голосование "за", но я от всей души призываю вас найти ответ на свой вопрос, прочитав учебник Шрирама Кришнамурти (бесплатно!) (Онлайн!), Языки программирования: приложение и интерпретация .

Я перефразирую книгу очень, очень кратко, суммируя развитие крошечных крошечных переводчиков, через которые она вас ведет:

  • язык арифметических выражений (AE)
  • язык арифметических выражений с именованными выражениями (WAE); реализация этого включает в себя разработку функции substitution , которая может заменить имена на значения
  • язык, который добавляет функции первого порядка (F1WAE): использование функции предполагает замену значения для каждого из имен параметров.
  • Тот же язык, без подстановки: оказывается, что «окружения» позволяют избежать накладных расходов при упреждающей замене.
  • язык, который устраняет разделение между функциями и выражениями, позволяя функции, которые должны быть определены в произвольных местоположениях (FWAE)

Это ключевой момент: вы реализуете это, и затем вы обнаруживаете, что с заменой это работает нормально, но с окружениями это не работает. В частности, чтобы исправить это, вы должны обязательно связать с определением оцененной функции среду, которая была на месте, когда она была оценена. Эта пара (fundef + Environment-of-Definition) является тем, что называется «замыканием».

Уф!

Хорошо, что происходит, когда мы добавляем изменяемые привязки к изображению? Если вы попробуете это сами, вы увидите, что естественная реализация заменяет среду, которая связывает имена со значениями, средой, которая связывает имена с привязками. Это ортогонально понятию замыканий; поскольку замыкания захватывают среды, а среды теперь отображают имена на привязки, вы получаете описанное вами поведение, благодаря которому мутация переменной, захваченной в среде, является видимой и постоянной.

Опять же, я очень призываю вас взглянуть на PLAI .

5 голосов
/ 16 декабря 2009

A лексическое замыкание - это то, в котором вложенные переменные (например, greeting-prefix в вашем примере) заключены в виде ссылки. Созданное замыкание не просто получает значение greeting-prefix во время его создания, но получает ссылку. Если greeting-prefix изменяется после создания замыкания, то его новое значение будет использоваться замыканием при каждом вызове.

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

В "imperative-languages-with-closures", таких как C # и Java (через анонимные классы), необходимо принять решение о том, заключена ли вложенная переменная в значение или в качестве ссылки. В Java это решение опережает только включение final переменных, которые эффективно имитируют функциональный язык в том, что касается этой переменной. В C # я считаю, что это другое дело.

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

5 голосов
/ 15 декабря 2009

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

(defn outer []
    (let [foo (get-time-of-day)]
      (defn inner []
          #(str "then:" foo " now:" (get-time-of-day)))))


(def then-and-now (outer))
(then-and-now)    ==> "then:1:02:03 now:2:30:01"
....
(then-and-now)    ==> "then:1:02:03 now:2:31:02"

когда эта функция определена, создается класс, и небольшая структура (замыкание) выделяется в куче, в которой хранится значение foo. класс имеет указатель на это (или он содержит его, я не уверен). если вы запустите это снова, тогда будет назначено второе закрытие для хранения этого другого foo. Когда мы говорим «эта функция закрывается над foo», мы имеем в виду, что она имеет ссылку на стриктуру / класс / все, что хранит состояние foo во время ее компиляции. Причина, по которой вам нужно закрыть что-то, заключается в том, что содержащая его функция исчезает до того, как данные будут использованы. В этом случае external (который содержит значение foo) закончится и исчезнет задолго до того, как будет использован foo, поэтому никто не будет рядом, чтобы модифицировать foo. конечно, foo может передать ссылку кому-то, кто может затем изменить его.

3 голосов
/ 16 декабря 2009
2 голосов
/ 16 декабря 2009

Вам может понравиться чтение О лямбдах, перехвате и изменчивости , которое описывает, как это работает в C # и F #, для сравнения.

2 голосов
/ 15 декабря 2009

Вы можете думать о замыкании как о «среде», в которой имена связаны с значениями . Эти имена полностью закрыты для замыкания, поэтому мы говорим, что оно «закрывает» свою среду. Таким образом, ваш вопрос не имеет смысла, в том смысле, что «снаружи» не может повлиять на закрытую среду. Да, замыкание может ссылаться на имя в глобальной среде (другими словами, если оно использует имя, которое не связано в его закрытой закрытой среде), но это другая история.

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

1 голос
/ 16 декабря 2009

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

Основная идея этого типа блокировки более просто проиллюстрирована на примере счетчика, который huaiyuan разместил в Common Lisp, пока я составлял этот ответ. На самом деле версия Clojure интересна тем, что показывает, что проблема закрытой переменной, изменяющей свое значение, действительно возникает в Clojure, если переменная содержит экземпляр одного из ссылочных типов.

(defn create-counter []
  (let [counter (atom 0)
        inc-counter! #(swap! counter inc)
        get-counter (fn [] @counter)]
    [inc-counter! get-counter]))

Что касается оригинального примера make-greeter, вы можете переписать его таким образом (обратите внимание на deref / @):

(defn make-greeter [greeting-prefix]
    (fn [username] (str @greeting-prefix ", " username)))

Затем вы можете использовать его для передачи персонализированных поздравлений от различных операторов различных разделов веб-сайта. : -)

((make-greeter "Hello from Gizmos Dept") "John")
((make-greeter "Hello from Gadgets Dept") "Jack").
0 голосов
/ 12 февраля 2010

Вы можете думать о закрытии как о «среда», в которой имена привязан к ценностям. Эти имена полностью личное закрытие, которое Вот почему мы говорим, что он "закрывается" его окружение. Так твой вопрос не имеет смысла, в том, что «снаружи» не может повлиять на закрытая среда. Да закрытие может относиться к имени в глобальная среда (другими словами, если он использует имя, которое не связано в его частная, закрытая среда), но это другая история.

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

CL-USER> (let ((x (list 1 2 3)))
           (prog1
               (let ((y x))
                 (lambda () y))
             (rplaca x 2)))
#<COMPILED-LEXICAL-CLOSURE #x9FEC77E>
CL-USER> (funcall *)
(2 2 3)

И - поскольку они, очевидно, возможны - я думаю, что вопрос является законным.

...