正如评论中所提到的,除非你知道这是个问题,否则不要担心性能。不能安全地假设你知道编译器会如何处理你的代码,或者机器将如何处理编译器生成的代码。
我应该提前指出我的Lisp风格可能很特别:我写了很长时间的Lisp,相当早就不再关心其他人对我的代码有什么看法(是的,我独自工作)。
还要注意,这整个答案都是关于风格的观点:我认为在编程中风格问题很有趣也很重要,因为程序不仅是与机器交流,也是与人类交流,而且在Lisp中尤为重要,因为Lisp的人们一直以来都明白这一点。但是,关于风格的观点是存在差异的,这是合理的。
我会选择像下面这样的形式:
(let* ((var ...)
(var ...)
...)
...)
最好情况下,这看起来很别扭,因为它像个错误。 另一种选择是
(let ((var ...))
(setf var ...)
...)
在我看来,这更加诚实,尽管它显然很卑琐(任何任务都会让我感到紧张,虽然有时候显然是必需的,而我不是某种功利主义者)。但是,在这两种情况下,我都会问自己为什么要这样做?特别是如果你有像这样的东西:
(let* ((var x)
(var
这是您的原始格式,除了我将var
绑定到x
以使整个表达式在编译时不被知晓。那么,为什么不直接写出您想要的表达式,而不是其中的一部分呢?
(let ((var (-
更易于阅读且含义相同。虽然在某些情况下,您可能无法轻松实现这一点:
(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))
即使不考虑副作用,这也是真实的!
但是我认为唯一需要有中间绑定的情况是某个子表达式的值在表达式中被使用多次:如果它只被使用一次,那么你可以像我在你上面的示例中所做的那样将其拼接到后面的表达式中,并且我会始终这样做,除非表达式特别难懂。
而且,在子表达式被使用多次或表达式特别复杂的情况下,我会为中间值使用不同的名称(想出名称并不难),或者在需要时将其绑定。 因此,从这里开始:
(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 ... ...) ...)
,除非多个变量的绑定涉及到语义上不同的内容,可能有一个明显为中间值的表达式用一个语义匿名的名称引用它(例如,在某些复杂算术运算中的 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
有什么问题,导致它们的结果需要再次强制转换?
这段代码有四个不同的绑定,代表了四种不同的含义,并以其含义命名:
-
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适应为您想要的语言有多么容易:编写此宏只需大约五分钟。
let*
子句中的计算包含自由变量,那么与内存相关的事情是否会改变。那么setf
将更加内存高效吗?编译器真的会优化掉所有东西吗? - Gwang-Jin Kim