modf icon indicating copy to clipboard operation
modf copied to clipboard

Multiple changes single copy

Open charJe opened this issue 1 year ago • 6 comments

What do you think about something like say pmodf (like psetf)? On the inside, it would be guaranteed that the object would only be copied once.

Something like this.

 (defstruct a b c)

(let ((a (make-a :b 1 :c 2)))
  (pmodf (a-b a) 2
         (a-c a) 3))

charJe avatar Mar 08 '23 22:03 charJe

I do like it. It has been a long time since I have used this code, but I took a look. Basically there are two ways that modf does its magic, it either knows how to functionally modify an object or it doesn't and it fakes it via a copy and subsequent setf.

This proposal would need to deal with both cases. First, because I combined the interface, we would want to provide a way to indicate that a particular modf expander is actually a copy+setf rather than an actual modify. That seems pretty simple, maybe we provide a 'define-modf-expander' and a 'define-modf-mutator' or something like that.

Then we could identify if we are doing a copy and if so we can stash that copy away and instead produce a copy+setf* with some arbitrary number of setf's. That might get tricky.

We shouldn't forget that there is a potential for doing optimizations on actual modify expanders. E.g. it is cheaper to modify the first two elements of a list at once rather than create an intermediate list you are going to throw out in the next step. However, that seems like it may be trickier to get right... I'm not sure. Or maybe I just need to think about it more.

If this tickles my curiosity enough, I may look into writing functionality for you. Work and other commitments, however, will get in the way so the wait may be effectively infinite. You should feel free to try and implementation yourself and I would love to review.

On Wed, Mar 8, 2023, 16:28 charJe @.***> wrote:

What do you think about something like say pmodf (like psetf)? On the inside, it would be guaranteed that the object would only be copied once.

Something like this.

(defstruct a b c)

(let ((a (make-a :b 1 :c 2))) (pmodf (a-b a) 2 (a-c a) 3))

— Reply to this email directly, view it on GitHub https://github.com/smithzvk/modf/issues/8, or unsubscribe https://github.com/notifications/unsubscribe-auth/AACIUR7MQATNNPUK5MMSAZDW3EBY3ANCNFSM6AAAAAAVUKSEGA . You are receiving this because you are subscribed to this thread.Message ID: @.***>

smithzvk avatar Mar 09 '23 21:03 smithzvk

I think the optimizations are the main reason I'm interested in this. For example with an clos object (more general than a list) modf-ing two slots will result in twice the wasted memory than the equivalent proposed pmodf.

charJe avatar Apr 07 '23 15:04 charJe

Hello again, charje here (I got locked out of my account). After playing with modf a bit more I think I realized that the modf macro already supports what I want. I am currently only using modf for clos objects. The thing I'm talking about is the more parameter like so:

(let ((a (list 1 2 3)))
  (modf (first a) 4
        a (second a) 5))

The above returns (4 5 3), and correct me if I'm wrong, but it will only create 1 new list and mutate the first and second elements. (I think the new list will still tail-share the (3) though.)

Assume we have a clos object.

(defclass person ()
  ((hair-color :accessor hair-color :initarg :hair-color)
   (age :accessor age :initarg :age)
   (id :accessor id :initarg :id)))

Is there a way (or would you be open to something like Haskell record-change syntax? Here is an example of what I mean using an imaginary modf-writers macro for lack of a better name:

(let ((james (make-instance 'person :hair-color :yellow :age 4 :id 1)))
  (modf-writers james
    hair-color :brown
    age 9))

The above would be equivalent to the following:

(let ((james (make-instance 'person :hair-color yellow :age 4 :id 1)))
  (modf (hair-color james) :brown 
        james (age james) 9))

githubforced2fabad avatar Apr 28 '24 05:04 githubforced2fabad

Another candidate for the name could be writef.

githubforced2fabad avatar Apr 28 '24 06:04 githubforced2fabad

Do you have any thoughts about this?

githubforced2fabad avatar May 07 '24 03:05 githubforced2fabad

Hey, sorry for the delay. I put off replying until I could look into it and lost track of this.

Yes, you are correct that in the list example you provided the lists will share the tail '(3).

I looked at the way Haskell deals with this and I don't think that fits well within the normal MODF macro. It makes sense to create a new construct for this as you suggest. I would think something along the lines of COPY-WITH-CHANGES (WRITEF feels like it should be writing to a file or something like that) which takes an object instance and the rest of the argument list is taken as keyword initializers for object slots. Something like this:

(defun copy-with-changes (obj &rest changes)
  #+ecl
  (error "No ECL implementation")
  #-ecl
  (let* ((class (class-of obj))
         (slot-groups (mapcar #'closer-mop:class-direct-slots
                              (closer-mop:class-precedence-list class)))
         (new-instance (apply 'make-instance class changes))
         (specified-args (let ((ht (make-hash-table)))
                           (iter (for (ia val . rest) on changes by #'cddr)
                             (setf (gethash ia ht) t))
                           ht))
         encounted-slots)
    (break)
    (iter
      (for slots in slot-groups)
      (iter
        (for slot in slots)
        (unless (or (member slot encounted-slots)
                    (some (lambda (x) (gethash x specified-args)) (closer-mop:slot-definition-initargs slot)))
          (cond ((slot-boundp obj (closer-mop:slot-definition-name slot))
                 (setf (slot-value new-instance (closer-mop:slot-definition-name
                                                 slot))
                       (slot-value obj (closer-mop:slot-definition-name slot))))
                (t (slot-makunbound
                    new-instance
                    (closer-mop:slot-definition-name slot)))))
        (push slot encounted-slots)))
    new-instance))

You'd use it like this:

(let ((james (make-instance 'person :hair-color :yellow :age 4 :id 1)))
  (copy-with-changes james :hair-color :brown :age 9))

You'd have to extend it to handle structs (and ECL and maybe other implementations out there... I haven't kept up).

That said... we must keep in mind that this is all just a hack when using this library that anything to do with CLOS, or arrays, or hash-tables or any other object that doesn't map nicely onto a functional paradigm. If you have a CLOS object (or array/hash-table/what-have-you) that has is small but holds a few large objects, this may still be a nice way to handle it as everything is shallow copied and performance may not be too bad.

Patches more than welcome.

smithzvk avatar May 07 '24 09:05 smithzvk