代码之家  ›  专栏  ›  技术社区  ›  Alexandru Popa

SBCL中奇怪的宏扩展错误

  •  1
  • Alexandru Popa  · 技术社区  · 6 年前

    我想写一个斐波那契数计算函数 fib 在Lisp中使用SBCL 1.3.3 for Windows x86\u 64。使用延迟计算避免重复。目前的工作准则是:

    (defvar *fibs* (make-hash-table))
    
    (defun get-value (idx)
      (if (functionp (gethash idx *fibs*))
        (setf (gethash idx *fibs*)
          (funcall (gethash idx *fibs*)))
        (gethash idx *fibs*)))
    
    (defun fib (n)
      (loop for i from 0 below n
        if (< i 2) do (setf (gethash i *fibs*) 1)
        else do (setf (gethash i *fibs*)
          (eval `(lambda () (+ (get-value ,(- i 2))
                               (get-value ,(- i 1)))))))
      (get-value (- n 1)))
    

    现在,我不想打电话 eval 在…内 fib公司 ,所以我在这里介绍宏:

    (defvar *fibs* (make-hash-table))
    
    (defun get-value (idx)
      (if (functionp (gethash idx *fibs*))
        (setf (gethash idx *fibs*)
          (funcall (gethash idx *fibs*)))
        (gethash idx *fibs*)))
    
    (defmacro code-for (idx)
      `(lambda () (+ (get-value ,(- idx 2))
                     (get-value ,(- idx 1)))))
    
    (defun fib (n)
      (loop for i from 0 below n
        if (< i 2) do (setf (gethash i *fibs*) 1)
        else do (setf (gethash i *fibs*) (code-for i)))
      (get-value (- n 1)))
    

    但它说:

    ; in: DEFUN FIB
    ;     (CODE-FOR I)
    ; 
    ; caught ERROR:
    ;   during macroexpansion of (CODE-FOR I). Use *BREAK-ON-SIGNALS* to intercept.
    ;   
    ;    Argument X is not a NUMBER: I
    ; 
    ; compilation unit finished
    ;   caught 1 ERROR condition
    

    很奇怪,我没有争论 X 在代码和中 I 始终用作整数。

    经过一些研究,我发现 code-for 宏内部 loop ,以及 的代码 已收到 i 作为符号(?)而不是作为一个数字,这就是抱怨。尽管如此,我仍然不知道为什么代码是错误的,或者如何改进它。

    2018年4月12日编辑。

    正如tfb所指出的,解决问题的最佳方法取决于问题是什么。整个问题是向学生解释什么是懒惰评估,如何在Lisp中进行评估,以及为什么有必要这样做。斐波那契数不是本例的主要目标。

    coredump显示了问题的根本原因(宏扩展)和解决方案(i的额外绑定)。不幸的是,这使得所有代码都不适合showcase,因为有太多额外的解释。所以我最终改变了 通过递归:

    (defvar *fibs* (make-hash-table))
    
    (defun get-value (idx)
      (if (functionp (gethash idx *fibs*))
        (setf (gethash idx *fibs*)
          (funcall (gethash idx *fibs*)))
        (gethash idx *fibs*)))
    
    (defun fib (n &optional (i (- n 1)))
      (if (< i 2) (setf (gethash i *fibs*) 1)
        (setf (gethash i *fibs*)
          (lambda () (+ (get-value (- i 2))
                        (get-value (- i 1))))))
      (if (zerop i) (get-value (- n 1))
        (fib n (- i 1))))
    
    2 回复  |  直到 6 年前
        1
  •  3
  •   user5920214 user5920214    6 年前

    [请注意,coredump的回答解释了宏的错误:我关注的是函数的错误以及解决问题的更好方法。]

    我不知道你为什么认为你需要 EVAL 此处或宏:您可以使用 LAMBDA 。以下是您的代码版本,可以使用:

    (defvar *fibs* (make-hash-table))
    
    (defun get-value (idx &optional (default nil))
      ;; return the value and whether it was there.  If it's a function,
      ;; call it and stash the result
      (multiple-value-bind (got presentp)
          (gethash idx *fibs* default)
        (values
         (typecase got
           (function (setf (gethash idx *fibs*)
                           (funcall got)))
           (t got))
         presentp)))
    
    (defun fib (n)
      (loop for i from 0 below n
            if (< i 2) do (setf (gethash i *fibs*) 1)
            else do (setf (gethash i *fibs*)
                          (let ((i i))
                            ;; rebind I as we don't want to depend on whatever
                            ;; LOOP does, which probably is mutate a single
                            ;; binding of I
                            (lambda () (+ (get-value (- i 2))
                                          (get-value (- i 1)))))))
      ;; just return the first value as we know the second will be T, and
      ;; it's not interesting
      (values (get-value (- n 1))))
    

    但这是一种相当糟糕的方法。相反,您可以只使用显式记忆函数,如下所示:

    (defun fibonacci (n)
      ;; an explicitly-memoized version of the Fibonacci function
      (let ((memo (make-hash-table :test #'eql)))
        (labels ((fib (m)
                   (cond
                    ((< m 1)
                     (error "defined on naturals (excluding 0)"))
                    ((< m 3)
                     1)
                    (t
                     (multiple-value-bind (v p) (gethash m memo)
                       (if p
                           v
                         (setf (gethash m memo) (+ (fib (- m 1))
                                                    (fib (- m 2))))))))))
          (fib n))))
    

    更好的是,定义一个宏,它可以让你记忆任何函数:有一些软件包可以让你这样做,尽管我不确定它们是什么(一个是我的,但我不确定没有更好的,或者实际上是现在找到我的一个的正确位置!)

        2
  •  2
  •   coredump    6 年前

    宏失败的原因

    这是什么 code-for 查看:

    (code-for i)
    

    第一个参数是符号 i 。但是,您试图在宏扩展期间使用此符号执行算术运算。这失败了。

    现在,我不想在fib中调用eval,所以我介绍了宏

    这通常是宏的错误用法。所有宏操作都是代码,无法知道 运行时 值将等于。

    也不 eval 也不需要宏

    您可以轻松构建闭包:

    (lambda () (+ (get-value (- i 2))
                  (get-value (- i 1))))
    

    这里唯一的问题是 lambda关闭的,正在被循环变异。当您最终调用闭包时,其值将不同。您必须建立新的绑定:

    (let ((i i)) (lambda ...))