Common Lisp 对象 setter 函数风格

Common Lisp object setter function style

我正在尝试编写一个函数,该函数采用名为 nodes 的用户定义对象列表来生成它们之间的连接。每个 node 对象都有一个用于其唯一编号的插槽 ('num') 和一个用于充当节点之间边缘的数字列表的插槽 ('edges')。 +max-edges+ 是一个整数,它定义边配对将被尝试的次数,+max-rooms+ 是节点列表中传递给函数的节点数(并且总是 < 50)。

这里有两个版本的函数试图解决这个问题:

(defun connect-nodes (node-list)
  "Given a NODE-LIST, repeats for +MAX-EDGES+ amount of times
to alter NODE-LIST in-place to connect randomly generated edges to nodes."
  (loop repeat +max-edges+
     do (let ((begin-node (random +max-rooms+))
              (end-node (random +max-rooms+)))
          (when (not (= begin-node end-node))
            (setf (slot-value (nth begin-node node-list) 'edges)
                  (cons end-node
                        (slot-value (nth begin-node node-list) 'edges)))
            (setf (slot-value (nth end-node node-list) 'edges)
                  (cons begin-node
                        (slot-value (nth end-node node-list) 'edges))))))))

(defun connect-nodes% (node-list)
  "Given a NODE-LIST, repeats for +MAX-EDGES+ amount of times
to alter NODE-LIST in-place to connect randomly generated edges to nodes."
  (loop repeat +max-edges+
     do (let ((begin-node (random +max-rooms+))
              (end-node (random +max-rooms+)))
          (when (not (= begin-node end-node))
            (let ((begin-node-lst (slot-value (nth begin-node node-list) 'edges))
                  (end-node-lst (slot-value (nth end-node node-list) 'edges)))
              (setf begin-node-lst (cons end-node begin-node-lst))
              (setf end-node-lst (cons begin-node end-node-lst)))))))

(connect-nodes) 按预期工作,但最后两行在风格上看起来很长,并且两次查找 setf 对象的插槽值,我想这可能是一个性能问题。

(connect-nodes%) 尝试通过将位置绑定到词法范围的位置来解决双重查找,但实际上并没有就地更改节点列表参数。没有进行任何更改,因为 let 绑定中的每个位置(begin-node-lstend-node-lst)仅在词法上绑定并且在 setf 之后超出范围。

所以我要求澄清几点:

我是 运行 slime + emacs + sbcl 如果这会影响你的回答。

编辑: 多亏了我的问题的答案中的建议,这就是我最终得到的 connect-nodes 函数的列表版本。我正在开发一个适用于向量的版本,因此这个版本的 connect-nodes 是一个通用函数的方法:

(defmethod connect-nodes ((node-list list))
  "Given a NODE-LIST, repeats for +MAX-EDGES+ amount of times
to alter NODE-LIST in-place to connect randomly generated edges to nodes."
  (loop repeat +max-edges+
     do (let ((begin-node (random +max-rooms+))
              (end-node (random +max-rooms+)))
          (when (not (= begin-node end-node))
            (push end-node (edges (nth begin-node node-list)))
            (push begin-node (edges (nth end-node node-list)))))))

而不是:

(setf (slot-value (nth begin-node node-list) 'edges)
      (cons end-node (slot-value (nth begin-node node-list) 'edges)))

你可以这样写:

(push end-node (slot-value (nth begin-node node-list) 'edges))

为什么以下没有按预期工作?

(let ((begin-node-lst (slot-value (nth begin-node node-list) 'edges))
      (end-node-lst (slot-value (nth end-node node-list) 'edges)))
  (setf begin-node-lst (cons end-node begin-node-lst))
  (setf end-node-lst (cons begin-node end-node-lst)))

您写道:尝试通过绑定位置来解决双重查找

那是行不通的。您可以绑定位置。您只能绑定值。 LET 将表单的值绑定到变量。

在 Common Lisp 中有一个地方的概念。许多副作用宏与位置一起使用:SETFPUSH 就是例子。一个地方只是访问代码的来源,并不是真正的first-class对象

地点示例:

  • foo 作为变量
  • (aref foo 10) 作为数组访问
  • (slot-value object 'foo) 作为插槽访问
  • (slot-value (find-object *somewhere* 'foo) 'bar) 作为插槽访问...

SETF这样的宏,在宏展开的时候,根据访问表单的来源,找出一个设置表单生成什么表单。它看不到绑定之类的东西,绑定表单来自哪里。

在这种情况下,通常会从数据结构中检索对象(通常是 CLOS 对象或结构),保留对该对象的引用,然后使用 SLOT-VALUEWITH-SLOTS 更改槽值.或者使用访问器。

(setf (slot-value person 'name)  "Eva Lu Ator")
(setf (slot-value person 'group) :development)

会是

(with-slots (name group) person
  (setf name  "Eva Lu Ator"
        group :development))

一般建议:

还要注意你的函数中混淆了 node 是什么。它是 node 类型的对象还是数字?如果它是一个数字,我会将变量命名为 node-number.

避免NTH和列表。如果您需要随机访问,请使用向量。

要么直接使用节点对象(而不是那些数字),要么为它们使用符号:node-123 和 link 某些注册表中节点对象的节点符号。您可能只想在某些情况下使用数字...

我会这样写代码:

(defun connect-nodes (node-vector)
  "Given a NODE-VECTOR, repeats for +MAX-EDGES+ amount of times to connect
nodes via randomly generated edges."
  (loop repeat +max-edges+
        for begin-node-number = (random +max-rooms+) and
            end-node-number   = (random +max-rooms+)
        when (/= begin-node-number end-node-number) do
        (let ((begin-node (aref node-vector begin-node-number))
              (end-node   (aref node-vector begin-node-number)))
          (push end-node   (slot-value begin-node 'edges))
          (push begin-node (slot-value end-node   'edges))))
  node-vector)

你对第二个函数的理解是正确的。

您可能希望在 edges 槽中存储实际节点而不是节点编号。然后,您可以将局部变量绑定到节点本身,而不是将局部变量绑定到要连接的两个节点内部的节点列表,这也比在内部重复调用 nth 更好setf 表格。然后,您还可以在访问 edges 时直接对节点进行操作,而不必执行额外的查找。

为了改进第一个函数的样式,我建议两点:

使用push代替(setf ... (cons thing ...))

slot-value 是一个访问器,因此,它可以用作一个位置。 setf 是改变位置值的一种方法,但 Common Lisp 定义了其他对位置的操作。您在此处使用的模式是在宏 push 中实现的。通过使用它,您可以显着简化您的表达式:

(push end-node (slot-value (nth begin-node node-list) 'edges))

为边定义访问器而不是使用 slot-value

slot-value 应该很少使用,并且作为一种低级机制,因为它比使用命名访问器更冗长且不够灵活。 slot-value 还将访问的重要部分,槽的名称,放在表达式的末尾,这通常会使代码更难阅读。在您的情况下,我会在 class 定义中将访问器命名为 edges

(edges :initform nil :accessor edges)

这将使您的第一个版本更具可读性:

(push end-node (edges (nth begin-node node-list)))