返回字段已更改的新结构

Returning a new structure with fields changed

我正在寻找一种简单的方法来 return 一个新结构,该结构是现有结构的副本,其中一些字段已更改,而不修改原始结构。

我知道您可以使用 setf 更改其中一个字段中的数据,如下所示:

[1]> (defstruct foo bar)
FOO
[1]> (defvar quux (make-foo :bar 12))
QUUX
[1]> (foo-bar quux)
12
[1]> (setf (foo-bar quux) 15)
15
[1]> (foo-bar quux)
15

但正如我所说,这实际上会破坏原始数据,这不是我想要的。

我当然可以这样做:

(defstruct foo bar baz) ; define structure
(defvar quux (make-foo :bar 12 :baz 200)) ; make new instance
(defvar ping (copy-foo quux)) ; copy instance
(setf (foo-bar ping) 15) ; change the bar field in ping

但这似乎更像是一种解决方法。

在 Erlang 中你可以这样做:

-record(foo, {bar, baz}). % defines the record

example() ->
  Quux = #foo{bar = 12, baz = 200}, % creates an instance of the record
  Ping = Quux#foo{bar = 15}. % creates a copy with only bar changed

没有修改数据。

PS 是的,我知道 Common Lisp 不是 Erlang;但是 Erlang 使得不可变地使用 records/structures 变得很方便,并且由于 Common Lisp 鼓励函数式风格,如果有类似的选项可用就好了。

Erlang records are similar to Prolog structures. The Prolog I know implements this as a predicate named update_struct/4 允许宏扩展:它接受一个类型参数并扩展为两个统一体。根据文档,在 Erlang 中完成了相同类型的处理。在 Common Lisp 中,我们不需要显式传递类型,如下面的 update-struct 函数所示:

(defun update-struct (struct &rest bindings)
  (loop
    with copy = (copy-structure struct)
    for (slot value) on bindings by #'cddr
    do (setf (slot-value copy slot) value)
    finally (return copy)))

例子

CL-USER> (defstruct foo bar baz)
FOO
CL-USER> (defparameter *first* (make-foo :bar 3))
*FIRST*
CL-USER> (defparameter *second* (update-struct *first* 'baz 2))
*SECOND*
CL-USER> (values *first* *second*)
#S(FOO :BAR 3 :BAZ NIL)
#S(FOO :BAR 3 :BAZ 2)

规格

Rainer Joswig 亲切地指出:

There is one thing which is undefined by the standard: using SLOT-VALUE on structure objects is undefined. But it should work on most implementations, as they provide this feature. The only implementation where it does not seem to work is GCL.

的确,HyperSpec 说的是 SLOT-VALUE:

Note in particular that the behavior for conditions and structures is not specified.

如果结构化由列表或向量支持,则实现的行为可能会有所不同(请参阅 ̀:TYPE 选项)。不管怎样,如果你需要便携,你最好使用类。 Rainer 在 common lisp: slot-value for defstruct structures.

中也详细解释了这个话题

其他不可变数据结构

也可以考虑使用 属性 列表,它与不可变方法配合得很好。

假设您的初始列表 x 是:

(:a 10 :b 30)

那么(list* :b 0 x)就是下面的列表:

(:b 0 :a 10 :b 30) 

... 其中与 :b 相关联的最新值遮盖了之后的值(参见 GETF)。

循环详细信息

bindings 列表是一个 属性 列表,键和值交替(如关键字参数)。 在上面的 LOOP 表达式中,我使用 ON 遍历绑定列表,这意味着我正在考虑每个子列表而不是每个元素。换句话说,(loop for x on '(a b c d)) 依次将 x 绑定到 (a b c d)(b c d)(c d),最后是 (c)

但是,由于我提供了自定义 BY 参数,下一个元素是使用 CDDR 计算的,而不是默认的 CDR (因此,我们前进了两个单元格而不是一个)。 这让我可以考虑列表中的每一对 key/value 元素,并通过解构语法将它们绑定到 slotvalue