Proposal: Clone Operator

This is my second of 2 related proposals, as previously mentioned here. The thought is the proposals are complementary.

Proposal: Clone Operator for JavaScript

I started a separate thread since they are separate things. Both are for functionally minded programmers.

Your thoughtful feedback is appreciated.

There's now .toSpliced, toSorted, and toReversed, so I don't think the use case is as compelling for arrays anymore. Separately, instances of classes can't be generically copied - data held in private fields or internal slots, or data held in a WeakMap keyed on the identity of the object, would break if the instance was naively cloned.

Arrays were only shown as a single illustration. And splice, sort, and reverse are only a few of an arrays mutations. There are more. And for objects. And for dates, etc. This correlates to every mutable type having commands which mutate it. Since JS is object-oriented much of the language if filled with mutable operations (commands). It only has a trivial few faux commands.

I'm sorry I didn't make it clearer, but the gist is you clone before you do any mutating operation (to the passed-in subject, array or otherwise) inside any function you want to remain pure.

This applies to every single custom type in user land as well.

Are you attempting to write pure functions? You're going to clone if the type itself isn't a persistent data structure with only faux commands.

@ljharb I believe the suggestion relies on a meta protocol, which is indeed a way to support class instances and other objects with internal state.

My concern is that it's not always clear what should be cloned. The proposal suggests that the meta protocol should only perform a "shallow clone", the definition of which is not clear. For collection like objects I can intuit that the members of the collection wouldn't be cloned. But what about composite objects (or anything with objects that are integral part of the object's state and themselves exposed through a method or property).

"Deep" cloning has a lot clearer semantics but is likely too onerous for the functional programming use case here. Structure cloning is suggested in further consideration, but is actually not sufficient for deep cloning as it does not clone internal state of non-intrinsic objects.

1 Like

I'm not sure I understand the problem you're citing. Can you provide an example of a type for which the shallow cloning algorithm is unclear?

No matter the type (object, array, date, etc). the shallow clone algorithm seems clear to me. This proposal means you write a function which produces an exact shallow copy of the thing being cloned, whatever the type.

const tweety = {name: "Tweety", type: "bird"};
const thief = {name: "Bart", type: "thief", pockets: ["watch", tweety]};
const copy = thief.clone(); //using object clone algorithm

The deep clone concern in the above code is do you also make a copy of tweety? With a shallow clone the answer is no. You allow the copy of Bart to hold the same Tweety reference.

The effect of the symbol is to avoid exposing a clone method which might be overwritten by someone. Symbols guarantee safety of intent. But the symbol for cloning stores a type appropriate shallow clone function inside a symbol address so that this is possible:

const copy = thief!; //same as `thief.clone()`

There is already a structuredClone function implemented in JS. That's for deep cloning. But if deep cloning is the stumbling block, disregard it and let's stick with shallow cloning as that's the focus.

Array is a collection, so it's fairy clear what the semantics of shallow clone should be.

Plain objects when solely composed of other plain objects or collections are also obvious to clone.

What is less clear is what to do with class instances, or other objects with behavior. In particular, when these objects are user provided. Maybe functional programs will seldomly encounter those, but standardized semantics does need to account for them.

There is no such thing as a generic "object" cloning algorithm that can handle all objects.

I'll be pedantic here but structuredClone is not implemented by JS. It's originally a Web function also present in some other environments. And as I mentioned it does not correctly handle internal state of non plain objects.

I do want to introduce a generic cloning algorithm in the language itself at some point, but intended for serialization. I don't believe it's possible to standardize anything else than a full deep clone, which for my serialization use case is needed anyway.

function Person(fname, lname){
  this.fname = fname;
  this.lname = lname;
}
const fred = new Person("Fred", "Flintstone")
const cloned = Object.assign(new Person(), fred);

A clone (for object types) is an Object.assign which retains typeness, nothing more.

Not only does it not necessitate a deep clone, it should avoid doing so. I have been doing FP in JS for a decade and I have yet to need it. But shallow cloning happens routinely.

In my example above, when you clone Bart there's no issue that the Tweety in the pocket is a copy of the original object reference. This is, in fact, ideal, since we're working with reference types. And for value types cloning is a nonissue altogether.

But let's say you don't link the default clone implementation. Since each clone function is hosted at a well-named symbol on the type, you can always decide how to implement clone on your custom types. Basically, clone is a protocol invoked with a special operator. And, by the way, there is first-class protocols proposal.

This is a very simple class with no private state. Anything with WeakMap or private fields based state will not work. Similar for any objects holding state in a closure.

Also this does not represent the case of a complex object which exposes a sub object through one of its methods or properties. Should that sub object be cloned or not. For shallow cloning it largely depends on the reason the object is being cloned in the first place.

My point is, the language itself cannot specify a generic "shallow object cloning" algorithm as there is no way to precisely define what such an algorithm should do.

Edit: I understand the proposal is for a protocol based clone. However I believe that:

  • there should be no default Object cloning behavior (throw if not present). I could be convinced to have a default cloning behavior for plain objects (no custom prototype chain)
  • the cloning protocol cannot be for a "shallow clone" as that is too domain/usage specific.

(I'm trying to leave the operator part out of this discussion, but I'm also highly skeptical the syntax cost is justified)

I've been using a library with objects and arrays and the clone protocol for a long time and 9 times out of 10 you're cloning plain objects and plain arrays and plain dates, the building blocks of JSON. I haven't had the problems with closure state and privates you purport will be problematic.

And again, you implement clone how you like. I'm just proposing to take the protocol up a notch into a syntax. But, if the first-class protocols proposal gets ratified into JavaScript I could live with just that, I guess.

Yes, but because that 1 out of 10 exists, it would be inappropriate imo for the language to standardize something that is actively wrong for a whopping 10% of use cases (i'm just using your numbers; obv none of us can actually provide objective data here)

And again, you implement clone how you like. I'm just proposing to take the protocol up a notch into a syntax.

Not sure why you didn't address this point, since that overcomes the objection you just made.

My objection is over standardizing a cloning behavior that does not have clear semantics. If the language was to merely introduce Symbol.clone, it would have to define the how the protocol is meant to be implemented by users. For shallow cloning, I cannot come up with a definition that works for 100% of objects. Can you come up with precise behavior for what it means to shallow clone the type of objects I mentioned?

For plain objects and arrays this is the convention:

const obj = Object.assign({}, original);
const arr = original.slice();

But even if in userland it didn't suit, the user would know why, and what was suitable, then override the default clone strategy in his module. Having supplied his proper strategy, clone would meet his precise needs, guaranteed.

That is to ask, can the user write cloneObject, cloneArray, cloneDate, clonePerson in a way he likes? The answer must be yes. Otherwise, it's saying the user's object is impossible to clone. If impossible, then the user created a complexity which prevents it and he can, if he wants and needs clone, remove that complexity.

But I've already explained that obj! is just obj.clone where clone is a method the user controls like every other method.

I can understand someone saying, but I don't clone and so I don't see any value here. I'd then guess that programmer is not doing any kind of FP, is not using the functional core, imperative shell pattern, that they're probably just doing OOP. Because in the FP world, unless you have proper persistent types you're probably using objects and arrays (the basic reference types). And they can be used to do FP, well enough for certain.

When I hear "but that won't work because...," I think "I clone all the time, been doing it for years and it works just fine" and that's why I've found the responses perplexing. I mean if users had no way to override the defaults, I'd get it, but I thought I'd explained the default could be overridden.

Or maybe you're saying that having a default which doesn't work 100% of the time makes no sense. I guess I don't see why convention over configuration couldn't be applied here, even if JS isn't a framework. The default works the vast majority of the time, but change if you need.

It's really not a big deal I guess. I have this sense that FP has not truly caught on in JS yet, and, to be frank, I am more invested in the other proposal than this one. I think this is a value add for those doing FP in JS, but I could live without it.

the user would know why, and what was sufficient,

i have found this to be almost entirely false in my experience. The defaults matter, and it should be unacceptable in API design for defaults to be capable of being silently wrong.

OK. Addressing your objection, what if there were no defaults? You provide your own. The clone operator only works if you set it up.

Sure. At that point, what's the value of the part that's built into the language? We already have "bring your own implementation", that's how it works right now.

The convenience of calling ! instead of .clone(), because of its ubiquity as something FP programmers do regularly. It's a protocol with a short name.

Just like you can create objects the long way of the short way {} and arrays too []. Brevity is afforded to common ops.

And there is talk of operator overloading, but you can already implement .add(x, y) and mult(x, y) so why do that?

Anyway, not a hill to die on. I withdraw the proposal.

Operator Overloading is a very early proposal that does not have a clear path for advancement, so "talk" isn't really something that can be relied upon.

Given that this syntax would presumably throw until an implementation was provided, and that adding syntax is a high cost if it wouldn't serve non-FP programmers, I think you'd have a hard time moving forward on this.

1 Like

Well, thanks for talking it through. :)