LET против LET * в Common Lisp - PullRequest
       53

LET против LET * в Common Lisp

79 голосов
/ 17 февраля 2009

Я понимаю разницу между LET и LET * (параллельное и последовательное связывание), и теоретически это имеет смысл. Но есть ли какой-нибудь случай, когда вам когда-нибудь действительно нужен LET? Во всем моем Лисп-коде, который я недавно просмотрел, вы можете заменить каждый LET на LET * без изменений.

Редактировать: ОК, я понимаю , почему какой-то парень изобрел LET *, предположительно в качестве макроса, когда-то. Мой вопрос, учитывая, что LET * существует, есть ли причина для LET остаться? Вы написали какой-нибудь реальный код на Лиспе, где LET * не будет работать так же хорошо, как обычный LET?

Я не покупаю аргумент эффективности. Во-первых, распознавать случаи, когда LET * может быть скомпилирован во что-то столь же эффективное, как LET, не кажется таким уж сложным. Во-вторых, в спецификации CL есть много вещей, которые просто не выглядят так, как будто бы они были разработаны с учетом эффективности. (Когда вы в последний раз видели LOOP с объявлениями типов? Их так сложно понять, что я никогда не видел, чтобы они использовались.) До тестов Дика Габриэля в конце 1980-х CL был совершенно медленным.

Похоже, что это еще один случай обратной совместимости: мудро, никто не хотел рисковать сломать что-то столь же фундаментальное, как LET. Это было мое предчувствие, но приятно слышать, что ни у кого нет глупо-простого случая, который я пропустил, когда LET сделал кучу вещей до смешного проще, чем LET *.

Ответы [ 16 ]

1 голос
/ 06 апреля 2012

Оператор let представляет единую среду для всех привязок, которые он указывает. let*, по крайней мере, концептуально (и если мы на мгновение игнорируем объявления) вводит несколько сред:

То есть:

(let* (a b c) ...)

это как:

(let (a) (let (b) (let (c) ...)))

Таким образом, в некотором смысле, let является более примитивным, тогда как let* является синтаксическим сахаром для написания каскада let -s.

Но не обращайте на это внимания. (И ниже я приведу обоснование того, почему мы должны «не обращать внимания»). Дело в том, что есть два оператора, и в «99%» кода не имеет значения, какой из них вы используете. Причина, по которой let предпочтительнее let*, заключается просто в том, что в его конце нет *.

Всякий раз, когда у вас есть два оператора, и у одного из них на его имени висит *, используйте оператор без *, если он работает в такой ситуации, чтобы сделать ваш код менее уродливым.

Это все, что нужно.

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

Теперь для этого обещанного обсуждения. Хотя let* концептуально вводит несколько сред, очень просто скомпилировать конструкцию let*, чтобы сгенерировать единую среду. Таким образом, хотя (по крайней мере, если мы игнорируем объявления CL ANSI), существует алгебраическое равенство, согласно которому один let* соответствует нескольким вложенным let -ам, что делает let более примитивным, но на самом деле нет причин для t разверните let* в let, чтобы скомпилировать его, и даже плохую идею.

Еще одна вещь: обратите внимание, что lambda Common Lisp на самом деле использует let* -подобную семантику! Пример:

(lambda (x &optional (y x) (z (+1 y)) ...)

здесь, x init-форма для y обращается к более раннему параметру x, и аналогично (+1 y) относится к более раннему y необязательному. В этой области языка четко прослеживается предпочтение последовательной видимости в связывании. Было бы менее полезно, если формы в lambda не могли видеть параметры; необязательный параметр не может быть сжато по умолчанию с точки зрения значения предыдущих параметров.

1 голос
/ 29 сентября 2010

С Позволяет использовать параллельное связывание,

(setq my-pi 3.1415)

(let ((my-pi 3) (old-pi my-pi))
     (list my-pi old-pi))
=> (3 3.1415)

И с Let * последовательная привязка,

(setq my-pi 3.1415)

(let* ((my-pi 3) (old-pi my-pi))
     (list my-pi old-pi))
=> (3 3)
1 голос
/ 12 августа 2010

В дополнение к ответу Райнера Йосвига и с пуристской или теоретической точки зрения. Позвольте & Позвольте * представляют две парадигмы программирования; функциональный и последовательный соответственно.

Что касается того, почему я должен просто продолжать использовать Let * вместо Let, ну, вы получаете удовольствие от того, что я возвращаюсь домой и думаю на чистом функциональном языке, в отличие от последовательного языка, где я провожу большую часть своего рабочего дня с:)

0 голосов
/ 25 февраля 2018

При 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 вложения, а не новая существенная абстракция.

0 голосов
/ 24 декабря 2011

кому хочется переписать letf vs letf * снова? количество размотать-защитить звонки?

проще оптимизировать последовательные привязки.

может быть, это повлияет на env ?

позволяет продолжения с динамическим экстентом?

иногда (пусть (x y z) (setq z 0 у 1 x (+ (setq x 1) (prog1 (+ х у) (setq x (1-x))))) (значения ()))

[Я думаю, что это работает]. Дело в том, что проще читать иногда.

0 голосов
/ 20 февраля 2009

Я в основном использую LET, если только мне специально не нужен LET *, но иногда я пишу код, который явно нуждается в LET, обычно при использовании различных (обычно сложных) значений по умолчанию. К сожалению, у меня нет удобного примера кода под рукой.

...