В Лиспе более идиоматично использовать let * или setf? - PullRequest
4 голосов
/ 10 октября 2019

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

(let* ((var 1)
       (var (* var 2))
       (var (- var)))
 (format t "var = ~a~%" var))

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

(let ((var 1))
 (setf var (* var 2))
 (setf var (- var))
 (format t "var = ~a~%" var))

(Очевидно, это упрощенный пример, который я обычно объединяю в одну декларацию.)

Я думаю, я просто предпочитаю первую версию, потому что яЯ никогда не мутировал. Он кажется немного более «функциональным» или чистым - и, конечно, потенциально более потоко-безопасным.

Но мне кажется, что последний может быть «более дешевым» с точки зрения выделения памяти или циклов выполнения.

Является ли одна из этих практик более идиоматической, чем другая;или [b] более эффективно с точки зрения использования памяти или тактов?

РЕДАКТИРОВАТЬ: Реальные примеры из моего кода.

(let* ((config-word      (take-multibyte-word data 2 :skip 1))
       (mem-shift-amount ...)
       (config-word      (translate-incoming-data config-word mem-shift-amount blank-value)))

  ...)
(let* ((reserve-byte-count (get-config :reserve-bytes))
       (reserve-byte-count (and (> reserve-byte-count 0) reserve-byte-count))
  ...)
(let* ((licence                (decipher encrypted-licence decryption-key))
       (licence-checksum1      (coerce (take-last 16 licence) '(vector (unsigned-byte 8))))
       (licence                (coerce (drop-last 16 licence) '(vector (unsigned-byte 8))))
       (licence-checksum2      (md5:md5sum-sequence (coerce licence '(vector (unsigned-byte 8)))))
       (licence                (and (equalp licence-checksum1 licence-checksum2)
                                    (decode licence))))
  ...)

Ответы [ 3 ]

3 голосов
/ 10 октября 2019

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

Я должен заранее указать, что мой стиль в Лиспе, вероятно, уникален:я давно написал Lisp и довольно давно перестал заботиться о том, что другие люди думают о моем коде (да, я работаю самостоятельно).

Также обратите внимание, что весь этот ответ является мнением о стиле: ядумаю, что вопросы стиля в программировании интересны и важны, так как программы предназначены для общения с людьми так же, как и для общения с машинами, и что это особенно важно в Лиспе, так как люди Лисп всегда это понимали. Но, тем не менее, мнения о стиле меняются, и на самом деле так.

Я бы нашел форму, подобную

(let* ((var ...)
       (var ...)
       ...)
  ...)

, в лучшем случае неудобную для чтения, потому что она пахнет ошибкой. Альтернатива

(let ((var ...))
  (setf var ...)
  ...)

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

(let* ((var x)
       (var (* var 2))
       (var (- var)))
  ...)

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

(let ((var (- (* x 2))))
  ...)

, которое легче читать и то же самое. Ну, есть случаи, когда вы не можете сделать это так легко:

(let* ((var (f1 x))
       (var (+ (f2 var) (f3 var))))
  ...)

В более общем случае, если значение некоторого промежуточного выражения используется более одного раза, его необходимо связать с чем-то. Соблазнительно сказать, что вы можете превратить вышеприведенную форму в такую:

(let ((var (+ (f2 (f1 x)) (f3 (f1 x)))))
  ...)

Но это небезопасно: если предположить, что f1 дорого, то компилятор может или не сможетобъединить два вызова к f1, но он, безусловно, может сделать это, только если знает , что f1 не имеет побочных эффектов и является детерминированным, и это, вероятно, требует героической стратегии оптимизации. Хуже того, если f1 на самом деле не функция, то это выражение даже не эквивалентно предыдущему:

(let ((x (random 1.0)))
  (f x x))

Не совпадает с

(f (random 1.0) (random 1.0))

Это вернодаже без учета побочных эффектов!

Однако я думаю, что only время, когда вам нужно иметь промежуточные привязки, - это когда значение некоторого подвыражения используется в выражении более одного раза: если оноиспользуется только один раз, тогда вы можете просто соединить его с более поздним выражением, как я делал в вашем примере выше, и я всегда делал бы это, если бы выражения не были действительно глупо большими.

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

(let* ((var (f1 x))
       (var (+ (f2 var) (f3 var))))
  ...)

Я бы превратил это либо в это:

(let* ((y (f1 x))
       (var (+ (f2 y) (f3 y))))
  ...)

Это проблема, связанная с тем, что y связывается в теле без веской причины, поэтомуЯ бы, вероятно, превратил это в это:

(let ((var (let ((y (f1 x)))
             (+ (f2 y) (f3 y)))))
  ...)

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


Один случай, когда я, возможно, согласился бы с идиомой (let* ((var ...) (var ... var ...)) ...), - это сгенерированный макросом код: я мог бы написать макроскоторый сделал это. Но я никогда не ожидал, что мне придется читать этот код.

В случае, когда я никогда не найду (let* ((var ...) (var ...)) ...) (или, если мой макрос successively ниже (successively (var ... ...) ...) приемлемый, допустимо, когда множественные привязки var относятся к семантически различным вещам, свозможное исключение некоторого явно промежуточного выражения с семантически анонимным именем (как, скажем, x в некоторой волосатой арифметике в контексте, где x не означало, естественно, координату). Использование того же имени дляЯ думаю, разные вещи в одном и том же выражении просто ужасны.

В качестве примера я бы взял это выражение из вопроса:

(let* ((licence                (decipher encrypted-licence decryption-key))
       (licence-checksum1      (coerce (take-last 16 licence) '(vector (unsigned-byte 8))))
       (licence                (coerce (drop-last 16 licence) '(vector (unsigned-byte 8))))
       (licence-checksum2      (md5:md5sum-sequence (coerce licence '(vector (unsigned-byte 8)))))
       (licence                (and (equalp licence-checksum1 licence-checksum2)
                                    (decode licence))))
  ...)

и превратил бы его в это выражение

(let* ((licence-with-checksum (decipher encrypted-licence decryption-key))
       (given-checksum (coerce (take-last 16 license-with-checksum)
                               '(vector (unsigned-byte 8))))
       (encoded-license (coerce (drop-last 16 license-with-checksum)
                        '(vector (unsigned-byte 8))))
       (computed-checksum (md5:md5sum-sequence encoded-license))
       (license (if (equalp given-checksum computed-checksum)
                            (decode encoded-license)
                          nil)))
  ...)

(Я также удалил хотя бы один ненужный вызов на coerce, и я бы беспокоился о некоторых других: действительно ли license-with-checksum имеет правильный тип? Если да, то какие take-last и drop-last ошибаетесь, что их результаты нужно снова принуждать?)

Это имеет четыре различных привязки, которые означают четыре разные вещи, которые названы так, как ониean:

  • license-with-checksum - это лицензия с прикрепленной контрольной суммой, которую мы собираемся проверять и декодировать;
  • given-checksum - контрольная сумма, которую мы получили от license-with-checksum
  • encoded-license - это закодированная лицензия от license-with-checksum;
  • computed-checksum - это контрольная сумма, которую мы вычислили из encoded-license;
  • license - это декодированная лицензия, есликонтрольные суммы совпадают, или nil, если они не совпадают.

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

И, конечно,, если license окажется nil, теперь мы можем сообщить об ошибке, используя любую или всю эту информацию.


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

(successively (x y
                 (* x x) 
                 (+ (sin x) (cos x)))
  x)

, то макрос расширится до

(let ((x y))
  (let ((x (* x x)))
    (let ((x (+ (sin x) (cos x))))
      x)))

И даже лучше вы можете сказать:

(successively ((x y) (values a b) (values (+ (sin x) (sin y))
                                          (- (sin x) (sin y))))
  (+ x y))

И вы получите

(multiple-value-bind (x y) (values a b)
  (multiple-value-bind (x y) (values (+ (sin x) (sin y)) (- (sin x) (sin y)))
    (+ x y)))

Что довольно мило.

Вот макрос:

(defmacro successively ((var/s expression &rest expressions) &body decls/forms)
  "Successively bind a variable or variables to the values of one or more
expressions.

VAR/S is either a single variable or a list of variables.  If it is a
single variable then it is successively bound by a nested set of LETs
to the values of the expression and any more expressions.  If it is a
list of variables then the bindings are done with MULTIPLE-VALUE-BIND.

DECLS/FORMS end up in the body of the innermost LET.

You can't interpose declarations between the nested LETs, which is
annoying."
  (unless (or (symbolp var/s)
              (and (listp var/s) (every #'symbolp var/s)))
    (error "variable specification isn't"))
  (etypecase var/s
    (symbol
     (if (null expressions)
         `(let ((,var/s ,expression))
            ,@decls/forms)
       `(let ((,var/s ,expression))
          (successively (,var/s ,@expressions)
            ,@decls/forms))))
    (list
     (if (null expressions)
         `(multiple-value-bind ,var/s ,expression
            ,@decls/forms)
       `(multiple-value-bind ,var/s ,expression
          (successively (,var/s ,@expressions) 
            ,@decls/forms))))))

Я думаю, это показывает, насколько легко адаптировать Lisp под язык, который вы хотите использовать. be: мне понадобилось около пяти минут, чтобы написать этот макрос.

2 голосов
/ 11 октября 2019

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

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

2 голосов
/ 11 октября 2019

Я бы сказал, ничего из вышеперечисленного;но другое:

(let* ((v0 1)
       (v1 (* v0 2))
       (v2 (- v1)))
 (format t "var = ~a~%" v2)

Подставляйте значимые имена по мере необходимости. Хорошие компиляторы Lisp должны иметь возможность удалять временные переменные, потому что макросы обильно генерируют временные переменные в форме gensyms. Часто эти родословные встречаются в тех случаях, когда они на самом деле не нужны;они связаны с чем-то, что не приводит к конфликту имен и не имеет множественных проблем с оценкой.

Например, в следующем расширении вместо #:LIMIT-3374 gensym может безопасно использоваться сам X. ,Множественные оценки X не вызывают проблем, и цикл не видоизменяется X:

[6]> (ext:expand-form '(loop for i below x collect i))
(BLOCK NIL
 (LET ((#:LIMIT-3374 X) (I 0))
  (LET ((#:ACCULIST-VAR-3375 NIL))
   (TAGBODY SYSTEM::BEGIN-LOOP (WHEN (>= I #:LIMIT-3374) (GO SYSTEM::END-LOOP))
    (SETQ #:ACCULIST-VAR-3375 (CONS I #:ACCULIST-VAR-3375)) (PSETQ I (+ I 1))
    (GO SYSTEM::BEGIN-LOOP) SYSTEM::END-LOOP
    (RETURN-FROM NIL (SYSTEM::LIST-NREVERSE #:ACCULIST-VAR-3375)))))) ;

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

Вы экономите только несколько байтов пространства в среде компилятора, не добавляя дополнительные символы, и это только в том случае, если эти символы уже не встречаются где-то еще.

Единственная веская причина в том, что эта переменная является специальной переменной, которую необходимо связать несколько раз, чтобы она появлялась в динамическом объеме функций, вызываемых из выражений init:

(let* ((*context-variable* (foo)) 
       (*context-variable* (bar)))
  ...)

И foo, и bar реагируют на текущее значение переменной *context-variable*;foo вычисляет значение, которое можно использовать как *context-variable*, и мы хотели бы, чтобы bar увидело это значение, а не то, которое foo было выставлено.

Или что-то вроде:

(let* ((*stdout* stream-x)
       (a (init-a)) ;; the output of these goes to stream-x
       (b (frob a))
       (*stdout* stream-y)
       (c (extract-from b)) ;; the output of this goes to stream-y
       ...)
  ...)
...