Я думаю, что этот вопрос состоит из двух частей. Существует общий вопрос о свободном доступе к переменным в Лиспе (и других языках), который является большой темой, поскольку существует много, много Лиспов и даже больше других языков, включая те, которые имеют динамическую область видимости для некоторых переменных (CL) и те, которые имеют только динамическая область (до недавнего времени элисп, многие другие старые реализации). Существует также вопрос о том, когда ссылки на переменные должны быть разрешены в Лиспе (и других языках), и, в частности, необходимость в прямых ссылках, а также когда и как они разрешаются ..
В этом ответе я обращаюсь только ко второй части, о предварительных ссылках, которые, как мне кажется, прямо вас смущают. Я приведу примеры использования Racket с языком r5rs
, поскольку именно это вы использовали в комментарии.
Прежде всего, любой язык, который поддерживает рекурсию без скачков через экстремальные значения ( aka с использованием Y-комбинатора), должен поддерживать ссылки на имена, которые еще не связаны, которые называются прямыми ссылками . Учтите это:
#lang r5rs
(define foo
(lambda (x)
(if (null? x)
0
(+ 1 (foo (cdr x))))))
ОК, поэтому при оценке функции имеется ссылка на foo
для рекурсивного вызова. Но foo
еще не связан в этой точке , потому что foo
будет привязан к самой функции, и это то, что мы определяем. И действительно, если вы делаете что-то вроде этого:
(let ((r (lambda (x)
(if (null? x)
0
(+ 1 (r (cdr x)))))))
r)
Это ошибка, потому что r
действительно еще не определено. Вместо этого вам нужно использовать letrec
, который делает подходящую магию, чтобы убедиться, что все работает.
Ну, вы можете утверждать, что letrec
превращается во что-то вроде этого:
(let ((r #f))
(set! r (lambda (x)
(if (null? x)
0
(+ 1 (r (cdr x))))))
r)
И, возможно, это так. Но как насчет этого?
#lang r5rs
(define foo
(lambda (x)
(if (null? x)
0
(bar x))))
(define bar
(lambda (x)
(+ 1 (foo (cdr x)))))
И для более сложных программ.
Так что здесь есть общая проблема с прямой ссылкой. По сути, невозможно писать программы таким образом, чтобы в тексте программы не было ссылок на имена, о которых пока ничего не известно. Обратите внимание, что в приведенной выше программе foo
относится к bar
, а bar
относится к foo
, поэтому нет порядка определений foo
и bar
который не включает ссылки на имена, которые еще не связаны в источнике.
Обратите внимание, что эта проблема затрагивает практически все языки программирования: в этом нет ничего уникального для языков семейства Lisp.
Итак, как это решается? Ну, ответ «это зависит от того, как определен конкретный язык, который вам нужен». Я не совсем уверен, что спецификация R5RS говорит об этом, но я думаю, что я уверен, что Ракет говорит об этом.
Ракет говорит, что все прямые ссылки должны быть отсортированы на уровне модуля. Так что, если у меня есть исходный файл Racket, например:
#lang r5rs
(define a
(lambda (x) (+ x y)))
(define y 1)
Это нормально, потому что в конце модуля определены и a
и x
, и поэтому определение a
подходит. Это ничем не отличается от определений foo
и bar
, приведенных выше, и обратите внимание, что исходный файл выглядит так:
#lang r5rs
(define foo
(lambda (x)
(if (null? x)
0
(bar x))))
не является законным: bar
не связано:
$ racket ts.rkt
ts.rkt:7:9: bar: unbound identifier
[...]
Таким образом, ответ на вопрос о прямой ссылке является двояким:
- написание программ практически на любом практическом языке, в частности на Лиспе, требует прямых ссылок на имена, которые еще не связаны в исходной точке;
- Спецификации языка должны определять, когда и как разрешаются такие прямые ссылки.
Я думаю, это то, что вас смущает. В частности модуль Racket, который содержит только это:
#lang r5rs
(define a (lambda (x) (+ x y)))
Недопустимо в Racket. Но этот модуль:
#lang r5rs
(define a (lambda (x) (+ x y)))
(define y 1)
Допустимо, потому что y
связан в момент разрешения прямых ссылок.
Common Lisp более сложен, чем этот, по двум причинам:
- это Lisp-2, поэтому привязки функций и переменных находятся в разных пространствах имен;
- нет стандартных привязок лексических переменных верхнего уровня.
Так, например, если мы возьмем что-то вроде этого (предполагается, что оно находится на верхнем уровне, без предшествующих ему других пользовательских определений):
(defun foo (x)
(+ x y))
Тогда y
не может быть ссылкой на лексическую переменную, потому что лексическое окружение foo
пусто, поскольку нет привязок лексической переменной верхнего уровня.
Таким образом, y
должен быть ссылкой на динамическую переменную (специальная переменная на языке CL). Но на самом деле CL решает проблему прямой ссылки для динамических переменных, говоря, что такие ссылки не разрешены. Таким образом, приведенное выше определение не является законным CL. Многие компиляторы скомпилируют его с предупреждением, но это не обязательно.
Следующий вариант, однако, подходит (обратите внимание, что я использовал соглашение CL для динамических переменных).
(defvar *y* 1)
(defun foo (x)
(+ x *y*))
Это хорошо, потому что *y*
известно до определения foo
.
И на самом деле я солгал: прямые ссылки на динамические переменные разрешены, но вы должны сказать о них язык:
(defun foo (x)
(declare (special *y*))
(+ x *y*))
И теперь, если есть более поздний (defvar *y* ...)
(или эквивалентный), вызов foo
будет в порядке. Если такого глобально-специального провозглашения *y*
не существует, то я думаю, что произойдет, если вызов foo
будет либо неопределенным, либо ошибкой (и я надеюсь, что последнее, но я не уверен.
Наконец, даже без глобального специального объявления для *y*
, это также хорошо:
(let ((*y* 3))
(declare (special *y*))
(foo 1))
Это нормально, потому что *y*
локально объявлен специальным (для него есть связанное объявление).
Прямые ссылки на функции разрешены в CL, и существуют сложные правила относительно того, когда они должны быть разрешены, но я не уверен, что могу вспомнить.