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)))))   (значения()))

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