Debugging F# NUnit equals for mixed type tuples

Twitter user Richard Dalton asked a great question recently:

Twitter question

And after a bit more digging he then mentioned

Twitter question

Interesting. I downloaded the NUnit source and saw this:

public bool AreEqual(object x, object y, ref Tolerance tolerance)
    this.failurePoints = new List<FailurePoint>();

    if (x == null && y == null)
        return true;

    if (x == null || y == null)
        return false;

    if (object.ReferenceEquals(x, y))
        return true;

    Type xType = x.GetType();
    Type yType = y.GetType();

    EqualityAdapter externalComparer = GetExternalComparer(x, y);
    if (externalComparer != null)
        return externalComparer.AreEqual(x, y);

    if (xType.IsArray && yType.IsArray && !compareAsCollection)
        return ArraysEqual((Array)x, (Array)y, ref tolerance);

    if (x is IDictionary && y is IDictionary)
        return DictionariesEqual((IDictionary)x, (IDictionary)y, ref tolerance);

    //if (x is ICollection && y is ICollection)
    //    return CollectionsEqual((ICollection)x, (ICollection)y, ref tolerance);

    if (x is IEnumerable && y is IEnumerable && !(x is string && y is string))
        return EnumerablesEqual((IEnumerable)x, (IEnumerable)y, ref tolerance);

    if (x is string && y is string)
        return StringsEqual((string)x, (string)y);

    if (x is Stream && y is Stream)
        return StreamsEqual((Stream)x, (Stream)y);

    if (x is DirectoryInfo && y is DirectoryInfo)
        return DirectoriesEqual((DirectoryInfo)x, (DirectoryInfo)y);

    if (Numerics.IsNumericType(x) && Numerics.IsNumericType(y))
        return Numerics.AreEqual(x, y, ref tolerance);

    if (tolerance != null && tolerance.Value is TimeSpan)
        TimeSpan amount = (TimeSpan)tolerance.Value;

        if (x is DateTime && y is DateTime)
            return ((DateTime)x - (DateTime)y).Duration() <= amount;

        if (x is TimeSpan && y is TimeSpan)
            return ((TimeSpan)x - (TimeSpan)y).Duration() <= amount;

    if (FirstImplementsIEquatableOfSecond(xType, yType))
        return InvokeFirstIEquatableEqualsSecond(x, y);
    else if (xType != yType && FirstImplementsIEquatableOfSecond(yType, xType))
        return InvokeFirstIEquatableEqualsSecond(y, x);
    return x.Equals(y);

That last line tipped me off. So lets look at some tests now:

> z;;
val it : int [] * int = ([|1|], 1)
> p;;
val it : int [] * int = ([|1|], 1)
> z.Equals(p);;
val it : bool = false
> z = p;;
val it : bool = true

So that makes sense, there’s no custom equality comparer for tuples, and since the references are different the obj equals fails. But why do the other things Richard said hold true then?

Well arrays have their own custom comparer that compares contents, that much is visible in the NUnit source. And tuples look to generate the same hash code IF they have value types in them, which you can test in fsi.

> let ref1 = ([], 2);;

val ref1 : 'a list * int

> let ref2 = ([], 2);;

val ref2 : 'a list * int

> ref1.GetHashCode();;

val it : int = 2

> ref2.GetHashCode();;

val it : int = 2

But if a tuple contains a reference type

> let ref3 = ([||], 1);;

val ref3 : 'a [] * int

> let ref4 = ([||], 1);;

val ref4 : 'a [] * int

> ref3.GetHashCode();;

val it : int = -1869554978

> ref4.GetHashCode();;

val it : int = -259699334

Suddenly they aren’t equal! Arrays, being a built in .NET primitive, follow these semantics for generating a hash code. Basically, they return a different value per each instance of the object in the app domain.

Now why does the f# = operator work? From the source, it looks like they have created custom comparators for f# types which does structural equality:

// Note: because these FastEqualsTupleN functions are devirtualized by (=), they have PER semantics
let inline FastEqualsTuple2 (comparer:System.Collections.IEqualityComparer) (x1,x2) (y1,y2) = 
	GenericEqualityWithComparerFast comparer x1 y1 &&
	GenericEqualityWithComparerFast comparer x2 y2

let inline FastEqualsTuple3 (comparer:System.Collections.IEqualityComparer) (x1,x2,x3) (y1,y2,y3) = 
	GenericEqualityWithComparerFast comparer x1 y1 &&
	GenericEqualityWithComparerFast comparer x2 y2 &&
	GenericEqualityWithComparerFast comparer x3 y3
// .... etc ...

let inline (=) x y = GenericEquality x y

Neat! I love it when things make sense.

1 thought on “Debugging F# NUnit equals for mixed type tuples

Leave a Reply

Your email address will not be published. Required fields are marked *