在 Lisp 中,使用 let* 还是 setf 更惯用?

Sod*_*hty 5 lisp optimization scheme idioms common-lisp

当计算一个值需要多个步骤时,我倾向于使用(let*)声明变量的多个版本,因此:

(let* ((var 1)
       (var (* var 2))
       (var (- var)))
 (format t "var = ~a~%" var))
Run Code Online (Sandbox Code Playgroud)

而不是在其他语言中期望的更命令式的风格:

(let ((var 1))
 (setf var (* var 2))
 (setf var (- var))
 (format t "var = ~a~%" var))
Run Code Online (Sandbox Code Playgroud)

(显然,这是一个过于简单的例子,我通常会将其合并为一个声明。)

我想我只是更喜欢第一个版本,因为我从来没有真正改变状态。它看起来更“实用”或更干净——当然,可能更线程安全。

但在我看来,后者在内存分配或执行周期方面可能“更便宜”。

这些做法中的一种是否[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)))

  ...)
Run Code Online (Sandbox Code Playgroud)
(let* ((reserve-byte-count (get-config :reserve-bytes))
       (reserve-byte-count (and (> reserve-byte-count 0) reserve-byte-count))
  ...)
Run Code Online (Sandbox Code Playgroud)
(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))))
  ...)
Run Code Online (Sandbox Code Playgroud)

小智 3

正如评论中提到的,我根本不会担心性能,直到您知道这是一个问题。假设您知道编译器将如何处理您的代码,或者机器将如何处理编译器生成的代码,这是不安全的。

我应该提前指出​​,我的 Lisp 风格可能很特殊:我已经编写 Lisp 很长时间了,很久以前就不再关心其他人对我的代码的看法(是的,我自己工作)。

另请注意,整个答案是关于风格的意见:我认为编程中的风格问题很有趣且重要,因为程序是与人交流,就像与机器交流一样,这在 Lisp 中尤其重要,因为 Lisp人们一直都明白这一点。但尽管如此,人们对风格的看法仍然存在差异,而且这是合理的。

我会找到一个像这样的表格

(let* ((var ...)
       (var ...)
       ...)
  ...)
Run Code Online (Sandbox Code Playgroud)

读起来很尴尬,因为它闻起来像是一个错误。另一种选择

(let ((var ...))
  (setf var ...)
  ...)
Run Code Online (Sandbox Code Playgroud)

对我来说似乎更诚实,尽管这显然很糟糕(任何作业都会让我抽搐,真的,尽管显然有时需要它,而且我不是某种功能纯粹主义者)。然而,在这两种情况下,我都会问自己为什么要这样做?特别是如果你有这样的事情:

(let* ((var x)
       (var (* var 2))
       (var (- var)))
  ...)
Run Code Online (Sandbox Code Playgroud)

这是你的原始形式,除了我已经绑定,var以便x在编译时不知道整个事情。那么,为什么不只写出你想要表达的意思而不是它的一部分呢?

(let ((var (- (* x 2))))
  ...)
Run Code Online (Sandbox Code Playgroud)

这更容易阅读并且是同一件事。嗯,在某些情况下你不能那么容易做到这一点:

(let* ((var (f1 x))
       (var (+ (f2 var) (f3 var))))
  ...)
Run Code Online (Sandbox Code Playgroud)

更一般地,如果某个中间表达式的值被多次使用,那么它需要绑定到某些东西。你可能会说,好吧,你可以将上面的形式变成这样:

(let ((var (+ (f2 (f1 x)) (f3 (f1 x)))))
  ...)
Run Code Online (Sandbox Code Playgroud)

但这并不安全:如果我们假设这f1是昂贵的,那么编译器可能能够也可能无法合并对 的两个调用f1,但它肯定只能在知道f1是无副作用且确定性的情况下才能这样做,并且可能需要英勇的优化策略。更糟糕的是,如果f1实际上不是一个函数,那么这个表达式甚至不等于前一个:

(let ((x (random 1.0)))
  (f x x))
Run Code Online (Sandbox Code Playgroud)

不一样

(f (random 1.0) (random 1.0))
Run Code Online (Sandbox Code Playgroud)

即使不考虑副作用也是如此!

但是,我认为唯一需要中间绑定的情况是某个子表达式的值在表达式中多次使用:如果它只使用一次,那么您可以将其拼接到后面的表达式中,就像我在您的中所做的那样上面的例子,我总是会这样做,除非表达式真的大得愚蠢。

在子表达式多次使用或者表达式非常复杂的情况下,我要做的就是为中间值使用不同的名称(为它们想出名称并不难)或者只是绑定它在您需要的地方。那么从这里开始:

(let* ((var (f1 x))
       (var (+ (f2 var) (f3 var))))
  ...)
Run Code Online (Sandbox Code Playgroud)

我会把它变成这样:

(let* ((y (f1 x))
       (var (+ (f2 y) (f3 y))))
  ...)
Run Code Online (Sandbox Code Playgroud)

y这有一个无缘由地束缚在体内的问题,所以我可能会把它变成这样:

(let ((var (let ((y (f1 x)))
             (+ (f2 y) (f3 y)))))
  ...)
Run Code Online (Sandbox Code Playgroud)

这仅在需要的地方精确地绑定中间值。我认为它的主要问题是来自命令式编程背景但不太熟悉表达式语言的人发现它很难阅读。但是,正如我所说,我不在乎他们。


我可能会接受这种(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))))
  ...)
Run Code Online (Sandbox Code Playgroud)

并将其变成这个表达式

(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)))
  ...)
Run Code Online (Sandbox Code Playgroud)

(我也删除了至少一个不需要的调用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)
Run Code Online (Sandbox Code Playgroud)

然后宏扩展为

(let ((x y))
  (let ((x (* x x)))
    (let ((x (+ (sin x) (cos x))))
      x)))
Run Code Online (Sandbox Code Playgroud)

更好的是你可以说:

(successively ((x y) (values a b) (values (+ (sin x) (sin y))
                                          (- (sin x) (sin y))))
  (+ x y))
Run Code Online (Sandbox Code Playgroud)

你会得到

(multiple-value-bind (x y) (values a b)
  (multiple-value-bind (x y) (values (+ (sin x) (sin y)) (- (sin x) (sin y)))
    (+ x y)))
Run Code Online (Sandbox Code Playgroud)

这非常好。

这是宏:

(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))))))
Run Code Online (Sandbox Code Playgroud)

我认为这表明将 Lisp 改造成您想要的语言是多么容易:我花了大约五分钟编写这个宏。