在 Lisp 中,使用 let* 还是 setf 更惯用?
In Lisp, is it more idiomatic to use let* or setf?
当一个值的计算需要多个步骤时,我倾向于使用(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))
(显然这是一个过于简单化的示例,我通常会将其组合成一个声明。)
我想我只是更喜欢第一个版本,因为我从来没有真正改变状态。它似乎更 "functional" 或更干净 - 当然,可能更线程安全。
但我想到后者在内存分配或执行周期方面可能 "cheaper"。
这些做法中的一种 [a] 比另一种更惯用吗?或 [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))))
...)
如评论中所述,在您知道这是一个问题之前,我根本不会担心性能。假设您知道编译器将如何处理您的代码,或者机器将如何处理编译器生成的代码是不安全的。
我应该提前指出我的 Lisp 风格可能是特殊的:我写 Lisp 已经很长时间了,很久以前就不再关心别人对我的代码的看法(是的,我自己工作) .
另请注意,整个答案都是关于风格的观点:我认为编程中的风格问题很有趣也很重要,因为程序既要与人交流,也要与机器交流,这尤其重要在 Lisp 中很重要,因为 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))
即使不考虑副作用也是如此!
但是我认为 只有 需要中间绑定的时间是某些子表达式的值在表达式中多次使用的地方:如果它只使用一次,然后你可以像我在上面的例子中那样将它拼接到后面的表达式中,我会一直这样做,除非表达式真的非常大。
并且在子表达式被多次使用或者表达式非常复杂的情况下,我会做的是为中间值使用不同的名称(为它们想出名称并不难) 或 只需将它绑定到您需要的地方。所以从这里开始:
(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
误认为他们的结果需要再次强制转换?)
这有四种不同的绑定,表示四种不同的事物,它们以它们的含义命名:
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 改编为您想要的语言是多么容易:我花了大约五分钟来编写这个宏。
我会说,上面的none;但 rther:
(let* ((v0 1)
(v1 (* v0 2))
(v2 (- v1)))
(format t "var = ~a~%" v2)
根据需要替换有意义的名称。好的 Lisp 编译器必须能够消除临时变量,因为宏会大量生成 gensyms 形式的临时变量。通常,这些 gensym 出现在实际上不需要它们的情况下;他们绑定到不会导致任何名称冲突并且没有多重评估问题的东西。
例如,在下面的扩展中,X
本身可以安全地用来代替 #:LIMIT-3374
gensym。 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)))))) ;
很少有充分的理由在同一构造中绑定相同的名称两次,即使在该类型的构造中允许这样做。
通过不驻留额外的符号,您只是在编译器的环境中节省了几个字节的 space,而且只有当这些符号没有出现在其他地方时才会这样。
一个很好的理由是涉及的变量是一个特殊变量,必须多次绑定才能出现在从 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
...)
...)
我会为中间步骤使用有意义的名称,或者,如果没有合理的名称出现,则使用线程宏(例如来自 arrows
(无耻插件))。我通常不会 setf
局部变量,但这主要是一种风格选择。 (我有时会在局部变量中 contained 结构上使用 setf
以避免 consing。)
有些编译器更擅长编译本地 let
绑定而不是 setf
修改,但这可能不是选择其中之一的最佳理由。
当一个值的计算需要多个步骤时,我倾向于使用(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))
(显然这是一个过于简单化的示例,我通常会将其组合成一个声明。)
我想我只是更喜欢第一个版本,因为我从来没有真正改变状态。它似乎更 "functional" 或更干净 - 当然,可能更线程安全。
但我想到后者在内存分配或执行周期方面可能 "cheaper"。
这些做法中的一种 [a] 比另一种更惯用吗?或 [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))))
...)
如评论中所述,在您知道这是一个问题之前,我根本不会担心性能。假设您知道编译器将如何处理您的代码,或者机器将如何处理编译器生成的代码是不安全的。
我应该提前指出我的 Lisp 风格可能是特殊的:我写 Lisp 已经很长时间了,很久以前就不再关心别人对我的代码的看法(是的,我自己工作) .
另请注意,整个答案都是关于风格的观点:我认为编程中的风格问题很有趣也很重要,因为程序既要与人交流,也要与机器交流,这尤其重要在 Lisp 中很重要,因为 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))
即使不考虑副作用也是如此!
但是我认为 只有 需要中间绑定的时间是某些子表达式的值在表达式中多次使用的地方:如果它只使用一次,然后你可以像我在上面的例子中那样将它拼接到后面的表达式中,我会一直这样做,除非表达式真的非常大。
并且在子表达式被多次使用或者表达式非常复杂的情况下,我会做的是为中间值使用不同的名称(为它们想出名称并不难) 或 只需将它绑定到您需要的地方。所以从这里开始:
(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
误认为他们的结果需要再次强制转换?)
这有四种不同的绑定,表示四种不同的事物,它们以它们的含义命名:
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 改编为您想要的语言是多么容易:我花了大约五分钟来编写这个宏。
我会说,上面的none;但 rther:
(let* ((v0 1)
(v1 (* v0 2))
(v2 (- v1)))
(format t "var = ~a~%" v2)
根据需要替换有意义的名称。好的 Lisp 编译器必须能够消除临时变量,因为宏会大量生成 gensyms 形式的临时变量。通常,这些 gensym 出现在实际上不需要它们的情况下;他们绑定到不会导致任何名称冲突并且没有多重评估问题的东西。
例如,在下面的扩展中,X
本身可以安全地用来代替 #:LIMIT-3374
gensym。 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)))))) ;
很少有充分的理由在同一构造中绑定相同的名称两次,即使在该类型的构造中允许这样做。
通过不驻留额外的符号,您只是在编译器的环境中节省了几个字节的 space,而且只有当这些符号没有出现在其他地方时才会这样。
一个很好的理由是涉及的变量是一个特殊变量,必须多次绑定才能出现在从 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
...)
...)
我会为中间步骤使用有意义的名称,或者,如果没有合理的名称出现,则使用线程宏(例如来自 arrows
(无耻插件))。我通常不会 setf
局部变量,但这主要是一种风格选择。 (我有时会在局部变量中 contained 结构上使用 setf
以避免 consing。)
有些编译器更擅长编译本地 let
绑定而不是 setf
修改,但这可能不是选择其中之一的最佳理由。