【问题标题】:Conditional variable binding in Common LispCommon Lisp 中的条件变量绑定
【发布时间】:2014-08-04 09:47:42
【问题描述】:

我想执行一个带有 2 个局部变量的函数,但是这些变量的值应该取决于某些条件。例如,假设我有两个变量xy,如果y > x,我想在let 中交换它们。交换应该是临时的,我不想用rotatef 改变状态。我的代码看起来像:

(setq x 2)
(setq y 1)
(let (if (> x y) ((x y) (y x)) ((x x) (y y)))
  (cons x y)) ; should return (1 . 2)

let 中的表达式不是有效的 Lisp。如何有条件地为局部变量赋值?变通的方法是将主体放在flet 中并使用不同的参数调用它,但看起来很笨拙:

(flet ((body (x y) (cons x y)))
  (if (< x y)
      (body x y)
      (body y x)))

【问题讨论】:

  • 你可以随时用PSETF交换它们。

标签: lisp common-lisp conditional-binding


【解决方案1】:

多值绑定和值

有很多选择,其中一些已经在其他答案中指出。我认为标题中的问题(“Common Lisp 中的条件变量绑定”)对于multiple-value-bindvalues 来说是一个很好的例子。我在下面使用了不同的变量名称,只是为了清楚地说明 x 和 y 的位置,以及原始值的来源。但是,名称可以相同;这只是将它们隐藏在里面。

(let ((a 3)
      (b 2))
  (multiple-value-bind (x y)
      (if (< a b)
          (values a b)
          (values b a))
    (cons x y)))
;=> (2 . 3)

然后,使用一点宏观,我们可以让它更干净一点,就像coredump did

(defmacro if-let (test bindings &body body)
  "* Syntax:
let ({var | (var [then-form [else-form]])}*) declaration* form* => result*
* Description: 
Similar to LET, but each binding instead of an init-form can have a
then-form and and else-form.  Both are optional, and default to NIL.
The test is evaluated, then variables are bound to the results of the
then-forms or the else-forms, as by LET."
  (let ((bindings (mapcar #'(lambda (binding)
                              (destructuring-bind (variable &optional then else)
                                  (if (listp binding) binding (list binding))
                                (list variable then else)))
                          bindings)))
    `(multiple-value-bind ,(mapcar 'first bindings)
         (if ,test
             (values ,@(mapcar 'second bindings))
             (values ,@(mapcar 'third bindings)))
       ,@body)))

(pprint (macroexpand-1 '(if-let (< x y) ((x x y)
                                         (y y x))
                         (cons x y))))

; (MULTIPLE-VALUE-BIND (X Y)
;     (IF (< X Y)
;         (VALUES X Y)
;         (VALUES Y X))
;   (CONS X Y))

(let ((a 3) (b 2))
  (if-let (< a b)
      ((x a b)
       (y b a))
    (cons x y)))
;=> (2 . 3)

与progv比较

在使用方面,这和sindikat's answer有一些相似之处,但是multiple-value-bind建立绑定就像let一样:默认是词法,但是全局或局部特殊声明将使绑定动态化。另一方面,progv 建立 动态 绑定。这意味着如果绑定完全由 progv 引入,您不会看到太大的区别(除了尝试返回闭包),但您不能 shadow 绑定.我们完全不需要做任何有条件的工作就可以看到这一点。这是两个示例 sn-ps。在第一个中,我们看到对 x 的内部引用实际上是指词法绑定,而不是 progv 建立的动态绑定。要引用 progv 建立的那个,实际上需要声明内部引用是特殊的。 progv 不接受声明,但我们可以本地使用。

(let ((x 1))
  (progv '(x) '(2)
    x))
;=> 1

(let ((x 1))
  (progv '(x) '(2)
    (locally (declare (special x))
      x)))
;=> 2

multiple-value-bind 实际上按照我们预期的方式进行绑定:

(let ((x 1))
  (multiple-value-bind (x) (values 2)
    x))
;=> 2

最好使用像 multiple-value-bind 这样的绑定结构,它默认建立词法绑定,就像 let 所做的那样。

【讨论】:

  • 请注意,使用包含相同变量的progv 的词法绑定,您不需要funcalllambda 来使progv 中的变量引用词法绑定。 progv always 中的变量指的是词法绑定,除非变量被声明为特殊的。对于progv,强大的力量伴随着巨大的微妙。
  • @m-n 你是对的!我在模仿sindikat's answer,其中没有no 词法绑定,其中内部出现do 获得动态绑定。例如,(progv '(x) '(1) x) =&gt; 1。您提出的观点非常重要,因为这意味着 progv 实际上不会像答案所期望的那样工作,因为(let ((x 'a)) (progv '(x) '(1) x)) =&gt; A
  • @m-n 我已经更新了我的答案以纳入您提出的观点(这比我提出的观点更强大,并且更容易证明)。感谢您的宝贵意见!
【解决方案2】:

如果你不想使用progv,正如 sindikat 所提到的,你总是可以这样写:

(defmacro let-if (if-condition then-bindings else-bindings &body body)
  `(if ,if-condition
     (let ,then-bindings
       ,@body)
     (let ,else-bindings
       ,@body)))

这样的表达方式

(let-if (> x y) ((x y) (y x)) ((x x) (y y))
       (cons x y))

将扩展为:

(IF (> X Y)
(LET ((X Y) (Y X))
  (CONS X Y))
(LET ((X X) (Y Y))
  (CONS X Y)))

【讨论】:

  • 我喜欢这个,并在my answer 的多值绑定之上提供了一个有点相似的基于宏的版本。请注意,这有一个缺点或一个特点:缺点是您可能必须重复变量名称(一次在 then-list 中,然后在 else-list 中);特点是您可以为 then 和 else 情况指定不同的变量来绑定(功能 a 或错误 :))。在我的回答中,我展示了一种方法,其中变量名称只出现一次,因此不可能忘记任何名称,但它消除了一些灵活性。
【解决方案3】:

rotatef

怎么样:

CL-USER> (defvar x 2)
X
CL-USER> (defvar y 1)
Y
CL-USER> (let ((x x)    ; these variables shadow previously defined
               (y y))   ; X and Y in body of LET
           (when (> x y)
             (rotatef x y))
           (cons x y))
(1 . 2)
CL-USER> x              ; here the original variables are intact
2                       ; ^
CL-USER> y              ; ^
1                       ; ^

但是,我认为在每一个这样的实际案例中,都有更简洁的方法来解决没有宏的问题。从功能的角度来看,Answer by msandiford 可能是最好的。

psetf

虽然rotatef 是一种非常有效的方法(它可能会被编译成大约三个机器指令交换内存中的指针),但它并不通用。

Rainer Joswing posted just a great solution as a comment 在发布问题后不久。遗憾的是,我在几分钟前检查了宏 psetf,这应该是非常有效且通用的解决方案。

psetf 首先评估其偶数参数,然后将评估值分配给奇数位置的变量,就像 setf 所做的那样。

所以我们可以这样写:

(let ((x x)
      (y y))
  (when (> x y)
    (psetf x y y x))
  ...)

就是这样,一个人可以有条件地将任何东西重新绑定到任何东西。我认为这比使用宏要好得多。因为:

  • 我认为这种情况并不常见;
  • 发布的答案中的一些宏重复了它们的主体代码,这可能非常大:因此您会获得更大的编译文件(使用宏的价格合理,但在这种情况下不是);
  • 每个自定义宏都会让其他人更难理解代码。

【讨论】:

  • 明确地说,这并不是真的有条件地重新绑定任何东西;这是重新绑定到相同的值,然后有条件地分配。这在 OP 的情况下肯定会起作用,但并不完全相同。如果原始值昂贵或有副作用,这也不是一个好的选择。这与 OP 的帖子有点不同,但请考虑 (if-let test ((x (f1) (f2))) …) 是否扩展为 (let ((x (f1))) (when test (setf x (f2))) …)。如果test 为真,那么both f1f2 都会被调用。这可能不是一件好事。
  • @Joshua,你是对的,它正在分配。虽然它并不总是一个好的选择,但有时它是获得你想要的东西的非常有效和简单的方法。但是,您的答案更正确。
【解决方案4】:

一种解决方案是使用progv 而不是let,它的第一个参数是要绑定值的符号列表,第二个参数是值列表,其余是正文。

(progv '(x y) (if (< x y) (list x y) (list y x))
  (cons x y)) ; outputs (1 . 2)

【讨论】:

  • Progv 只建立特殊的(即动态范围的)绑定。对于这个特定的示例,我们不会看到差异,但是如果返回任何闭包,或者调用另一个使用特殊变量的函数,则返回可能会令人惊讶。我使用了类似的方法与多值绑定(默认情况下按词法绑定,就像 let 一样)并添加了与my answer 的比较。
  • @sindikat progv 的优点是它可以计算出运行时要绑定哪些变量。考虑到这一点,(progv (quote ...) ...) 是您不需要它的信号。 progv 的一个缺点是编译器不会确定它将绑定哪些变量。这有两个后果:第一个,很明显,一个是您可能会在某些用途下看到未绑定的变量警告。但是,隐藏的后果会很严重:如果xy 是您的sn-p 中的词法变量,那么(cons x y) 将指代词法(外部)绑定,而不是progv 引入的动态绑定!
  • (let ((x 1)) (progv '(x) '(10) x)) -> 1,而不是 10,除非您已将 x 声明为全局特殊的。
【解决方案5】:

另一种选择可能是:

(let ((x (min x y))
      (y (max x y)))
  (cons x y))

【讨论】:

  • 这适用于这种特殊情况,但它并没有真正为标题“Common Lisp 中的条件变量绑定”中的问题提供一般解决方案。它不容易扩展到两个以上的变量,并且最终会执行两次基本相同的比较。
  • 一般情况的扩展是用适合您情况的选择器替换minmax。我的观点是,并不总是需要计算整个绑定列表,只需计算值即可。
  • 可以这么说,如果选择器是正交的或独立的,这不是一个坏方法。如果它们是相关的,这(就像在最小/最大情况下)仍然会导致冗余计算。例如,如果你做了(let ((x (first l)) (y (second l)) (z (third l))) …),你最终会遍历l 很多次,而(destructuring-bind (x y z) l …) 会(希望更好)。作为一个人为的例子,(destructuring-bind (x y z) (sort l '&lt;) …) 会比(let ((x (first (sort l '&lt;))) (y (second (sort l '&lt;))) (z (third (sort l '&lt;)))) …) 更好。
【解决方案6】:

我的建议是destructuring-bindmultiple-value-bind 之一。

如果您预计需要经常执行此操作,我建议您使用宏来生成绑定。我提供了一个可能的宏(未经测试)。

(defmacro cond-let (test-expr var-bindings &body body)
  "Execute BODY with the VAR-BINDINGS in place, with the bound values depending on 
   the trueness of TEST-EXPR.

   VAR-BINDINGS is a list of (<var> <true-value> <false-value>) with missing values 
   being replaced by NIL."

  (let ((var-list (mapcar #'car var-bindings))
        (then-values (mapcar #'(lambda (l)
                                 (when (cdr l) 
                                   (nth 1 l)))
                             var-bindings))
        (else-values (mapcar #'(lambda (l)
                                 (when (cddr l))
                                    (nth 2 l)))
                             var-bindings))
     `(destructuring-bind ,var-list
         (if ,test-expr
             (list ,@then-values)
           (list ,@else-values)))))

【讨论】:

  • 我在my answer 中使用了非常相似的方法。请注意,您不需要使用(when (cdr l) …)(when (cddr l) …) 进行防护;当列表不够“大”时,这些函数返回 nil。 (与我的回答中的第一个、第二个和第三个相同。)在这里使用 destructuring-bind 有点危险,因为它会进入子列表。因此,您可以这样做,例如,(cond-let test ( ((a b &amp;rest c) (list 1 2 3 4 5) (list 6 7 8 9 0))) (list a b c)) 并得到 (1 2 (3 4 5))(6 7 (8 9 0)),这可能不是预期的。
  • 这也不允许像 LET 那样不在列表中的变量。例如,使用 LET,您可以执行 (let (x (y 2)) …) 并将 x 绑定到 nil。
  • @JoshuaTaylor 几乎每种方法都存在问题。实际上,对于具体问题,只需执行(let ((x (min x y)) (y max x y))) ...) 就可以解决问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-21
  • 2011-04-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多