Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Satisfy the "comparison" constraint with IComparable<'T> #816

Open
4 of 5 tasks
teo-tsirpanis opened this issue Dec 7, 2019 · 12 comments
Open
4 of 5 tasks

Satisfy the "comparison" constraint with IComparable<'T> #816

teo-tsirpanis opened this issue Dec 7, 2019 · 12 comments

Comments

@teo-tsirpanis
Copy link

teo-tsirpanis commented Dec 7, 2019

I propose we allow a class that implements just the generic IComparable<'T> interface, satisfy the comparison constraint.

The existing way of approaching this problem in F# is by implementing the non-generic interface IComparable (or IStructuralComparable). However this approach is not ideal, because it requires a type cast (and potentially unboxing) of a value of type obj. Implementing all three interfaces would increase boilerplate code even more.

Pros and Cons

The advantages of making this adjustment to F# are increased ease of defining a custom comparison function, and conformity to modern coding practices by using generic interfaces.

The disadvantages of making this adjustment to F# are none, I guess. As for backwards compatibility, we can provide an automatic implementation of the two non-generic interfaces, if there are not specified.

Extra information

Estimated cost (XS, S, M, L, XL, XXL): S

Related suggestions: The issue was first reported in dotnet/fsharp#7945.

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I or my company would be willing to help implement and/or test this
@cartermp
Copy link
Member

cartermp commented Dec 8, 2019

I haven't seen a similar suggestion, though I'd be surprised if this hasn't been brought up before...

...regardless, I think this would be great. It's very surprising to me that the non-generic one satisfies, but the generic one does not.

Also, I really wish equality and comparison in .NET wasn't so difficult. Just a quick glance at .NET FX for comparison

IComparable has 76 implementations and 216 references
IComparable<'T> has 41 implementations and 25 references
IComparer has 102 implementations and 200 references
IComparer<'T> has 62 implementations and 209 references

So if we're going by implementations/references, then the non-generic IComparable is the best choice for satisfying the comparison constraint. But as you can see, the framework certainly makes use of all four to a large degree.

@teo-tsirpanis
Copy link
Author

IComparable has 76 implementations [...]

@cartermp, keep in mind that many of them are either in private or internal types, or in the likes of System.Web or LINQ to SQL.

And you were talking about .NET Framework. I guess this has changed with .NET Core.

@cartermp
Copy link
Member

cartermp commented Dec 8, 2019

And you were talking about .NET Framework. I guess this has changed with .NET Core.

I would expect none of that to change with .NET Core, at least for any existing API, otherwise it would be an API breaking change.

@teo-tsirpanis
Copy link
Author

teo-tsirpanis commented Dec 8, 2019 via email

@abelbraaksma
Copy link
Member

abelbraaksma commented Dec 8, 2019

While I'm in favor of this, and I vaguely remember this has been discussed before, it may not be so trivial as it seems.

This is not a breaking change to the F# language design

I'm not sure this is true.

Considering that the compiler currently expects the non-generic IComparable, and suppose this were accepted, how can existing non-generic code that relies on IComparable compare two objects that are not of the same type? If you would only implement IComparable<T>, this is not possible,and existing code would fail to compile. Meaning that you would still have to implement the non-generic version anyway.

At best I see this moving forward if the compiler can take advantage of the fact that the generic interface is implemented (not sure if it currently does). But simply allowing the comparison constraint to be satisfied when only the generic interface is implemented, I'm not sure that's even possible.

In addition, when you implement IComparable<T>, you should also implement IEquatable<T>, and overload the comparison and equation operators (as strongly recommended by the docs, under 'Notes to implementers'). So, if we replace the constraint to mean that the generic, but not the non-generic is to be implemented, that's going to result in more work for programmers.

(that is not to say that it would be good to have this, and esp with value types it can yield a far better performance too)

@cartermp
Copy link
Member

cartermp commented Dec 8, 2019

@abelbraaksma Currently, these are the rules:

If the type is a named type, then the type definition does not have, and is not inferred to have, the NoComparison attribute, and the type definition implements System.IComparable or is an array type or is System.IntPtr or is System.UIntPtr.

(section 5.2.10 of the spec)

This suggestion would change it to be (bolded difference):

If the type is a named type, then the type definition does not have, and is not inferred to have, the NoComparison attribute, and the type definition implements System.IComparable, or System.IComparible<'T>, or is an array type, or is System.IntPtr, or is System.UIntPtr.

I don't believe that would be a breaking change. Existing nongeneric code that doesn't meet the current rules would continue to fail to satisfy that constraint.

@teo-tsirpanis
Copy link
Author

@abelbraaksma

overload the comparison and equation operators (as strongly recommended by the docs, under 'Notes to implementers').

This advice is for C# developers.

If you would only implement IComparable, this is not possible,and existing code would fail to compile.

If you would only implement IComparable<T>, your intent is that this type must not be compared against objects of another type.

Then the compiler would still implement IComparable behind the scenes, by casting the object (and raising an exception if it is of a different type), and defer to IComparable<T> for the actual comparison.

@abelbraaksma
Copy link
Member

abelbraaksma commented Dec 8, 2019

I don't believe that would be a breaking change. Existing nongeneric code that doesn't meet the current rules would continue to fail to satisfy that constraint.

@cartermp, You are right, maybe my concern was less of a concern than I thought. However, I still think it is a breaking change, though arguably much smaller, and probably only in edge cases:

  • Suppose you have a type that currently implements both IComparable and IComparable<T>, there's a chance that behavior is different after this change, even with @teo-tsirpanis's suggestion. And there may be no way to know which of the interfaces will be called when the comparison constraint is in effect (it shouldn't matter, usually, but still).
  • If currently a type only implements IComparable and not IComparable<T>, I assume it would continue to work with library functions, but possibly not with user-created functions that now just assume IComparable<T> (or vice versa).
  • If currently code (like inline code or SRTP) relies on the property that comparison means IComparable is present, this code may fail, or not even compile if used with a new or updated type that implements IComparable<T>
  • Any existing code (not inline or SRTP) that relies on this property can now fail at runtime (i.e., consider a match x with :? IComparable as foo -> foo.CompareTo(bar)..., where x is inferred as having the comparison constraint).

The problem is with "or". Previously, any code that expects a type having the comparison constraint can rely on the fact that IComparison is implemented. After this change, it can be either IComparison, IComparison<T> or both.

I believe that when the BCL was faced with the same issue, they typically introduced different methods for sorting: either generic, or non-generic. Perhaps this all can be inferred and none of my concerns above will end up becoming real-world problems, but on the face of it, I think there are certainly risks involved.

(again, still would love this in, if it is possible and with little backwards-compatibility issues)

@abelbraaksma
Copy link
Member

Just some thoughts/ideas (not necessarily good ones):

  1. We'd default to the generic version if both are present
  2. If a type is comparable and has IComparable implemented, but not IComparable<'T>, then the compiler will emit boilerplate code for IComparable<T> that invokes the user-code of the IComparable implementation. We may emit a warning.
  3. But what if the type is not user-code? Will the compiler magically switch when a function expects comparison and casts to IComparable or IComparable<'T> and if either is not present, implement this on the fly?
  4. Perhaps existing library code can be updated to prefer the generic over the non-generic implementation if both are present

@baronfel
Copy link
Contributor

baronfel commented Dec 8, 2019

how would this interact with the language suggestion around allowing multiple implementations of interfaces with distinct generic arguments? Would the derived IComparable implementations type-test and invoke the an appropriate matching member? If so, what order would the types be checked? Declaration order? Some normalized name ordering?

@charlesroddie
Copy link

I think this goes together with IEquatable and IEquatable. Can those be added to the suggestion? There was work done on IEquatable in dotnet/fsharp#6175 and I hope that can be returned to.

@abelbraaksma In addition, when you implement IComparable, you should also implement IEquatable, and overload the comparison and equation operators

Even though that is still the official advice (https://docs.microsoft.com/en-us/dotnet/api/system.iequatable-1?view=net-5.0), the non-generic interfaces are only needed to support non-generic collections which are obsolete . So I think the advice is unnecessary at this point.

It is so horrible to have to write:

type T(value:int) =
    member t.Value = value
    interface System.IEquatable<T> with member x.Equals y = x.Value = y.Value
    override x.Equals(yObj:obj) =
        match yObj with
        | :? T as y -> x.Value = y.Value
        | _ -> failwith "incompatible types"
    override x.GetHashCode() = 0

@nkosi23
Copy link

nkosi23 commented Nov 7, 2024

One aspect that isn't mentioned is also C# interop. I've just been working with a class defined in a .NET library over which I have no control.

The class implements a custom IComparable <> and I was unable to use seq.Max as a result. I've had to create a wrapper type implementing IComparable as a workaround.

As a user I found it irritating that such a fundamental interface is not supported, especially since I was left scratching my head for quite a while before understanding what was happening. This is not the sort of limitation I would have expected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants