Search code examples
f#overridingequalitydiscriminated-union

How does one override .Equals() for a Discriminated Union?


I have a discriminated union type and want to override .Equals().

In this simple example I could have used the .Equals function for int to solve the problem, but in my code otherStuff does not support structural comparison.

The following code was my best try:

[<CustomEquality>]
type ModelArg = { Name: string; OtherStuff: int}
    with override this.Equals (o: obj) = this.Name = (o :?> ModelArg).Name

I then got a red squiggly line and the following message:

"The struct, record or union type 'ModelArg' has an explicit implementation of 'ObjectEquals'. Consider implementing a matching override for 'Object.GetHashCode'."

I would like to avoid doing that because I really only care for the field Name and also, for performance reasons.

Of course I could write an equals function but I would not be able to use it with List functions like List.contains and I need to do that.

Any suggestions?


Solution

  • The error is telling you that, since you're overriding the Equals method, it's a very good idea to override GetHashCode as well.

    The reason for this is that in .NET in general (not just in F#), hash codes are often used as an approximation of equality. For example, if you were to put your objects in a hash table, the hash table would distribute them between buckets based on GetHashCode, and would look them up in the buckets that way too. Then, if Equals is implemented differently than GetHashCode, the hash table's behavior will be unpredictable - it might fail to look up an object that was just inserted or something similar.

    Further, the error message does not suggest that you include the int in the definition of equality. All it says is that you need to implement GetHashCode, and do it in the same sense as your Equals implementation. There is also no performance penalty for doing this, as long as you never actually call GetHashCode. And if you do - see above.

    Since all your Equals implementation does is compare the Name field, it would probably make sense to delegate GetHashCode to the same field as well:

    [<CustomEquality; NoComparison>]
    type ModelArg = { Name: string; OtherStuff: int}
        with 
           override this.Equals (o: obj) = this.Name = (o :?> ModelArg).Name
           override this.GetHashCode() = this.Name.GetHashCode()
    

    Finally, your implementation of Equals would crash when called with a null or with an object of another type. I would suggest that you handle this case if you want your code to be robust:

           override this.Equals (o: obj) = 
               match o with 
               | :? ModelArg as ma -> this.Name = ma.Name
               | _ -> false