Как уже упоминалось в комментарии, я не стал бы беспокоиться о производительности, пока вы не узнаете, что это проблема. Не безопасно предполагать, что вы знаете, что компиляторы будут делать с вашим кодом, или что машина будет делать с кодом, который генерирует компилятор.
Я должен заранее указать, что мой стиль в Лиспе, вероятно, уникален:я давно написал 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: мне понадобилось около пяти минут, чтобы написать этот макрос.