开发者

Generic Equals implementation

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开发者_JS百科.

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)


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
    ]


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.)


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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜