Одной из особенностей языка Scheme, в отличие от многих других динамических языков и "взрослых" диалектов Лиспа, является лексическая область видимости идентификаторов (как в классических компилируемых языках). С одной стороны, это удобно для статического анализа кода программы и особенно удобно для компиляции. С другой стороны, это затрудняет непосредственные операции с лексическим окружением.
Одной из самых проблематичных операций, реализацию которой затрудняет идеология Scheme, является стандартная для ряда диалектов форма is-bound?
, предназначенная для проверки, определён ли её аргумент, то есть занесён ли он в системный ассоциативный список атомов и, как следствие, имеет ли он вообще какое-либо связанное с ним значение. Рассмотрим, как мы можем реализовать данную форму в Gambit Scheme.
Зачем вообще это нужно? Например, мы хотим динамически подгрузить какой-нибудь имеющий побочные эффекты модуль в уже выполняющуюся программу, а этот модуль хочет проверить, выполнялись ли его функции раньше и, как следствие, нужно ли выполнять начальную инициализацию состояния, либо подхватить уже готовое. Если модуль попробует проверять свои собственные переменные без их предварительного создания с помощью define
, то выполнение просто прервётся с ошибкой "Unbound variable". А если начнёт с define
, то потеряет их прошлое состояние.
К счастью, такое положение дел подсказывает нам выход из ситуации – мы можем воспользоваться обработчиком исключений, чтобы получить искомый результат.
Ясно, что is-bound?
должна представлять собой специальную форму, а не обычный функциональный вызов, так как её аргумент не должен вычисляться перед вызовом. Для реализации специальной формы воспользуемся макроопределением, например, таким:
(define-syntax is-bound?
(syntax-rules ()
((_ var)
(with-exception-catcher
(lambda (e)
(if (unbound-global-exception? e)
#f
(raise e)))
(lambda () var #t)))))
Здесь мы пытаемся вычислить var
, и, если это удалось, переходим дальше к возврату #t
, а если возникло исключение – то убеждаемся, что это unbound-global-exception, и тогда возвращаем #f
. Если же мы получили какое-то другое исключение, то эскалируем его наверх.
В данном случае фактическим параметром формы может быть не только атом, но и произвольное выражение, что, однако, несущественно с точки зрения прагматики.
Успех? Не совсем.
В интерпретаторе всё работает ожидаемым образом. Однако, если мы попробуем скомпилировать нашу программу при помощи gsc
, то обнаружим, что с точки зрения компилятора несвязанных атомов не существует (чего, собственно, и следовало ожидать). В отличие от интерпретатора, компилятор распространяет лексическую область видимости атома назад по тексту программы.
К счастью, не всё совсем плохо. Вместо лексической неопределённости компилятор использует начальное присваивание атому специального значения #!unbound
. Поэтому наш макрос легко модифицировать таким образом, чтобы он работал и при интерпретации, и при компиляции:
(define-syntax is-bound?
(syntax-rules ()
((_ var)
(with-exception-catcher
(lambda (e)
(if (unbound-global-exception? e)
#f
(raise e)))
(lambda ()
(not (eq? var #!unbound)))))))
В интерпретаторе для неопределённых атомов будет работать исключение, а в компиляторе – сравнение.
Теперь точно успех.
Отметим, что при компиляции будут наблюдаться неопределённые результаты, если в качестве параметра указывать сложное выражение – но мы на это, собственно, и не рассчитывали. При желании можно предусмотреть дополнительную проверку типа var
.
А дальше уже можно делать, например, такие штуки:
(define *my-mutex*
(if (is-bound? *my-mutex*)
*my-mutex*
(make-mutex)))
и тому подобное.