3

I have several generic equality functions, which are used when overriding Object.Equals:

type IEqualityComparer<'T> = System.Collections.Generic.IEqualityComparer<'T>

let equalIf f (x:'T) (y:obj) =
  if obj.ReferenceEquals(x, y) then true
  else
    match box x, y with
    | null, _ | _, null -> false
    | _, (:? 'T as y) -> f x y
    | _ -> false

let equalByWithComparer (comparer:IEqualityComparer<_>) f (x:'T) (y:obj) = 
  (x, y) ||> equalIf (fun x y -> comparer.Equals(f x, f y))

Typical usage would be:

type A(name) =
  member __.Name = name
  override this.Equals(that) = 
    (this, that) ||> equalByWithComparer StringComparer.InvariantCultureIgnoreCase (fun a -> a.Name)

type B(parent:A, name) =
  member __.Parent = parent
  member __.Name = name
  override this.Equals(that) = (this, that) ||> equalIf (fun x y ->
    x.Parent.Equals(y.Parent) && StringComparer.InvariantCultureIgnoreCase.Equals(x.Name, y.Name))

I'm mostly happy with this. It reduces boilerplate[wikipedia]. But I'm annoyed having to use equalBy instead of the more concise equalByWithComparer in type B (since its equality depends on its parent's).

It feels like it should be possible to write a function that accepts a reference to the parent (or 0..N projections), which are checked for equality using Equals, along with a property to be checked and its accompanying comparer, but I've yet been unable imagine its implementation. Perhaps all this is overdone (not sure). How might such a function be implemented?

EDIT

Based on Brian's answer, I came up with this, which seems to work okay.

let equalByProjection proj (comparer:IEqualityComparer<_>) f (x:'T) (y:obj) = 
  (x, y) ||> equalIf (fun x y -> 
    Seq.zip (proj x) (proj y)
    |> Seq.forall obj.Equals && comparer.Equals(f x, f y))

type B(parent:A, otherType, name) =
  member __.Parent = parent
  member __.OtherType = otherType //Equals is overridden
  member __.Name = name
  override this.Equals(that) = 
    (this, that) ||> equalByProjection
      (fun x -> [box x.Parent; box x.OtherType])
      StringComparer.InvariantCultureIgnoreCase (fun b -> b.Name)
Daniel
  • 47,404
  • 11
  • 101
  • 179

3 Answers3

2

Are you just looking for something that takes e.g.

[
    (fun x -> x.Parent), (fun a b -> a.Equals(b))
    (fun x -> x.Name), (fun a b -> SC.ICIC.Equals(a,b))
]

where you have the list of (projection x comparer) to run on the object? (Probably will need more type annotations, or clever pipelining.)

Brian
  • 117,631
  • 17
  • 236
  • 300
  • The projections would be tested with `Equals` (which makes this easier than I anticipated since they can be boxed). A comparer is only passed for the "extra" property. The assumption is the projections refer to types where `Equals` has already been overridden. – Daniel Sep 09 '11 at 19:10
  • This is one place where existential types would be nice, since the natural type for your list would be `(exists 'p. ('x -> 'p) * ('p -> 'p -> bool)) list`, which isn't directly expressable in F#. – kvb Sep 09 '11 at 19:24
  • @kvb: What's the difference between an existential type and `obj` here? – Daniel Sep 09 '11 at 19:38
  • @Daniel - you really want the result type of your projection to be the input to your comparison. Imagine that you've got a fast `intEquals` method. Then you want `(fun x -> x.Id), intEquals` to be an entry in your list, not `(fun x -> x.Id), (fun a b -> intEquals (unbox a) (unbox b))`. – kvb Sep 09 '11 at 20:20
  • @kvb: Good point. That _would_ be ideal. In my case `obj` works okay because I'm simply calling `Equals`. – Daniel Sep 09 '11 at 20:22
  • @Daniel - you can actually encode existentials faithfully in F#, but without direct language support the result is so ugly that it's not worth it. – kvb Sep 09 '11 at 20:22
  • 1
    I'd love to see it anyway, for curiosity's sake. Can I bribe you with an up-vote to post it as an answer? – Daniel Sep 09 '11 at 20:25
  • 1
    what about this version: https://gist.github.com/1207279, it doesn't use boxing and verifies that comparer matches types being compared – desco Sep 09 '11 at 20:41
2

Another implementation, based on Brian's suggestion:

open System
open System.Collections.Generic

// first arg is always 'this' so assuming that it cannot be null
let rec equals(a : 'T, b : obj) comparisons = 
    if obj.ReferenceEquals(a, b) then true
    else 
        match b with
        | null -> false
        | (:? 'T as b) -> comparisons |> Seq.forall(fun c -> c a b)
        | _ -> false

// get values and compares them using obj.Equals 
//(deals with nulls in both positions then calls <first arg>.Equals(<second arg>))
let Eq f a b = obj.Equals(f a, f b) 
// get values and compares them using IEqualityComparer
let (=>) f (c : IEqualityComparer<_>) a b = c.Equals(f a, f b)

type A(name) =
  member __.Name = name
  override this.Equals(that) = 
    equals (this, that) [
        (fun x -> x.Name) => StringComparer.InvariantCultureIgnoreCase
        ]

type B(parent:A, name) =
  member __.Parent = parent
  member __.Name = name
  override this.Equals(that) = 
    equals(this, that) [
        Eq(fun x -> x.Parent)
        (fun x -> x.Name) => StringComparer.InvariantCultureIgnoreCase
    ]
desco
  • 16,642
  • 1
  • 45
  • 56
  • 1
    One slight downside to this approach (for me) is I dislike having special purpose operators (`Eq`, `=>`) hanging around in a large project. I can never remember what they do. – Daniel Sep 10 '11 at 01:14
0

Just to satisfy Daniel's curiosity, here's how to encode the existential type

exists 'p. ('t -> 'p) * ('p -> 'p -> bool)

in F#. Please don't up-vote this answer! It's too ugly to recommend in practice.

The basic idea is that the existential type above is roughly equivalent to

forall 'x. (forall 'p. ('t -> 'p) * ('p -> 'p -> bool) -> 'x) -> 'x

because the only way that we could implement a value of this type is if we really have an instance of ('t -> 'p) * ('p -> 'p -> bool) for some 'p that we can pass to the first argument to get out a return value of the arbitrary type 'x.

Although it looks more complicated than the original type, this latter type can be expressed in F# (via a pair of nominal types, one for each forall):

type ProjCheckerUser<'t,'x> =
    abstract Use : ('t -> 'p) * ('p -> 'p -> bool) -> 'x
type ExistsProjChecker<'t> =
    abstract Apply : ProjCheckerUser<'t,'x> -> 'x

// same as before
let equalIf f (x:'T) (y:obj) =               
    if obj.ReferenceEquals(x, y) then true               
    else               
    match box x, y with               
    | null, _ | _, null -> false               
    | _, (:? 'T as y) -> f x y               
    | _ -> false      

let checkAll (l:ExistsProjChecker<_> list) a b =
    // with language support, this could look more like:
    // let checkProj (ExistsProjChecker(proj,check)) = check (proj a) (proj b)
    // l |> List.forall checkProj
    let checkProj = {new ProjCheckerUser<_,_> with 
                        member __.Use(proj,check) = check (proj a) (proj b) }
    l |> List.forall 
            (fun ex -> ex.Apply checkProj)

let fastIntCheck (i:int) j = (i = j)
let fastStringCheck (s:string) t = (s = t)

type MyType(id:int, name:string) =
    static let checks = 
        // with language support this could look more like:
        // [ExistsProjChecker((fun (t:MyType) -> t.Id, fastIntCheck)
        //  ExistsProjChecker((fun (t:MyType) -> t.Name, fastStringCheck)]
        [{ new ExistsProjChecker<MyType> with 
               member __.Apply u = u.Use ((fun t -> t.Id), fastIntCheck)    }
         { new ExistsProjChecker<MyType> with 
               member __.Apply u = u.Use ((fun t -> t.Name), fastStringCheck) }]
    member x.Id = id
    member x.Name = name
    override x.Equals(y) =
        equalIf (checkAll checks) x y

As you can see, the lack of language support results in a lot of boilerplate (basically all of the object creation expressions, calls the the method Use and Apply), which makes this approach unattractive.

kvb
  • 54,864
  • 2
  • 91
  • 133
  • 1
    I promised I would up-vote it, but since you asked me not to... Thanks for posting it anyway. – Daniel Sep 09 '11 at 22:02