为什么 SBCL(或一般的 Common lisp?)不喜欢我的循环?

Why does SBCL (or Common lisp in general?) dislike my loop?

递归 find-dir-upwards 按预期工作,但 find-dir-upwards-loop 拒绝编译,在 SBCL(Win64、Portacle 1.4、SBCL 2.0.0)下抱怨类型不匹配(即使添加了类型注释) ).

我做错了什么?

(defun find-dir-upwards-loop (dir marker-file)
  (loop for prev = nil then curr
        for curr = dir then (uiop:pathname-parent-directory-pathname curr)
        until (equal curr prev)
        with f = (merge-pathnames marker-file curr)
        when (uiop:file-exists-p f) return curr))

(defun find-dir-upwards (dir marker-file)
  (let ((f (merge-pathnames marker-file dir)))
    (if (uiop:file-exists-p f)
        dir
        (let ((parent (uiop:pathname-parent-directory-pathname dir)))
          (unless (equal dir parent)
            (find-dir-upwards parent marker-file))))))

简化代码

经过一番思考,我意识到您可以将原始功能简化为:

(defun find-dir-upwards (dir marker-file)
  (let ((parent (uiop:pathname-parent-directory-pathname dir)))
    (unless (equal dir parent)
      (if (probe-file (merge-pathnames marker-file dir))
          dir
          (find-dir-upwards parent marker-file)))))

其对应的循环函数为:

(defun find-dir-upwards (dir marker-file)
  (loop for parent = (uiop:pathname-parent-directory-pathname dir)
        unless (equal dir parent)
        do (if (probe-file (merge-pathnames marker-file dir))
               (return dir)
               (setf dir parent))))  

正如@Rezo 在 中指出的那样,probe-file 可以在不同的 Common Lisp 实现之间移植,而 uiop:file-exists-p 则不能。

原回答

我会以不同的方式定义循环。

使用 (let ((dir dir)) (loop ... (setf dir (<some updater function>)))) 习惯用法来获取一些引用变量,这些变量在循环中每 运行 更新一次 - 直到满足条件。

(defun find-dir-upwards (dir marker-file)
  (let ((f (merge-pathnames marker-file dir))
        (dir dir))
    (loop for parent = (uiop:pathname-parent-directory-pathname dir)
          unless (equal dir parent)
          do (if (uiop:file-exists-p f) ;; if marker-file in dir
                 (return dir)           ;; then arrived at destination
                 (progn
                   (setf dir parent)    ;; otherwise climb up one dir-level
                   (setf f (merge-pathnames marker-file dir)))))))

然后进一步分解为:

(defun marker-file-in-dir-p (dir marker-file)
  (uiop:file-exists-p (merge-pathnames marker-file dir)))

(defun find-dir-upwards (dir marker-file)
  (let ((dir dir))
    (loop for parent = (uiop:pathname-parent-directory-pathname dir) ;; parent from dir
          unless (equal dir parent)
          do (if (marker-file-in-dir-p dir marker-file)
                 (return dir)
                 (setf dir parent)))))

将 'FOR' 和 'WITH' 的步骤形式结合起来会产生这个问题。它似乎试图在 'for' 创建 x 之前初始化 'with',但并非总是如此。我还没有解决它,但我以前遇到过这个问题。例如:

(loop for x = 0 then (+ x 1)
      with y = (+  x 1)
      when (> x 10) return (list x y))

Yields:

Value of X in (+ X 1) is NIL, not a NUMBER.
   [Condition of type SIMPLE-TYPE-ERROR]

Restarts:
 0: [RETRY] Retry SLIME REPL evaluation request.
 1: [*ABORT] Return to SLIME's top level.
 2: [ABORT] abort thread (#<THREAD "new-repl-thread" RUNNING {10044CC083}>)

Backtrace:
  0: (SB-C::%COMPILE-TIME-TYPE-ERROR (NIL) NUMBER #<unused argument> (X) "(+ X 1)" NIL)
  1: ((LAMBDA ()))
  2: (SB-INT:SIMPLE-EVAL-IN-LEXENV (LOOP FOR X = 0 THEN ...) #<NULL-LEXENV>)
  3: (EVAL (LOOP FOR X = 0 THEN ...))
 --more--

但是删除步骤形式或 with 似乎工作正常:

CL-USER> (loop for x from 0 to 10
              with y = (+  x 1)
              collect (list x y))
((0 1) (1 1) (2 1) (3 1) (4 1) (5 1) (6 1) (7 1) (8 1) (9 1) (10 1))
CL-USER> (loop for x = 0 then (+ x 1)
              when (> x 10) return x)
11

您不能(可移植地)在 until 之后放置 with。看Hyperspec中的语法:

loop [name-clause] {variable-clause}* {main-clause}* => result*

With是一个variable-clause,但是until是一个termination-test,这是一个main-clause。所有 main-clause 必须在所有 variable-clause 之后出现。

FOR部分答案后的WITH:

(loop for x = 0 then (+ x 1)
      with y = (+  x 1)
      when (> x 10) return (list x y))

这是一系列事件:

  1. 将建立X变量
  2. Y变量将被建立并初始化为(+ x 1)
  3. X 变量将设置为 0
  4. ...

可以看到,在操作1)中,X会是一个变量,但不会初始化为0。这将在 操作 3) 中发生。因此在操作2)X不会被初始化为0->它的值可能是NIL。将 1 添加到 NIL 是一个错误。