вторник, 26 августа 2008 г.

Глобальные переменные в Common Lisp и let

С самого начал изучения программирования в умных книжках читал об "ужасах" использования глобальных переменных. Поскольку я склонен доверять "умным людям", то всегда избегал использования таких средств. Правда, был период, когда находясь под сильным влиянием Александреску широко использовал Singleton-ы - чудом избежал больших проблем, утвердился на собственном опыте, что глобальные переменные - зло, а пресловутый паттерн "одиночка" совершенно не решает свойственных им проблем.

Но вот я стал изучать Common Lisp и роясь в исходников многих библиотек обнаружил, что lisp-разработчики совершенно не стесняются использовать глобальные переменные. Что это? Код был написан профессионально и качественно, но широкое использование глобальных переменных меня просто шокировало. Разве они (разработчики) не знают что это очень опасно? К тому же, конкретно библиотека cl-pdf вызывала у меня серьезные опасения за корректностью своей работы в условиях многопоточности (использовалась в рамках веб-сервера hunchentoot). Поскольку система, всё же, работала нормально, то видимо была какая-то тонкость, которую я не понимал. Посему, просто отложил этот факт в памяти и вернулся к работе.

Первым шагом к пониманию стало прочтение в SICP описания "модели вычислений с окружением". Знания получил, но понимания тогда ещё не обрёл :-)

Наступлению прозрения, как это часто бывает, способствовало несчастье :-) При работе с cl-pdf в sbcl при текущей локали ru_RU.UTF-8 у меня неверно обрабатывался русский текст и получались всякие невменяемые закорючки. Исследование выявило проблему не в самой cl-pdf, а в chunga (при работе под sbcl). Проблему можно было решить либо небольшим патчем (буквально, переделать одну строку), что мне не очень нравилось, либо в моём коде за счёт временного изменения переменной sb-impl::*default-external-format*, которую следовало временно установить, например, в :latin-1. Первоначально я написал примерно следующее:
(let ((old-external-format  sb-impl::*default-external-format*))
(setf sb-impl::*default-external-format* :latin-1)
(|Здесь код по выводу сгенерённого pdf-документа в поток|)
(setf sb-impl::*default-external-format* old-external-format))
Необходимого эффекта я добился, но код, конечно, совершенно ужасен. Во-первых, не обрабатываются возможные исключения, в результате чего, измененная глобальная переменная может так и не восстановить свое прежнее значение (конечно, можно добавить обработку исключений, но смотреть будет еще страшнее). Во-вторых, система изменяются глобально, что в условиях многопоточности чревато большими проблемами (наступление которых - лишь вопрос времени). Короче, весьма характерный пример проблем с глобальными переменными. И тут я понял:
(let ((sb-impl::*default-external-format* :latin-1))
(|Здесь код по выводу сгенерённого pdf-документа в поток|))
Это работает! Здесь значение глобальной переменной sb-impl::*default-external-format* связывается с новым значение, которое действует только внутри блока let и совершенно не затрагивает остальную систему. И это потрясающе! Такая возможность решает изрядную долю проблем, связанных с использованием глобальных переменных и позволяет делать код проще без потери гибкости за счёт возможности передавать аргументы не через формальные аргументы функций, а через "окружение".

P.S. lispnik, после прочтения данного соообщения, указал ссылку на свой старый пост, в котором данная тема раскрыта горараздо лучше, при чём на основе теории и спецификаций (а не практики и эксперимента, как в моём случае). Прочитать можно здесь: http://lispnik.livejournal.com/9137.html

Комментариев нет:

Отправить комментарий