被 Lisp 列表比较搞糊涂了

Confused by Lisp list comparison

下面是我的 Lisp 程序,其中比较了列表和列表。一个是根据用户输入生成的,一个是预先生成的 table 的一部分。我单独测试了设置输入的功能;在那里,(isValidMove '(0 0)) returns T, 但是当使用 query-io 构建比较列表时,我得到了错误。在任何语言中,比较总是给我带来麻烦,因为有多少不同的东西对我来说是相同的,但对计算机来说却明显不同;我认为这与我在这里遇到的问题相同。 (顺便说一下,我只包含了一个更大程序的一部分)。

;Local Variables (program wide)
;Board values stores x/o and defaults to " "
(setf boardValues (make-array '(3 3)
    :initial-element " ")
);end boardValues

;List of all valid moves remaining 
(setf validMoves (list 
    (list 0 0) (list 0 1) (list 0 2) 
    (list 1 0) (list 1 1) (list 1 2) 
    (list 2 0) (list 2 1) (list 2 2)))

;Functions

;Function call the will prompt the user for input, if the move is
;not vaild, repromts for a move
(defun getUserMove ()

    (let ((move (read-line *query-io*)))
    (if (isValidMove move)
        (progn
            (setf (aref validMoves (car move) (cdr move)) 'x)
            (remove move validMoves))
            (getUserMove)))
);end getUserMove

;Function call that process the move, returns T if move is valid
;and F if move is invalid

(defun isValidMove (move)
    (dolist (m validMoves) 
    (if (equalp m move)
        (return T)))
) ;end isValidMove

GETUSERMOVE 中,您将 MOVE 绑定为计算 READ-LINE 的结果。 READ-LINE returns 一个 字符串 而不是 列表 。 (参考:http://www.lispworks.com/documentation/HyperSpec/Body/f_rd_lin.htm

因此,ISVALIDMOVE 永远不会计算为 T,因为字符串永远不会 EQUALP 到列表。

要将您的字符串转换为列表,您需要调用 EVAL。但对用户输入的数据使用 EVAL 时要小心!

让我们通过代码修复问题:

(setf boardValues (make-array '(3 3)
    :initial-element " ")
)

不要使用SETF声明全局变量。使用 DEFVARDEFPARAMETER。全局变量是特殊的:为了表明这一点,使用"ear-muffs"。此外,您不需要 Common Lisp 中的注释来说明变量或函数的用途:定义它们时使用文档字符串:

(defvar *board-values* (make-array '(3 3) :initial-element " ")
  "Board values stores x/o and defaults to \" \"")

此外,使用 Common Lisp 命名约定(没有驼峰式大小写,使用 - 分隔单词;恕我直言,Lisp 的约定更具可读性)。

VALIDMOVES 相同的问题。将您的定义替换为:

(defvar *valid-moves*
  (list '(0 0) '(0 1) '(0 2) 
        '(1 0) '(1 1) '(1 2) 
        '(2 0) '(2 1) '(2 2))
  "List of all valid moves remaining ")

现在,功能的变化涉及更多:

(defun get-user-move ()
  (let ((*read-eval* nil))
    (let ((move (read-from-string (read-line *query-io*))))
      (when (valid-move-p move)
        (setf (aref *board-values* (car move) (cadr move)) 'x)
        (setf *valid-moves* (remove move *valid-moves* :test #'equalp))
        (get-user-move)))))

我们逐行来看:

  1. 使用 CL 名称约定
  2. 我们将从字符串中读取。 CL有reader宏,强大但危险的东西。通过将 *READ-EVAL* 设置为 NIL,我们抑制了可能在读取时发生的评估。
  3. 将用户输入读入变量 MOVEREAD-LINE 将接受用户输入,return 输入的字符串,READ-FROM-STRING 会将字符串转换为 Lisp 数据.因此,如果用户输入“(1 2)”,MOVE 将包含列表 (1 2).
  4. 我们对有效着手执行多项操作,如果无效则什么都不做。在这种情况下最好使用 WHEN 而不是 IF:不需要 PORGN 并且与 IF 相比更准确地显示我们在做什么。在 CL 中更习惯于命名谓词以 -P 结尾而不是前面有 IS,因此 ISVALIDMOVE 被重命名为 VALID-MOVE-P.
  5. 这里我假设原始程序有一个错误:你在 VALIDMOVES 上调用 AREF,这不是一个数组。我假设您想更改董事会的状态。请注意,坐标是使用 MOVECARCADR 获取的:MOVE 是列表,而不仅仅是一对,实际上它具有 (CONS X (CONS Y NIL)) 的形式。所以,要得到Y需要取(CAR (CDR MOVE))(CADR MOVE)(SECOND MOVE)(CDR MOVE) 将 return (Y) - 不是数组的有效索引。
  6. REMOVE不影响原序列!即使你使用破坏性的 DELETE 你也不应该依赖它改变原始序列,而是使用 returned 值。因此,删除移动后需要重新分配 *VALID-MOVES* 值。此外,需要为 REMOVE 提供不同的相等性测试,因为 (EQL (LIST 1 2) (LIST 1 2))NIL.
  7. 递归调用自身。也许,更惯用的 CL 代码会使用 LOOP,但这里的递归并没有错。

注意:我稍微改变了一个函数的语义:它 return 是一个无效的动作。否则,用户必须中断评估才能停止。更好的办法是有一个特殊的退出输入(符号QUITQ?)并在无效移动的情况下重新提示输入。

最后,函数 VALID-MOVE-P 在 CL 中非常简单:

(defun valid-move-p (move)
  (member move *valid-moves* :test #'equalp))

代替手动遍历列表,可以使用原始(在某种意义上"provided by CL standard")函数MEMBER

现在可以使用了:

CL-USER> (get-user-move)
(0 1)
(0 2)
(10 20)
NIL
CL-USER> *board-values*
#2A((" " X X) (" " " " " ") (" " " " " "))
CL-USER> *valid-moves*
((0 0) (1 0) (1 1) (1 2) (2 0) (2 1) (2 2))

PS。代码仍然远非理想,但它有效。