Одной из особенностей языка 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)))

и тому подобное.

Можно даже написать форму redefine, которая объявляет атом без переинициализации его значения (если оно было):

(define-syntax redefine
  (syntax-rules ()
    ((_ name body ...)
     (define name 
       (if (is-bound? name) 
           name 
           body ...)))))

Комментарии (2)


  1. kmatveev
    14.01.2025 09:10

    Я не понял, зачем это может быть нужно. Я не спец в системе модулей Scheme, и не понимаю, что такое "динамическая подгрузка" модулей, но разве модуль - это не набор define-ов, которые все одновременно попадают в область видимости? Если функция инициализации попала в область видимости, то и всё остальное содержимое модуля тоже попало.

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


    1. vadimr Автор
      14.01.2025 09:10

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

      Конечно, если программа у вас быстренько что-то посчитала и выдала результат, то такая постановка вопроса неактуальна. Но, например, систему управления большим комплексом оборудования перезапустить – это целое организационное мероприятие, которого хотелось бы избежать.