При let
все выражения инициализации переменных видят точно такое же лексическое окружение: то, что окружает let
. Если эти выражения захватывают лексические замыкания, все они могут использовать один и тот же объект среды.
В let*
каждое инициализирующее выражение находится в другой среде. Для каждого последующего выражения среда должна быть расширена для создания нового. По крайней мере, в абстрактной семантике, если замыкания захвачены, они имеют разные объекты среды.
A let*
должен быть хорошо оптимизирован, чтобы свести на нет ненужные расширения среды для того, чтобы он подходил в качестве повседневной замены для let
. Должен быть компилятор, который работает, какие формы обращаются к чему-либо, а затем преобразует все независимые в большие, объединенные let
.
(Это верно, даже если let*
- это просто макрооператор, который генерирует каскадные let
формы; оптимизация выполняется для этих каскадных let
s).
Вы не можете реализовать let*
как единый наивный let
со скрытыми назначениями переменных для инициализации, потому что будет выявлено отсутствие надлежащей области видимости:
(let* ((a (+ 2 b)) ;; b is visible in surrounding env
(b (+ 3 a)))
forms)
Если это превращено в
(let (a b)
(setf a (+ 2 b)
b (+ 3 a))
forms)
это не будет работать в этом случае; внутренний b
затеняет внешний b
, поэтому мы добавляем 2 к nil
. Такого рода преобразование можно выполнить, если мы переименуем все эти переменные в альфа-формате. Окружающая среда тогда хорошо сплющена:
(let (#:g01 #:g02)
(setf #:g01 (+ 2 b) ;; outer b, no problem
#:g02 (+ 3 #:g01))
alpha-renamed-forms) ;; a and b replaced by #:g01 and #:g02
Для этого нам нужно рассмотреть поддержку отладки; если программист входит в эту лексическую область с помощью отладчика, мы хотим, чтобы они имели дело с #:g01
вместо a
.
В общем, let*
- это сложная конструкция, которая должна быть хорошо оптимизирована для выполнения, а также let
в тех случаях, когда она может быть уменьшена до let
.
Уже одно это не оправдывает предпочтение let
над let*
. Давайте предположим, что у нас есть хороший компилятор; почему бы не использовать let*
все время?
Как общий принцип, мы должны отдавать предпочтение конструкциям более высокого уровня, которые делают нас продуктивными и снижают количество ошибок, по сравнению с склонными к ошибкам конструкциями более низкого уровня и максимально полагаться на хорошие реализации конструкций более высокого уровня, чтобы мы редко приходится жертвовать их использованием ради производительности. Вот почему мы в первую очередь работаем на таком языке, как Лисп.
Это рассуждение неприменимо к let
против let*
, потому что let*
явно не является абстракцией более высокого уровня относительно let
. Они примерно на «равном уровне». С let*
вы можете внести ошибку, которая решается простым переключением на let
. И наоборот . let*
на самом деле просто мягкий синтаксический сахар для визуально разрушающегося let
вложения, а не новая существенная абстракция.