【问题标题】:How to pass a symbol of a condition of a function to a macro如何将函数条件的符号传递给宏
【发布时间】:2019-03-06 22:32:35
【问题描述】:

我正在尝试将函数条件的符号传递给宏,并查看结果:

(defmacro macro-test-1 (form condition)
  `(handler-case (funcall ,form)
     (,condition (c)
       (declare (ignore c))
       (format t "~a" 'why?))))

(macro-test-1 #'(lambda () (error 'simple-type-error)) division-by-zero)
;; OK, I get the simple-type-error as expected.

(defun test-1 (condition)
  (macro-test-1 #'(lambda () (error 'simple-type-error)) condition))
; in: DEFUN TEST-1
;     (SB-INT:NAMED-LAMBDA TEST-1
;         (CONDITION)
;       (BLOCK TEST-1
;         (MACRO-TEST-1 #'(LAMBDA () (ERROR 'SIMPLE-TYPE-ERROR)) CONDITION)))
; 
; caught STYLE-WARNING:
;   The variable CONDITION is defined but never used.
; 
; compilation unit finished
;   caught 1 STYLE-WARNING condition
TEST-1
;; what happened?

(test-1 'division-by-zero)
WHY?
NIL
;; what happened?

我对发生的事情感到很困惑,我一直在试图弄清楚它,我希望我错过了一些愚蠢的东西。

向上 1

和我想象的一样,愚蠢的错误,现在我意识到我在尝试做什么,宏会在编译时扩展,而我在运行时传递给函数的参数,所以宏不会收到条件论据正确。所以我看到了解决这个问题的两种可能性,将macro-test-1 转换为函数或将test-1 转换为宏。

其实我这里测试过,改成功能还是不行:

CL-USER> (defun macro-test-1 (form condition)
  (handler-case (funcall form)
     (condition (c)
       (declare (ignore c))
       (format t "~a" 'why?))))

; in: DEFUN MACRO-TEST-1
;     (SB-INT:NAMED-LAMBDA MACRO-TEST-1
;         (FORM CONDITION)
;       (BLOCK MACRO-TEST-1
;         (HANDLER-CASE (FUNCALL FORM)
;                       (CONDITION (C) (DECLARE #) (FORMAT T "~a" 'WHY?)))))
; 
; caught STYLE-WARNING:
;   The variable CONDITION is defined but never used.
; 
; compilation unit finished
;   caught 1 STYLE-WARNING condition
WARNING: redefining COMMON-LISP-USER::MACRO-TEST-1 in DEFUN

CL-USER> (macro-test-1 #'(lambda () (error 'simple-type-error)) 'division-by-zero)
WHY?
NIL

但是,当您将 macro-test-1 重新定义为宏,并将 test-1 重新定义为宏时:

CL-USER> (defmacro test-1 (condition)
  `(macro-test-1 #'(lambda () (error 'simple-type-error)) ,condition))
TEST-1

 CL-USER> (test-1 division-by-zero)
; Evaluation aborted on #<SIMPLE-TYPE-ERROR {1001BB8FF3}>.

我仍然不确定为什么函数不起作用,评估规则不是评估所有参数,然后将评估的参数传递给函数体?因为它不起作用?

向上 2

我知道handler-case 不起作用,因为您需要在编译时知道错误,并且将condition 作为运行时函数参数传递将无法知道编译时错误,所以它不工作。我强调这个单一的原因,而不是因为宏的发生具有编译时间,我在下面提到的一个问题导致我陷入了这整个混乱,并让我相信可以通过函数传递condition。我可以这样做:

(defmacro macro-test-1 (fn value)
`(funcall ,fn ,value 1))

(macro-test-1 #'= 1)
;; => T it is OK

(defun test-1 (fn value)
(macro-test-1 fn value))

(test-1 #'= 1)
;; => why it is OK?

上面的代码有效,即使我在运行时将参数传递给函数,为什么它有效?如果宏在编译时展开,为什么当我调用test-1 时它会工作?还是宏在编译时并不总是展开?我在这里错过了什么?

向上 3

我决定更深入,并尝试:

(defmacro macro-test-1 (fn value)
`(,fn ,value 1))

(macro-test-1 = 1)
;; => T it is OK

(defun test-1 (fn value)
  (macro-test-1 fn value))
; in: DEFUN TEST-1                      
;     (SB-INT:NAMED-LAMBDA TEST-1       
;         (FN VALUE)                    
;       (BLOCK TEST-1 (MACRO-TEST-1 FN VALUE)))
;                                       
; caught STYLE-WARNING:                 
;   The variable FN is defined but never used.
; in: DEFUN TEST-1                      
;     (MACRO-TEST-1 FN VALUE)           
; ==>                                   
;   (FN VALUE 1)                        
;                                       
; caught STYLE-WARNING:                 
;   undefined function: FN              
;                                       
; compilation unit finished             
;   Undefined function:                 
;     FN                                
;   caught 2 STYLE-WARNING conditions   
WARNING: redefining COMMON-LISP-USER::TEST-1 in DEFUN
TEST-1

是的,我知道如果我尝试如下所示,它不会按预期退出:

(test-1 '= 1)
; Evaluation aborted on #<UNDEFINED-FUNCTION FN {1004575323}>. ;

但我想找到一种方法让它工作,所以我尝试了,直到我可以,将macro-test-1重置为:

(defmacro macro-test-1 (fn value)
  `(eval (,fn ,value 1)))
WARNING: redefining COMMON-LISP-USER::MACRO-TEST-1 in DEFMACRO
MACRO-TEST-1
(defun test-1 (fn value)
  (macro-test-1 fn value))
WARNING: redefining COMMON-LISP-USER::TEST-1 in DEFUN
TEST-1

(test-1 '= 1)
T

当然这只适用于handler-casecase,如果我重新定义了它的宏,我认为这不应该是一个好的做法,我也不需要它,但我喜欢去它不需要的地方,好吧,那么,我学会了犯错。

【问题讨论】:

    标签: macros common-lisp


    【解决方案1】:

    宏是代码转换。因此,扩展可以早在您评估defun 时发生。例如。

    (defun test-1 (condition)
      (macro-test-1 #'(lambda () (error 'simple-type-error)) condition))
    
    ;; becomes this 
    
    (defun test-1 (condition)
      (handler-case (funcall #'(lambda nil (error 'simple-type-error)))
        (condition (c) 
          (declare (ignore c)) (format t "~a" 'why?))) 
    

    现在假设您希望对simple-type-error 进行处理程序案例检查。你会这样写:

      (handler-case expression
        (simple-type-error () 
          (format t "~a" 'why?))) 
    

    不是

      (handler-case expression
        ('simple-type-error () 
          (format t "~a" 'why?))) 
    

    例如。 handler-case 是语法,该位置不能将变量评估为某些错误,但必须是类型说明符,并且可能由系统在编译时处理。这就是您得到 condition 从未使用过的原因,因为您的 handler-case 检查名为 condition 的类型,而不是您作为 condition 参数发送的类型。

    使test-1 成为宏实际上将division-by-zero 作为符号传递给macro-test-1,结果如下:

    (handler-case (funcall #'(lambda nil (error 'simple-type-error)))
      (division-by-zero (c) 
        (declare (ignore c)) 
        (format t "~a" 'why?)))
    

    这也意味着需要在编译时知道错误,因为您不能让宏在变量中传递值。这就是为什么它起作用的原因,所以当您希望某些用户输入要处理的错误时,您无法使用您的解决方案来做到这一点。

    编辑

    up2 中,您会问为什么会这样:

    (defun test-1 (fn value) 
      (macro-test-1 fn value))
    

    所以我们将找出实际保存的内容:

    (macroexpand-1 '(macro-test-1 fn value))
    ; ==> (funcall fn value)
    ; ==> t
    

    因此你的函数变成了这样:

    (defun test-1 (fn value) 
      (funcall fn value))
    

    handler-case 是在您想要的地方没有使用变量或表达式的语法,这就是为什么它不起作用的原因,但它当然适用于所有函数,包括 funcall,因为它会评估所有函数论据。

    向您展示一个不工作的不同示例是case

    (defun check-something (what result default-value value)
      (case value
        (what result)
        (otherwise default-value)))
    

    case 是一个宏,所以实际发生了什么。我们可以在上面做macroexpand-1看看:

    (macroexpand-1 
      '(case value
        (what result)
        (otherwise default-value))
       )
    ; ==> (let ((tmp value))
    ;       (cond ((eql tmp 'what) result) 
    ;             (t default-value)))
    ; ==> t
    

    宏期望 case 值是文字,因此引用它们,以便它们永远不会被评估。您清楚地看到的结果函数what 从未使用过,就像condition 没有使用一样:

    (defun check-something (what result default-value value)
      (let ((tmp value))
        (cond ((eql tmp 'what) result) 
              (t default-value))))
    

    宏是对语法的抽象。您需要能够在没有宏的情况下编写代码,而是看到这是一种重复的模式,而不是添加从简化版本重写为完整版本的抽象。如果一开始就无法完成,则无法将其重写为宏。

    函数也一样。我们拥有宏的全部原因是为了控制评估。 if

    (defun my-if (predicate consequence alternative)
      (cond (predicate consequence)
            (t alternative)))
    
    (my-if t 'true 'false)   ; ==> true    
    (my-if nil 'true 'false) ; ==> false    
    

    但是由于函数总是评估它们的参数,所以你不能这样做:

    (defun factorial (n)
      (my-if (<= n 1)
             1
             (* n (factorial (1- n)))))
    

    这将永远不会停止,因为作为一个函数,所有 3 个参数总是被评估,并且即使 n 为负数,(* n (factorial (1- n)))) 也会完成,并且它将有无限的递归。相反,使用宏会将my-if 替换为生成的cond,并且condif 都不会评估它们的所有参数,而不是在匹配真实谓词的参数上进行短路。

    您可以使用macroexpand-1 来检查您的代码是否确实正确。您应该能够用我们的输入替换输入。您是否使用 macroexpand 应用扩展,直到它不再扩展。例如。 cond 也将扩展为嵌套的if

    编辑 2

    来自up3

    (defun test-1 (fn value)
      (macro-test-1 fn value))
    

    这是同样的问题。宏函数获取fnvalue 作为绑定,结果是:

    (defun test-1 (fn value)
      (fn value))
    

    这可能在 Scheme 中有效,但在 Common Lisp 中,运算符prosition 中的符号与其他位置不同。因此,当 CL 试图找到函数 fn 时,它永远不会看起来接近变量 fn。解决这个问题的唯一方法是使用funcall,然后你实际上根本不需要宏:

    (defun with-1 (fn value)
      (funcall fn value 1))
    
    (with-1 #'+ 10) ; ==> 11
    

    注意#' 前缀。这是(function ...) 的缩写,所以它实际上是(function +)function 是一种特殊形式,它采用参数符号并从函数命名空间中获取值。

    使用eval,您可以做很多事情,但要付出一定的代价。它不会被优化,甚至可能只是被解释,它可能会让你在运行时编译时出错,并且会面临安全风险。一个很好的例子是一个在线交互式 ruby​​,它刚刚做了eval,它运行良好,直到有人评估删除所有系统文件的代码。 evalconsidered harmful 甚至是邪恶的。在我的职业生涯中,我看到eval 被故意使用了 3 次。 (PHP 2 次,requirejs 1 次)。有一次我向作者提出挑战,说可能有更好的方法来做到这一点。当然,handler-casecase 都可以与 eval 一起使用,因为评估的代码将具有正确的格式,但是您将失去词法范围。例如。

    (let ((x 10))
      (eval '(+ x 1))); 
    ; *** EVAL: variable X has no value
    

    你可能很聪明,会这样做:

    (let ((x 10))
      (eval `(+ ,x 1))) ; ==> 11
    

    但是如果它是一个列表或其他不能自我评估的东西呢?

    (let ((x '(a b)))
      (eval `(cons '1 ,x)))
    ; *** undefined function: a
    

    因此eval 也有其挑战。远离教育以外的其他目的。

    【讨论】:

    • 我更新了我的问题,见up 2,是什么让我感到困惑。
    • 我想我明白了。我玩的多一点,见up 3
    • @PerduGames 我已经更新了答案。除了eval,它当然胜过所有法律,原因是一样的。
    • 很好,我怀疑“eval”不是一个好习惯,因为我来自 javascript,“eval”打开了一些漏洞,和你说的一样。但是“eval”有其安全的用途。
    猜你喜欢
    • 2013-07-26
    • 1970-01-01
    • 1970-01-01
    • 2018-05-16
    • 2016-09-03
    • 2012-12-29
    • 2012-05-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多