1
votes

I need to compare objects of different types which I know all have properties Id and Legacy_id. Unfortunately I cannot add interface to them since the types come from database schema. I hoped the following comparer would work:

type Comparer<'T >()=
  interface System.Collections.Generic.IEqualityComparer<'T> with 
    member this.Equals (o1:'T,o2:'T)=
      o1.Legacy_id=o2.Legacy_id
    member this.GetHashCode(o:'T)=
      o.Id+o.Legacy_id

Also I have instantiations of the comparer type with the types. So, theoretically compiler has enough information.

But it gives an error: "Lookup on object of indeterminate type based on information prior to this program point. A type annotation may be needed prior to this program point to constrain the type of the object. This may allow the lookup to be resolved."

I wonder why F# fails here? Are there any real/theoretical restrictions or it is just not implemented? Such kind of inference could be very useful.

I suspect that an explanation is about F# compiler is only forward walking. The limitation C# doesn't have. That's what error message is complaining about. Is that so?

2
This can't be compiled, because any given 'T is not guaranteed to have a property named Id. Here's a thought experiment: what would happen if you instantiated this class as Comparer<int>? - Fyodor Soikin
@FyodorSoikin But I don't instantiate it as Comparer<int>. So, until I instantiate it with right types compilation could work. - alehro
This wouldn't work in C# either since 'T does not have an Id property. C#/F# generics are typechecked once and are not expanded based on their usage like C++ templates. - Lee
The F# type inference is powerful, but not perfect. One of the things it can't do is infer "pseudo-interfaces" like "all of these classes have an Id attribute". That's partly because it's tied to the .Net type system, which doesn't allow such pseudo-interfaces. But also, the F# compiler wants to make guarantees of safety. And just because you don't instantiate Comparer<int> now, doesn't mean the compiler can know you won't do so in the future. Since it can't guarantee that, it can't assume that 'T will always have an Id property. - rmunn
I think I've found an answer. It seems possible. It's about duck-typing and compile time polymorphism. Please hold on while I'm trying to apply it to my case. stackoverflow.com/questions/7065939/f-and-duck-typing - alehro

2 Answers

6
votes

Member constraints cannot be used on types, which is why you can't do this. See here.

What you could do is create a comparer class that accepts an explicit equality-checking and hash code generation function for a specific type, e.g.

type Comparer<'T>(equalityFunc, hashFunc) =
  interface System.Collections.Generic.IEqualityComparer<'T> with 
    member this.Equals (o1:'T,o2:'T)=
      equalityFunc o1 o2
    member this.GetHashCode(o:'T)=
      hashFunc o

Then you could use an inline function to generate instances of the above for types that match the constraints you wish to impose:

let inline id obj =
    ( ^T : (member Id : int) (obj))

let inline legacyId obj =
    ( ^T : (member Legacy_id : int) (obj))

let inline equals o1 o2 =
    id o1 = id o2

let inline hash o =
    id o + legacyId o

let inline createComparer< ^T when ^T : (member Id: int) and ^T : (member Legacy_id : int) >() =
    Comparer< ^T >(equals, hash) :> System.Collections.Generic.IEqualityComparer< ^T >

Say you have some type TestType that has the two required properties:

type TestType =
   member this.Legacy_id = 7
   member this.Id = 9

You can then do, for example, createComparer<TestType>() to generate an equality comparer appropriate to your type.

5
votes

A concise way of getting a comparer for e.g. creating a HashSet is:

let inline getId o = (^T : (member Id : int) o)
let inline getLegacyId o = (^T : (member Legacy_id : int) o)

let inline legacyComparer< ^T when ^T : (member Id : int) and ^T : (member Legacy_id : int)>() =
    { new System.Collections.Generic.IEqualityComparer<'T> with
          member __.GetHashCode o = getId o + getLegacyId o
          member __.Equals(o1, o2) = getLegacyId o1 = getLegacyId o2 }

Type inference is about inferring a type, not proving that some generic type parameter satisfies arbitrary conditions for all instantiations (see nominal vs. structural type systems). There are many good answers on statically resolved type parameters on SO already.