Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Returning a new structure with fields changed

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.

like image 671
Electric Coffee Avatar asked Jul 17 '16 13:07

Electric Coffee


1 Answers

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.

like image 90
coredump Avatar answered Oct 29 '22 12:10

coredump