The Philosophy of COPY

Part of Rebol's pitch is that it is a "relatively simple" data structure, which has a bit of invisible magic (in the form of bindings) that helps imbue it with behavior. But it's not necessarily simple, and when you try to define an operation like COPY then it opens up a lot of questions.

For instance, what should this do?

obj: make object! [a: 10]
blk: compose [(obj) (obj) (bind 'a obj)]
blk2: copy/deep blk
blk/1/a: 20
blk2/1/a: 30
dump [(blk/1/a) (blk2/2/a) (get blk/3)] 

Questions:

  • OBJECT!s which are added directly to blocks are often (usually?) intended as references. The only other way to "link" to a reference is through a WORD!/PATH!/GET-WORD!/GET-PATH!, which may not always be appropriate or convenient. Should COPY/DEEP by default presume that an object inside a block be cloned, forcing anyone linking to an object to use a WORD!/PATH!/GET-WORD!/GET-PATH! to do so? Should you instead say at minimum copy/deep/types block [any-context! any-array!] ?

  • If multiple references to the same object are contained in a block being copied, should the object be copied each time...or just once, and each common 'reference" to the original object forward to the same copy?

  • Do bindings participate in forwarding, or do they not change? This would be played out in the case above because you are copying the a WORD! which is bound to an object that gets copied in the same operation as that a is. Was the likely intent for the copied a to now reference the copied object? Note: While not a new problem, it gets more complex in Ren-C, as the bindings of ACTION!s dictate their participation in 'derived binding' inheritance...copying an OBJECT! without forwarding the bindings of ACTION!s inside of it will mean the methods will think they should run in the original object.

There's probably no single answer

This resembles a problem I've seen before in a non-Rebol project. At that time, I felt like the best way to bias deep copying was to follow along the backbone, but leave all linkages as-is. Then, adjust the process so that it was easier to hook. It might sound inefficient, but pushing the forwarding behavior into userspace could be the right thing to do...winding up with something a bit more like a MAP-EACH with some extra powers.

So I think my wish is that COPY/DEEP not deeply copy ANY-CONTEXT! by default. Because it seems like the average case of wanting to put an object into a block is to do it as a reference or linkage, where you want the same behavior as a WORD! that looks up to an OBJECT!.

A newer way to look at it, might be to supply a list of objects that you'd want the system to forward during a copy. Then have a means of collecting object references out of blocks, if you wanted to say "collect all the objects and then forward them". Put those things together and you wind up with the design of a more flexible answer than trying to make a COPY that tries to do it all.

Any opinions?

Nothing against a custom half-shallow-half-deep copy, but in general I would like to see a copy that understands self-references.
Funnily enough, PRINT detects them, but COPY does not.
My personal preference is that PRINT make the copies explicit, and that COPY use the same method internally to perform deep copying properly.
See https://rosettacode.org/wiki/Deepcopy#Common_Lisp.