-
Notifications
You must be signed in to change notification settings - Fork 216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Handling circular references #207
Comments
This would probably best be solved by a custom I would be open to linking to such an implementation, or perhaps even putting it into the readme for people to copy/paste. |
Updated my original comment with a strategy.
In this case, anything with a reference to a node higher up the chain is going to blow the stack. Generally speaking, it's a good idea to ensure that logic is built into recursive functions to ensure that they can never infinitely recurse. That in mind, proposed strategy above is an approach which covers the issue or throws an appropriate error message if it can't. If you'd like, I'd be glad to submit a PR. |
Using seen would cause false positives in some cases. Example:
If we're walking Child2, Child1 and Grandchild1 would have been 'seen', but because they're not in the ancestral lineage, they can be merged. That's why we need a direct lineage map to be passed to the recursive function. Without a third parameter, an But again, this isn't too difficult to solve and could be taken care of with a few extra lines. Let me know if you'd consider a PR. |
actually, on reflection, this is a much harder problem that I don't really want to try to solve here – merging two trees makes sense, but merging two cyclical graphs doesn't make sense – what would you even expect the output to be if you tried to merge const a = { child: { grandchild: { tiny_baby: 'hi } } }
const b = { child: { } }
b.child.grandchild = b
merge(a, b) That seems nondeterministic, right? I'm inclined to say "the solution is to eliminate cycles in your objects before trying to merge them" |
Edit: I misread the original code... updating my response. |
Effectively, that becomes: const a = { child: { grandchild: { tiny_baby: 'hi' } };
let b;
b = { child: { grandchild: b } }; You're correct that in this case this cannot be merged. Effectively here, if we were to assign a pseudo-AST to it, we have a
In your example, it would either pass off the conflict to a handler function, or if none is given, it would throw a circular reference error. |
As far as whether it should be solved I have two thoughts: 1. It's generally a good idea to ensure that recursive functions cannot ever hit an infinite recursion scenario.That in mind, putting in some logic to to track this is good practice. And if you're going to do that, the extra bits to allow handling those scenarios isn't too much extra and would help in making it so only those scenarios that should throw an exception do. (ie, there's a reference to a parent but nothing to merge with it, we can just keep the reference as a reference to its merged counterpart) 2. How often will two graphs be merged?It depends on the context. This is very common in some areas of CS, and not as much in others. (Any object which contains a reference to a parent will become un-mergeable, and this is fairly common.) Ultimately, it's your call. Not supporting it will limit the scopes in which the library can be used, where adding the ability to handle circular references will certainly broaden that and help a lot of people. At the very least, it will reduce bug reports when people find themselves hitting circular ref issues. Again, the rules are pretty simple (pseudo-AST again)
The default behaviour can be changed, but this prevents stack being blown and allows users to have control over circular issues, which will allow the library to be used in more contexts. |
I'm amenable to this. If we added any code to address this, my preferred solution would be to throw an error with a clear message as soon as a cycle was detected. Merging data structures in JS is complicated enough already, I don't want to make my life the hell that it would become by trying to solve general CS "merging cyclical graphs" problems. |
Makes sense. How about this:
Simple, non-breaking, and still allows us hell-dwellers to do our thing, as needed. 😄 ExampleThe following is how I could implement the behaviour I proposed earlier using this extra param: let a, b;
a = { child: { parent: a.child } }
b = { child: { hi: 1 } }
merge(a, b, {
customMerge: () => (a, b, circularReferences) => {
if (circularReferences && (a === undefined || b === undefined))
return circularReferences.get(a ?? b); // The value will be a reference to mergedObj.child
}
}); If you're down with that, let me know, and I'll do the PR. |
deepmerge should not blow up when merging with circular references. I'd love to see this getting implemented. |
Please read the thread above – I'm open to throwing a clear error when a cycle is detected, but I do not see an obvious way to return sensical data when merging a cycle. |
I think the default behavior should be throwing an error. If custom merge function is provided then that function can determine how to handle cycles. |
Agreed. My proposal above should meet those specifications. I'd still willing to do the PR. |
@TehShrike I agree that handling circular references is not an easy task, but I think a good starting point is to inspect solutions on stringifying circular references. I think a similar approach could handle circular references here, so it would allow to deep merge objects with circular references without throwing an error. This implementation would give this package a superior status when it comes to deepmerge-solutions.
I think this is a first good logical step, though. (Note that I don't want to say that you must implement a proper circular reference handling, I'm just saying it would be super nice to have out of the box) |
Unless I'm misunderstanding you, stringifying would not work as it would still have a circular issue. This sort of thing is not as complicated as it may seem. It's quite common in AST-like scenarios. I apologize if I've not explained the proposed solution clearly. To clarify, you simply maintain a linear set of parent references during the walk, then pass that set to the custom merge function, if present. The custom merge function can determine what to do if This allows for fast-exit throw behaviour as the default while also providing for custom handling if the user desires to. |
@nonara Unfortunately, you misunderstood me. I know that stringify is not a solution to this problem, I was just saying that it has the same problem and it can be worked around. I was just making a point that it's not impossible to fix this, properly.
I get your point, I just wish that custom function could be part of the lib in the first place. So I seek a generic approach so to speak. I'm aware of the main concern now, it seems to be that you might try to merge an object that has nested objects that are part of deeper circular references on the opposite object you want to merge with. Simply skipping as soon as we see circular references would skip the deep merge and the result would be weak. But what about skipping circular references when we come across them and do the deep merge in both directions, merging a in b and then b in a? I think this would successfully merge and handle circular references well. Little sample: const a = {
a1 = 1,
a2 = 2,
a3 = 3
};
a.a = a;
const b = {
a1 = 1,
b1 = 1,
c1 = 1,
a = {
a3 = 4
a5 = 5
}
} Of course we have conflicting values here. If I remember correctly the first object will be in favor of conflicts, it should not? if we favor {
"a1": 1,
"a2": 2,
"a3": 3,
"a5": 5,
"a1": 1,
"b1": 1,
"c1": 1,
"a": {
"a1": 1,
"a2": 2,
"a3": 3,
"a5": 5,
} (here But if we favor {
"a1": 1,
"a2": 2,
"a3": 4,
"a5": 5,
"a1": 1,
"b1": 1,
"c1": 1,
"a": {
"a1": 1,
"a2": 2,
"a3": 4,
"a5": 5,
} (here |
Issue
In a circular reference scenario, it goes into an infinite recursion, which blows the stack.
Reproduction
Fix Suggestion
When recursively walking objects, maintain a chain Map<key: original, value: cloneOrOriginal (depends on settings)> of references to objects & arrays.
Walk function logic:
The text was updated successfully, but these errors were encountered: