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

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 ]

81 голосов
/ 26 февраля 2009

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* сигнализирует программисту (тот, который читает код), что привязки не являются независимыми, но есть некоторый нисходящий поток - который усложняет вещи.

В Common Lisp есть правило, что значения для привязок в LET вычисляются слева направо. Как оцениваются значения для вызова функции - слева направо. Итак, LET является концептуально более простым утверждением, и его следует использовать по умолчанию.

Типы в LOOP? Используются довольно часто. Есть несколько примитивных форм объявления типов, которые легко запомнить. Пример: * +1032 *

(LOOP FOR i FIXNUM BELOW (TRUNCATE n 2) do (something i))

Выше объявляется, что переменная i является fixnum.

Ричард П. Габриэль опубликовал свою книгу о тестах Lisp в 1985 году, и в то время эти тесты также использовались с Lisp без CL. Сам Common Lisp был совершенно новым в 1985 году - книга CLtL1 , в которой описывался язык, была только что опубликована в 1984 году. Неудивительно, что в то время реализации были не очень оптимизированы. Внедренные оптимизации были в основном такими же (или менее), что и предыдущие реализации (например, MacLisp).

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

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

Вам не нужно LET, но вы обычно хотите это.

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

Может быть более эффективно использовать LET, чем LET * (в зависимости от компилятора, оптимизатора и т. Д.):

  • параллельные привязки могут выполняться параллельно (но я не знаю, действительно ли это делают какие-либо системы LISP, и формы init все равно должны выполняться последовательно)
  • параллельные привязки создают единую новую среду (область действия) для всех привязок. Последовательные привязки создают новую вложенную среду для каждой привязки. Параллельные привязки используют меньше памяти и имеют более быстрый поиск переменных .

(вышеуказанные пункты относятся к схеме, другому диалекту LISP. Clisp может отличаться.)

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

Я приду с надуманными примерами. Сравните результат этого:

(print (let ((c 1))
         (let ((c 2)
               (a (+ c 1)))
           a)))

с результатом выполнения этого:

(print (let ((c 1))
         (let* ((c 2)
                (a (+ c 1)))
           a)))
11 голосов
/ 17 февраля 2009

В LISP часто возникает желание использовать самые слабые возможные конструкции. Некоторые руководства по стилю скажут вам использовать = вместо eql, если вы знаете, что сравниваемые элементы, например, числовые. Идея часто состоит в том, чтобы указать, что вы имеете в виду, а не программировать компьютер эффективно.

Однако могут быть реальные улучшения эффективности, когда вы говорите только то, что вы имеете в виду, и не используете более сильные конструкции. Если у вас есть инициализации с LET, они могут выполняться параллельно, в то время как LET* инициализации должны выполняться последовательно. Я не знаю, будут ли какие-либо реализации на самом деле делать это, но некоторые могут хорошо в будущем.

9 голосов
/ 27 февраля 2012

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

(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 на одно и то же значение.

9 голосов
/ 19 февраля 2009

Основное различие в общем списке между LET и LET * состоит в том, что символы в LET связаны параллельно, а в LET * связаны последовательно. Использование LET не позволяет параллельно выполнять init-формы, а также не позволяет изменять порядок init-форм. Причина в том, что Common Lisp позволяет функциям иметь побочные эффекты. Таким образом, порядок оценки важен и всегда слева направо в форме. Таким образом, в LET, init-формы оцениваются сначала слева направо, затем создаются привязки слева направо параллельно. В LET * форма init вычисляется, а затем привязывается к символу в последовательности слева направо.

CLHS: специальный оператор LET, LET *

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

я иду на шаг дальше и использую bind , объединяющий let, let*, multiple-value-bind, destructuring-bind и т. Д., И даже расширяемый.

Как правило, мне нравится использовать "самую слабую конструкцию", но не с let и друзьями, потому что они просто создают шум в коде (предупреждение субъективности! Не нужно пытаться убедить меня в обратном ...)

4 голосов
/ 19 мая 2010
(let ((list (cdr list))
      (pivot (car list)))
  ;quicksort
 )

Конечно, это будет работать:

(let* ((rest (cdr list))
       (pivot (car list)))
  ;quicksort
 )

А это:

(let* ((pivot (car list))
       (list (cdr list)))
  ;quicksort
 )

Но это мысль, которая имеет значение.

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

Предположительно, с помощью let компилятор обладает большей гибкостью для изменения порядка кода, возможно, для увеличения места или скорости.

Стилистически, использование параллельных привязок показывает намерение, что привязки сгруппированы вместе; иногда используется для сохранения динамических привязок:

(let ((*PRINT-LEVEL* *PRINT-LEVEL*)
      (*PRINT-LENGTH* *PRINT-LENGTH*))
  (call-functions that muck with the above dynamic variables)) 
2 голосов
/ 23 февраля 2013

ОП запрашивает "когда-либо действительно нужно 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) не сработает. Но эти макросы довольно небрежны по разным причинам; так что это подрывает аргумент «фактической необходимости».

...