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-lst
和 end-node-lst
)仅在词法上绑定并且在 setf
之后超出范围。
所以我要求澄清几点:
- 我对为什么第二个函数无法更改参数列表的理解正确吗?
- 第一个函数在风格上是否正确?有没有更好的方法来编写这个函数,它不会为
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 中有一个地方的概念。许多副作用宏与位置一起使用:SETF
和 PUSH
就是例子。一个地方只是访问代码的来源,并不是真正的first-class对象
地点示例:
foo
作为变量
(aref foo 10)
作为数组访问
(slot-value object 'foo)
作为插槽访问
(slot-value (find-object *somewhere* 'foo) 'bar)
作为插槽访问...
像SETF
这样的宏,在宏展开的时候,根据访问表单的来源,找出一个设置表单生成什么表单。它看不到绑定之类的东西,绑定表单来自哪里。
在这种情况下,通常会从数据结构中检索对象(通常是 CLOS 对象或结构),保留对该对象的引用,然后使用 SLOT-VALUE
或 WITH-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)))
我正在尝试编写一个函数,该函数采用名为 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-lst
和 end-node-lst
)仅在词法上绑定并且在 setf
之后超出范围。
所以我要求澄清几点:
- 我对为什么第二个函数无法更改参数列表的理解正确吗?
- 第一个函数在风格上是否正确?有没有更好的方法来编写这个函数,它不会为
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 中有一个地方的概念。许多副作用宏与位置一起使用:SETF
和 PUSH
就是例子。一个地方只是访问代码的来源,并不是真正的first-class对象
地点示例:
foo
作为变量(aref foo 10)
作为数组访问(slot-value object 'foo)
作为插槽访问(slot-value (find-object *somewhere* 'foo) 'bar)
作为插槽访问...
像SETF
这样的宏,在宏展开的时候,根据访问表单的来源,找出一个设置表单生成什么表单。它看不到绑定之类的东西,绑定表单来自哪里。
在这种情况下,通常会从数据结构中检索对象(通常是 CLOS 对象或结构),保留对该对象的引用,然后使用 SLOT-VALUE
或 WITH-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)))