LET против LET * в Common Lisp
Я понимаю разницу между LET и LET * (параллельная или последовательная привязка), и в качестве теоретического вопроса это имеет смысл. Но есть ли случаи, когда вы когда-либо нуждались в LET? Во всем моем коде Lisp, который я недавно просмотрел, вы можете заменить каждый LET с помощью LET * без изменений.
Изменить: Хорошо, я понимаю, почему какой-то парень изобрел LET *, предположительно как макрос, назад. Мой вопрос заключается в том, что LET * существует, есть ли причина, по которой LET может остаться? Вы написали какой-либо фактический код Lisp, где LET * не будет работать, а также простой LET?
Я не покупаю аргумент эффективности. Во-первых, распознавание случаев, когда LET * можно скомпилировать во что-то столь же эффективное, как LET, просто не кажется таким трудным. Во-вторых, в спецификации CL есть много вещей, которые просто не кажутся такими, что они были разработаны вокруг эффективности вообще. (Когда в последний раз вы видели LOOP с объявлениями типа? Те, кого так трудно понять, я никогда не видел, чтобы они использовались.) До тестов Дика Габриэля конца 1980-х годов CL был довольно медленным.
Похоже, что это еще один случай обратной совместимости: разумно, никто не хотел рисковать сломать что-то столь же фундаментальное, как LET. Это была моя догадка, но мне было приятно услышать, что у кого-то есть глупо-простой случай, который мне не хватало, где LET делал кучу вещей смехотворно легче, чем LET *.
Ответы
Ответ 1
LET
сам по себе не является реальным примитивным языком функционального программирования, так как он может быть заменен на LAMBDA
. Вот так:
(let ((a1 b1) (a2 b2) ... (an bn))
(some-code a1 a2 ... an))
похож на
((lambda (a1 a2 ... an)
(some-code a1 a2 ... an))
b1 b2 ... bn)
Но
(let* ((a1 b1) (a2 b2) ... (an bn))
(some-code a1 a2 ... an))
похож на
((lambda (a1)
((lambda (a2)
...
((lambda (an)
(some-code a1 a2 ... an))
bn))
b2))
b1)
Вы можете себе представить, какая из них более простая. LET
, а не LET*
.
LET
упрощает понимание кода. Вы видите связку привязок, и каждый может прочитывать каждую привязку индивидуально, без необходимости понимать поток "эффектов" сверху вниз/влево-вправо. Используя LET*
сигналы программисту (тот, который читает код), что привязки не являются независимыми, но есть какой-то поток сверху вниз, что усложняет ситуацию.
Общее Lisp имеет правило, что значения для привязок в LET
вычисляются слева направо. Как оцениваются значения для вызова функции - слева направо. Таким образом, LET
- концептуально более простой оператор, и он должен использоваться по умолчанию.
Типы в LOOP
? Используются довольно часто. Есть некоторые примитивные формы объявления типа, которые легко запомнить. Пример:
(LOOP FOR i FIXNUM BELOW (TRUNCATE n 2) do (something i))
Выше объявляется переменная i
равной fixnum
.
Ричард П. Габриэль опубликовал свою книгу в тестах Lisp в 1985 году, и в то время эти тесты также использовались с не-CL Lisps. Сам общий Lisp был совершенно новым в 1985 году - книга CLtL1, в которой описывается, что язык был только что опубликован в 1984 году. Неудивительно, что в то время реализации не были очень оптимизированы. Реализованные оптимизации были в основном одинаковыми (или менее), которые были реализованы ранее (например, MacLisp).
Но для LET
vs. LET*
основное отличие состоит в том, что код, использующий LET
, легче понять для людей, поскольку предложения привязки независимы друг от друга - тем более, что это плохой стиль, чтобы воспользоваться преимуществами оценка слева направо (не задавая переменные как побочный эффект).
Ответ 2
Вам не нужно LET, но вы обычно этого хотите.
LET предполагает, что вы просто выполняете стандартную параллельную привязку, при этом ничего сложного не происходит. LET * вызывает ограничения на компилятор и предлагает пользователю, что есть причина, по которой необходимы последовательные привязки. С точки зрения стиля LET лучше, если вам не нужны дополнительные ограничения, наложенные LET *.
Лучше использовать LET, чем LET * (в зависимости от компилятора, оптимизатора и т.д.):
- параллельные привязки могут быть выполняться параллельно (но я не знаю, действительно ли какие-либо системы LISP это делают, а формы init должны выполняться последовательно)
- параллельные привязки создают единственную новую среду (область) для всех привязок. Последовательные привязки создают новую вложенную среду для каждой привязки. Параллельные привязки используют меньше памяти и имеют более быстрый поиск переменных.
(Указанные выше маркерные точки относятся к Scheme, другому диалогу LISP. clisp может отличаться.)
Ответ 3
Я прихожу с надуманными примерами. Сравните результат:
(print (let ((c 1))
(let ((c 2)
(a (+ c 1)))
a)))
с результатом выполнения этого:
(print (let ((c 1))
(let* ((c 2)
(a (+ c 1)))
a)))
Ответ 4
В LISP часто возникает желание использовать самые слабые возможные конструкции. В некоторых руководствах по стилям вам будет предложено использовать =
, а не eql
, если вы знаете, например, что сравниваемые элементы являются числовыми. Идея часто заключается в том, чтобы указать, что вы имеете в виду, а не эффективно программировать компьютер.
Однако могут быть реальные улучшения эффективности, говоря только о том, что вы имеете в виду, и не используете более сильные конструкции. Если у вас есть инициализации с помощью LET
, они могут выполняться параллельно, а инициализация LET*
должна выполняться последовательно. Я не знаю, будут ли какие-либо реализации на самом деле делать это, но некоторые могут в будущем.
Ответ 5
Основное отличие в общем списке между LET и LET * заключается в том, что символы в LET связаны параллельно, а в LET * связаны последовательно. Использование LET не позволяет выполнять инициализационные формы параллельно, а также не позволяет изменять порядок инициализаций. Причина в том, что Common Lisp позволяет функциям иметь побочные эффекты. Поэтому порядок оценки важен и всегда остается слева направо в форме. Таким образом, в LET начальные формы сначала оцениваются слева направо, а затем создаются привязки слева направо. В LET * начальная форма оценивается и затем привязывается к символу в последовательности слева направо.
CLHS: Специальный оператор LET, LET *
Ответ 6
Недавно я написал функцию из двух аргументов, где алгоритм выражен наиболее четко, если мы знаем, какой аргумент больше.
(defun foo (a b)
(let ((a (max a b))
(b (min a b)))
; here we know b is not larger
...)
; we can use the original identities of a and b here
; (perhaps to determine the order of the results)
...)
Предположим, что b
было больше, если бы мы использовали let*
, мы бы случайно установили a
и b
на одно и то же значение.
Ответ 7
i сделайте еще один шаг и используйте bind, который объединяет let, let, let, multiple-value-bind, destructuring-bind и т.д., и он даже расширяемый.
Обычно мне нравится использовать "самую слабую конструкцию", но не с let и friends, потому что они просто дают шум коду (предупреждение субъективности! нет необходимости пытаться убедить меня в обратном...)
Ответ 8
Предположительно, используя let
, у компилятора больше гибкости для изменения порядка кода, возможно, для улучшения пространства или скорости.
Стилистически, используя параллельные привязки, показано, что привязки группируются вместе; это иногда используется для сохранения динамических привязок:
(let ((*PRINT-LEVEL* *PRINT-LEVEL*)
(*PRINT-LENGTH* *PRINT-LENGTH*))
(call-functions that muck with the above dynamic variables))
Ответ 9
(let ((list (cdr list))
(pivot (car list)))
;quicksort
)
Конечно, это сработало бы:
(let* ((rest (cdr list))
(pivot (car list)))
;quicksort
)
И это:
(let* ((pivot (car list))
(list (cdr list)))
;quicksort
)
Но это мысль, которая имеет значение.
Ответ 10
OP запрашивает "когда-либо действительно необходимый LET"?
Когда был создан Common Lisp, была загружена лодка существующего кода Lisp в разных диалектах. Вкратце, люди, которые разработали Common Lisp, приняли решение создать диалект Lisp, который обеспечил бы общую основу. Они "необходимы", чтобы упростить и привлекать порт существующего кода в Common Lisp. Выход из LET или LET * из языка, возможно, послужил другим достоинствам, но он проигнорировал бы эту ключевую цель.
Я использую LET, предпочитая LET *, потому что он сообщает читателю что-то о том, как разворачивается поток данных. В моем коде, по крайней мере, если вы видите LET *, вы знаете, что ранжированные значения будут использоваться в более позднем связывании. "Мне" нужно это сделать, нет; но я думаю, что это полезно. Тем не менее, я редко читал код, который по умолчанию имеет значение LET * и появление LET-сигналов, которые автор действительно этого хотел. То есть например, чтобы поменять смысл двух варов.
(let ((good bad)
(bad good)
...)
Существует спорный сценарий, который подходит к "реальной необходимости". Он возникает с помощью макросов. Этот макрос:
(defmacro M1 (a b c)
`(let ((a ,a)
(b ,b)
(c ,c))
(f a b c)))
работает лучше, чем
(defmacro M2 (a b c)
`(let* ((a ,a)
(b ,b)
(c ,c))
(f a b c)))
так как (M2 c b a) не будет работать. Но эти макросы довольно неряшливы по разным причинам; так что подрывает аргумент "актуальной необходимости".
Ответ 11
В дополнение к ответу Райнера Йосвига и с пуристской или теоретической точки зрения. Пусть и пусть * представляют две парадигмы программирования; функциональный и последовательный соответственно.
Из-за того, почему я должен просто использовать Let * вместо Let, ну, вы отвлекаетесь от меня, возвращаясь домой и думая о чистом функциональном языке, в отличие от последовательного языка, где я большую часть своего рабочего дня с:)
Ответ 12
С помощью использования параллельной привязки
(setq my-pi 3.1415)
(let ((my-pi 3) (old-pi my-pi))
(list my-pi old-pi))
=> (3 3.1415)
И с Let * serial binding,
(setq my-pi 3.1415)
(let* ((my-pi 3) (old-pi my-pi))
(list my-pi old-pi))
=> (3 3)
Ответ 13
Оператор let
вводит единую среду для всех привязок, которые она задает. let*
, по крайней мере, концептуально (и если игнорировать объявления на мгновение) вводит несколько сред:
То есть:
(let* (a b c) ...)
выглядит следующим образом:
(let (a) (let (b) (let (c) ...)))
Итак, в некотором смысле let
более примитивен, тогда как let*
является синтаксическим сахаром для записи каскада let
-s.
Но неважно. Факт - это два оператора, а в "99%" кода не имеет значения, какой из них вы используете. Причиной предпочтения let
над let*
является просто то, что в конце нет обрыва *
.
Всякий раз, когда у вас есть два оператора, и у него есть *
зависание на его имя, используйте тот, у которого есть *
, если он работает в этой ситуации, чтобы ваш код был менее уродливым.
Это все, что нужно.
Как я уже сказал, я подозреваю, что если let
и let*
обменялись своими значениями, то, вероятно, было бы более редко использовать let*
, чем сейчас. Поведение последовательного связывания не будет беспокоить большинство кодов, которые этого не требуют: редко нужно было использовать let*
для запроса параллельного поведения (и такие ситуации также могут быть исправлены путем переименования переменных, чтобы избежать затенения).
Ответ 14
В основном я использую LET, если мне не требуется LET *, но иногда я пишу код, явно требующий LET, обычно при выполнении сложного (обычно сложного) дефолта. К сожалению, у меня нет подходящего примера кода.
Ответ 15
кто чувствует, как снова переписывать letf vs letf *? количество звонков, защищенных от разговора?
проще оптимизировать последовательные привязки.
возможно, это влияет на env?
позволяет продолжить с динамической протяжённостью?
иногда (пусть (x y z) (setq z 0 y 1 x (+ (setq x 1) (prog1 (+ x y) (setq x (1- x))))) (значения()))
[Я думаю, что это работает], проще, проще читать иногда.