Использование замыкания вместо глобальной переменной - PullRequest
0 голосов
/ 25 июня 2019

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

* (defparameter *x* 0)
*X*
* (defun foo ()
    (incf *x*))
FOO
* (defun bar ()
    (foo))
BAR
* (bar)
1
* *x*
1

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

* (let ((x 0))
    (defun inc-x () (incf x))
    (defun reset-x () (setf x 0))
    (defun get-x () x))
GET-X
* (defun bar ()
    (inc-x))
BAR
* (defun foo ()
    (bar))
FOO
* (get-x)
0
* (foo)
1
* (get-x)
1

Глобальная переменная исчезла, но все равно кажется, что выражение (inc-x) имеет (скрытый) побочный эффект и будет возвращать разные (но неиспользуемые) значения при каждом вызове. Подтверждает ли это, что использование замыкания для рассматриваемой переменной не решает проблему прозрачности?

Ответы [ 4 ]

8 голосов
/ 25 июня 2019

глобальные переменные являются проблематичными, главным образом потому, что они могут помешать ссылочной прозрачности

Если кто-то хочет создать глобальное значение конфигурации, глобальная переменная в Common Lisp просто прекрасна.

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

Нет общего требования для процедур быть ссылочно-прозрачными .

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

(let ((x 0))
  (defun inc-x () (incf x))
  (defun reset-x () (setf x 0))
  (defun get-x () x))

Практически выше это означает, что оно

  • трудно проверить
  • имеет проблематичные последствия перезагрузки кода
  • запрещает компилятору файлов распознавать функции верхнего уровня, создаваемые
  • целый API для управления одной переменной
5 голосов
/ 25 июня 2019

Ссылочная прозрачность означает, что если вы связываете некоторую переменную x с выражением e, вы можете заменить все вхождения x на e без изменения результата. Например:

(let ((e (* pi 2)))
  (list (cos e) (sin e)))

Выше можно написать:

(list (cos (* pi 2))
      (sin (* pi 2)))

Полученное значение эквивалентно первому для некоторого полезного определения эквивалентности (здесь equalp, но вы можете выбрать другое). Сравните это с:

(let ((e (random))
  (list e e))

Здесь выше, каждый вызов random дает различный результат (статистически), и, таким образом, поведение отличается, если вы повторно используете один и тот же результат несколько раз или генерируете новый после каждого вызова.

Специальные переменные похожи на дополнительные аргументы для функций, они могут влиять на результат результата, просто будучи привязанными к различным значениям. Рассмотрим *default-pathname-defaults*, который используется для построения путей.

Фактически, для данной привязки этой переменной каждый вызов (merge-pathnames "foo") возвращает один и тот же результат. Результат изменяется, только если вы используете одно и то же выражение в другом динамическом контексте, что ничем не отличается от вызова функции с другими аргументами.

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

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

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

1 голос
/ 25 июня 2019

Где побочный эффект второго примера?x внутри let недоступен снаружи.

Вот еще один пример замыкания с функциями верхнего уровня и явным счетчиком внутри него.

(defun repeater (n)
  (let ((counter -1))
     (lambda ()
       (if (< counter n)
         (incf counter)
         (setf counter 0)))))

(defparameter *my-repeater* (repeater 3))
;; *MY-REPEATER*
(funcall *my-repeater*)
0
(funcall *my-repeater*)
1

https://lispcookbook.github.io/cl-cookbook/functions.html#closures

1 голос
/ 25 июня 2019

Этот код не является ссылочно прозрачным.Это улучшение по сравнению с special переменными.

Код, который вы поместите, будет функционалом nonce , если вы отбросите reset-x.

Мой ответ ваш предыдущий вопрос имел общие рекомендации по special переменным.Для вашего конкретного случая, может быть, они того стоят?Я мог бы видеть случай использования специальных переменных в качестве одноразового номера, например, когда, вероятно, глупо передавать их.

Common Lisp имеет так много возможностей для работы с глобальной информацией, что редко возникает необходимостьдля того, чтобы иметь много глобальных переменных.Вы можете определить список *env* для хранения ваших значений, или поместить их в хеш-таблицу, или поместить их в списки символов, или упаковать их в замыкание, чтобы обойти, или сделать что-то еще, или использовать CLOS.

...