通用等于实现

发布于 2024-12-03 23:55:55 字数 2003 浏览 1 评论 0原文

我有几个通用的相等函数,它们在重写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))

典型用法是:

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

我对此非常满意。它减少了样板[wikipedia]。但我很恼火必须在类型 B 中使用 equalBy 而不是更简洁的 equalByWithComparer (因为它的相等性取决于其父级的相等性)。

感觉应该可以编写一个接受对父级(或 0..N 投影)的引用的函数,使用 Equals 检查父级(或 0..N 投影)的相等性,以及要检查的属性和它附带的比较器,但我还无法想象它的实现。也许这一切都太过分了(不确定)。这样的功能如何实现呢?

编辑

根据布莱恩的回答,我想出了这个,这似乎工作正常。

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)

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)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

最后的乘客 2024-12-10 23:55:55

另一个实现,基于 Brian 的建议:

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
    ]

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
    ]
浮萍、无处依 2024-12-10 23:55:55

您是否只是在寻找需要例如

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

在对象上运行(投影x比较器)列表的东西? (可能需要更多类型注释或巧妙的管道。)

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

南街女流氓 2024-12-10 23:55:55

为了满足大牛的好奇心,下面介绍如何在 F# 中对存在类型进行编码

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

。请不要对此答案投赞成票!在实践中推荐它太难看了。

基本思想是,上面的存在类型大致相当于

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

因为我们实现这种类型值的唯一方法是如果我们真的有一个 ('t -> 'p) * ('p -> 'p -> bool) 对于某些 'p,我们可以将其传递给第一个参数以获取任意类型的返回值 'x.

尽管它看起来比原始类型更复杂,但后一种类型可以用 F# 表示(通过一对名义类型,每个 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

如您所见,缺乏语言支持导致在很多样板文件中(基本上所有对象创建表达式,都调用方法 UseApply),这使得这种方法没有吸引力。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文