Returning a new structure with fields changed
Asked Answered
J

1

6

I'm looking for a simple way to return a new structure which is a copy of an existing one with some fields changed, without modifying the original.

I get that you can use setf to change the data in one of the fields, like so:

[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

But as I stated, this essentially destroys the original data, which isn't what I'm going for.

I could of course do something like this:

(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

But it seems more like a work-around than anything.

In Erlang you can do something like this:

-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

No data modified.

PS Yes I'm aware that Common Lisp isn't Erlang; but Erlang made it convenient to work with records/structures immutably, and since functional style is encouraged in Common Lisp, it would be nice if there was a similar option available.

Journalize answered 17/7, 2016 at 13:25 Comment(4)
“Since functional style is encouraged in Common Lisp”: actually I think we can say that Common Lisp is agnostic about functional style, and in particular the CLOS (object-oriented part in Common Lisp) is completely based on side-effects.Estuarine
It's possible to do OOP immutablyJournalize
Of course it is possible. But Common Lisp has a set of operators that allows the modification of running systems (for instance changes in a class automatically modifies the instances already created), and this has always been considered as a “bonus” of Common Lisp with respect to other languages. I do not want to say that functional style is not possible neither is a good style: it is an excellent style of programming I only note that I've never seen in Common Lisp something that “encourage” functional style more then imperative style: for this reason I think CL is agnostic with respect to it.Estuarine
I mostly base this info off of land of lisp, which in one of the early chapters says functional style is preferred when writing lisp codeJournalize
D
5

Erlang records are similar to Prolog structures. The Prolog I know implements this as a predicate named update_struct/4 which admits a macro expansion: it takes a type parameter and expands into two unifications. The same kind of processing is done in Erlang, according to the documentation. In Common Lisp, we don't need to pass the type explicitly, as shown with the following update-struct function:

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

Example

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)

Specification

Rainer Joswig kindly pointed out that:

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.

Indeed, the HyperSpec says about SLOT-VALUE:

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

An implementation might behave differently if the structured is backed by a list or vector (see the ̀:TYPE option). Anyway, if you need to be portable you'd better use classes. This topic was also explained in detail by Rainer in common lisp: slot-value for defstruct structures.

Other immutable data-structures

Consider also using property lists, which play nicely with an immutable approach.

Say your initial list x is:

(:a 10 :b 30)

Then (list* :b 0 x) is the following list:

(:b 0 :a 10 :b 30) 

... where the most recent value associated with :b shadows the ones after (see GETF).

Loop details

The bindings list is a property list, with alternation of keys and values (like keyword parameters). In the LOOP expression above, I am iterating over the list of bindings using ON, which means that I am considering each sublist instead of each element. In other words (loop for x on '(a b c d)) successively binds x to (a b c d), (b c d), (c d) and finally (c).

However, since I provide a custom BY argument, the next element is computed using CDDR, instead of the default CDR (so, we advance by two cells instead of by one). That allows me to consider each pair of key/value elements in the list, and bind them to slot and value thanks to the destructuring syntax.

Diametral answered 17/7, 2016 at 13:37 Comment(4)
Could you explain what exactly you're doing in that loop? I can't seem to decode itJournalize
Super clever solution with the explanation. Had no idea generic struct functions existed either! Thanks!Journalize
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.Tehuantepec
@RainerJoswig Thanks a lot.Diametral

© 2022 - 2024 — McMap. All rights reserved.