Skip to content
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

Add support for comparing cyclic structures #244

Merged
merged 29 commits into from Oct 4, 2011
Merged

Add support for comparing cyclic structures #244

merged 29 commits into from Oct 4, 2011

Conversation

ghost
Copy link

@ghost ghost commented Jul 13, 2011

As per the discussion in issue #240, I've added support for comparing cyclic structures to _.isEqual. All the unit tests, including my nine additions, pass.

albemuth and others added 4 commits May 3, 2011 21:16
if ((!a && b) || (a && !b)) return false;
// `NaN` values are toxic.
if (_.isNaN(a) || _.isNaN(b)) return false;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be an egal comparison:

if (_.isNaN(a)) return _.isNaN(b);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't backward-compatible, but I agree nonetheless.

@michaelficarra
Copy link
Collaborator

LGTM other than my comments. +1 from me.

// Internal recursive comparison function.
function eq(a, b, stack) {
// Identical objects are equal.
if (a === b) return a != 0 || 1 / a == 1 / b;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now it requires two divisions for any strictly equal falsey values a and b instead of just 0 or -0 as my suggestion would have done. Example: a = b = false. Use !==.

Edit: looks like @kflorence beat me to it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the division would only be executed for the value false...null !=0 and void 0 != 0. I agree, though; it's unnecessary. The latest commit should fix this. Thank you for your other suggestions as well.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty string: '' == 0. But you're right that not all falsey values would execute the division operations.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes. I'd forgotten about the empty string. Thanks!

for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false;
return true;
// Ensure that both objects contain the same number of properties.
var result = aKeys.length == bKeys.length;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of comparing the relative lengths of just the own keys? All keys, including prototypal ones, are going to be compared in the next step.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was in the original _.isEqual function, so I assumed that it was intentional. If it wasn't, then we can have the function compare either own properties or all properties. The deepEqual method in the CommonJS unit testing specification adopts the former approach, but this is clearly problematic for objects that have no own properties and different prototype chains. I use the latter approach in Spec and Maddy, since objects with different inherited properties are clearly not equivalent. This will be more performant as well, since it won't require a dependency on _.keys, and won't loop twice for objects with different sizes. Want me to patch it?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes ... but these sort of changes / tweaks are why I'm still uneasy with merging in a homebrew deep-equal comparison function. It would be awfully nice to have something like this tested in the wild for a while before merging. But so it goes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I can see why you're uneasy, but I also think it'd be useful for other developers to have a deep comparison function to use, rather than writing their own.

// Internal recursive comparison function.
function eq(a, b, stack) {
// Identical objects are equal.
if (a === b) return a !== 0 || 1 / a == 1 / b;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0===0, so I don't quite understand the logic behind the rest of this line.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 === -0, but the two aren't identical. 1 / 0 == Infinity; 1 / -0 == -Infinity.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Hadn't considered -0. I think it's confusing enough to warrant
a comment though.

On Wed, Jul 13, 2011 at 12:35 PM, kitgoncharov <
reply@reply.github.com>wrote:

@@ -595,44 +593,67 @@
return obj;
};

  • // Perform a deep comparison to check if two objects are equal.
  • _.isEqual = function(a, b) {
  • // Check object identity.
  • if (a === b) return true;
  • // Different types?
  • var atype = typeof(a), btype = typeof(b);
  • if (atype != btype) return false;
  • // Basic equality test (watch out for coercions).
  • // Internal recursive comparison function.
  • function eq(a, b, stack) {
  • // Identical objects are equal.
  • if (a === b) return a !== 0 || 1 / a == 1 / b;

0 === -0, but the two aren't identical. 1 / 0 == Infinity; 1 / -0 == -Infinity.

Reply to this email directly or view it on GitHub:
https://github.com/documentcloud/underscore/pull/244/files#r64357

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I'll add it in the next commit.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably include a link to harmony:egal in the comment

// Unwrap any wrapped objects.
if (a._chain) a = a._wrapped;
if (b._chain) b = b._wrapped;
// One of them implements an isEqual()?
// Invoke a custom `isEqual` method if one is provided.
if (a.isEqual) return a.isEqual(b);
if (b.isEqual) return b.isEqual(a);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a does not have an isEqual method (or it returns false), then we should just return false upon seeing this condition, right? Unless we foresee someone wanting non-commutative equality. But that's almost as crazy as irreflexive equality, and nobody would be crazy enough to do that...

edit: added links for @jdalton

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michaelficarra: Like this? if (a.isEqual && !a.isEqual(b)) return false;.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kitgoncharov: like this:

if(typeof a.isEqual == 'function') return a.isEqual(b);
if(typeof b.isEqual == 'function') return false;

PS: I always make sure [[Call]] is implemented before attempting to call something that may not be a function. And any object with [[Call]] will respond to typeof with 'function'.

if (a == b) return true;
// One is falsy and the other truthy.
// Optimization; ensure that both values are truthy or falsy.
if ((!a && b) || (a && !b)) return false;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative solution:

if (!a != !b) return false;

For more clarity, this also works:

if (!a == !!b) return false;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 first alternative solution

@jashkenas
Copy link
Owner

It would be worth making sure that whatever algorithm we want to adopt here also passes the QUnit suite of deep equality tests:

https://github.com/jquery/qunit/blob/master/test/same.js

... and we should take a look at the implementation of QUnit.equiv, as well.

// Compare object types.
var typeA = typeof a;
if (typeA != typeof b) return false;
// The type comparison above prevents unwanted type coercion.
if (a == b) return true;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't a == b guaranteed to be false at this point?
We already know that a !== b && typeof a != typeof b. Doesn't that imply a != b?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bman654: no:

$ node
> a = 0
0
> b = ''
''
> a !== b
true
> typeof a != typeof b
true
> a == b
true
> 

edit: also, sorry for taking so long to respond. I literally had this tab open since you asked that question and didn't have a chance to get back to you.

edit again: it looks like your "what we know" was mistyped and I found a counterexample to that instead of the real source. You meant "we already know that a !== b && typeof a == typeof b. Hmm, let me think about this one. It looks like you're right, though.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michaelficarra The only case I can think of where that may present a problem is an object with a custom valueOf method, such as {valueOf: function () { return 1; }}. Since the == algorithm uses an identity comparison if both operands are objects, however, a == b is never true.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kitcambridge: I'm pretty confident that @bman654 was right. I'd make the change.

@michaelficarra
Copy link
Collaborator

@jashkenas (and other participants, collaborators, and maintainers): I think this pull is ready to be reviewed now.

@ghost
Copy link
Author

ghost commented Sep 5, 2011

@michaelficarra Not quite; I need to fix some last-minute bugs and add more unit tests. Temporarily closing the pull request...

@ghost ghost closed this Sep 5, 2011
@michaelficarra
Copy link
Collaborator

@kitcambridge: whoops, sorry for jumping the gun. I thought it was ready.

Kit Cambridge added 2 commits September 5, 2011 12:25
…. Ignore inherited properties when deep comparing objects. Use a more efficient `while` loop for comparing arrays and array-like objects.
@ghost ghost reopened this Sep 5, 2011
var isNumberA = _.isNumber(a), isNumberB = _.isNumber(b);
if (isNumberA || isNumberB) return isNumberA && isNumberB && +a == +b;
// Compare boolean objects by value. The value of `true` is 1; the value of `false` is 0.
var isBooleanA = toString.call(a) == '[object Boolean]', isBooleanB = toString.call(b) == '[object Boolean]';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this [[Class]] check is used to compare boolean object wrappers, not primitives..._.isBoolean returns false for boolean objects. I'd suggest adding this check to _.isBoolean instead:

_.isBoolean = function(obj) {
  return obj === true || obj === false || typeof obj == 'object' && toString.call(obj) == '[object Boolean]';
};

Thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

@ghost
Copy link
Author

ghost commented Sep 5, 2011

Okay, I think this is now ready for review. Thanks for your help, everyone.

// Optimization; ensure that both values are truthy or falsy.
if (!a != !b) return false;
// `NaN` values are equal.
if (_.isNaN(a)) return _.isNaN(b);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My previous comment proposed adding the expression if (a != b) return false; below this line. This would have worked in one of my local branches, where I attempted to consolidate this series of checks into a single do...while loop, but would have clearly failed in this context. My apologies for any confusion; I'm not sure why I even posted that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was confused so I decided to temporarily ignore it. Good thing, I guess.

@jashkenas
Copy link
Owner

Two further requests on this ... Can we remove the change to _.isBoolean, and make this _.deepEqual instead of changing the current meaning of _.isEqual ?

@jashkenas
Copy link
Owner

Nevermind ... the change to the semantics is fine -- I'll tweak the boolean.

@ghost
Copy link
Author

ghost commented Oct 4, 2011

@jashkenas Thanks. I'll be off to class shortly, so I'm afraid I won't be able to make any changes for the next hour and a half. When I return, though, would you like for me to update the documentation as well?

@jashkenas
Copy link
Owner

No worries -- I'm working through it all now.

@jashkenas jashkenas merged commit 7760e6a into jashkenas:master Oct 4, 2011
@jashkenas
Copy link
Owner

@kitcambridge and @michaelficarra -- thanks for all the great work on this. Landed on master.

@ghost
Copy link
Author

ghost commented Oct 4, 2011

Fantastic. Thank you!

@michaelficarra
Copy link
Collaborator

Awesome. I loved this pull. Many thanks to @kitcambridge, this wouldn't have been started without you. Underscore's isEqual is infinitely more useful thanks to these efforts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants