【问题标题】:Common Lisp object setter function styleCommon Lisp 对象设置器函数样式
【发布时间】:2015-02-22 04:30:12
【问题描述】:

我正在尝试编写一个函数,该函数采用名为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)中的每个位置都仅在词法上绑定,并且在 setfs 之后超出范围。

所以我要求澄清几点:

  • 我对为什么第二个函数无法更改参数列表的理解是否正确?
  • 第一个函数在风格上是否正确?有没有更好的方法来编写这个函数,它不会为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)))))))

【问题讨论】:

  • 我得到的一般建议是:1) 使用PUSH 宏,它执行SETFing 的模式,将项目的值consed 到一个地方的列表中。 2) 当我可以直接在边槽中添加对它们的引用时,为我的对象存储数字查找键似乎很浪费。

标签: coding-style common-lisp


【解决方案1】:

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

您可能希望将实际节点存储在edges 插槽中,而不是节点编号。然后,您可以将它们绑定到节点本身,而不是将局部变量绑定到要连接的两个节点内的节点列表,这也比在 @ 内重复调用 nth 看起来更好987654324@ 表格。然后,您还可以在访问 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 还将访问的重要部分,即槽的名称,放在表达式的末尾,这通常会使代码更难阅读。在您的情况下,我会在类定义中将访问器命名为 edges

(edges :initform nil :accessor edges)

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

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

【讨论】:

  • 我什至没有考虑存储对其他节点的引用,而不仅仅是偏移量。稍后当我遍历节点时,这肯定会清理一些其他代码。我必须对其进行原型设计,看看我是否理解在不创建现有节点对象的额外副本的情况下它是如何工作的。
【解决方案2】:

代替:

(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 是示例。一个地方只是访问代码的来源,不是真正的一等对象

地点示例:

  • 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 并将节点符号链接到某个注册表中的节点对象。您可能只想在某些情况下使用数字...

我会这样写代码:

(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)

【讨论】:

  • 感谢您的详细解释。作为您一般建议的后续问题,是避免NTH 和列表的原因,因为它需要遍历列表直到您到达指定的目的地,而不是可以执行简单(且成本更低)整数索引的向量集合查找?
猜你喜欢
  • 2015-12-12
  • 1970-01-01
  • 2017-10-19
  • 1970-01-01
  • 1970-01-01
  • 2011-05-24
  • 1970-01-01
  • 2020-08-16
  • 1970-01-01
相关资源
最近更新 更多