Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# comparison vs C# IComparable

My problem, in a nutshell, is this:

What can I do about storing a tuple (or any type with a constraint of 'comparison') in a C# container that requires an IComparable?

This works:

> let x (y : 'a when 'a : comparison) = y ;;
val x : y:'a -> 'a when 'a : comparison

> x (1,2) ;;
val it : int * int = (1, 2)

I would have thought this would work:

> let x (y : IComparable<int>) = y ;;

val x : y:IComparable<int> -> IComparable<int>

> x (1,2) ;;

  x (1,2) ;;
  ---^^^

stdin(28,4): error FS0001: The type ''a * 'b' is not compatible with the type 'IComparable<int>'

And this as well:

> let x (y : IComparable) = y ;;

val x : y:IComparable -> IComparable

> x (1,2) ;;

  x (1,2) ;;
  ---^^^

stdin(30,4): error FS0001: The type ''a * 'b' is not compatible with the type 'IComparable'

EDIT

I follow the argument that F# doesn't do implicit upcasting. However, even explicitly:

> (1, 2) :> IComparable ;;

  (1, 2) :> IComparable ;;
  ^^^^^^^^^^^^^^^^^^^^^

stdin(43,1): error FS0193: Type constraint mismatch. The type 
    int * int    
is not compatible with type
    IComparable    
The type 'int * int' is not compatible with the type 'IComparable'

I suppose this makes sense as the comparability of a F# tuple is inferred structurally within the F# type system, and perhaps that extra information is not available to .NET.

It seems one workaround per a comment below is invoking

Tuple<_,_> (1,2) ;;

Or even

box (1, 2) :?> IComparable ;;
like image 389
Adam Klein Avatar asked Aug 06 '13 22:08

Adam Klein


2 Answers

F# does not do implicit upcasting as C# does. If you request an IComparable, then you are requesting an IComparable and not something which can be upcast to IComparable

What you really want, is requesting a type, that happens to implement IComparable, but you are still working with the specific type.

That's why let x (y : 'a when 'a : comparison), see that y is of type 'a, whereas 'a can be statically upcast to comparison (if you want to access a member of comparison, you will have to upcast to comparison using :> first)

On the other hand let x (y : IComparable<int>) = y requests very explicitly a IComparable<int>. But you are passing (1,2), a value, that can be upcast to IComparable. So if you pass (1,2) :> IComparable<int> or even (1,2) :> _, the compiler will be able to pass the value. You can wrap up the comparable, but you lose type information, the return value will be a IComparable and no longer an int*int.

let wrapComparable value = 
    {
        new IComparable with
            member this.CompareTo other = 
                match other with
                | :? 'a as other -> compare value other
                | _ -> raise <| InvalidOperationException()
    }

Also, here you need to consider, that IComparable is based on obj so you probably need to consider the case, where your other is of a different type.

In case, you only need IComparable<'a> the code becomes simpler:

let wrapComparable value = 
    {
        new IComparable<_> with
            member this.CompareTo other = compare value other
    }

As such, as a rule of thumb, you usually want to make a generic function with type constraints, rather than requesting an interface, as you would in C#. This is due to the fact, that F# does not do automatic upcasting.

A very detailed explanation about equality and comparisons can be found in http://lorgonblog.wordpress.com/2009/11/08/motivating-f-equality-and-comparison-constraints/ and http://blogs.msdn.com/b/dsyme/archive/2009/11/08/equality-and-comparison-constraints-in-f-1-9-7.aspx. Also the MSDN states, that

If you are only using tuples from F# and not exposing them to other languages, and if you are not targeting a version of the .NET Framework that preceded version 4, you can ignore this section.

Tuples are compiled into objects of one of several generic types, all named Tuple, that are overloaded on the arity, or number of type parameters. Tuple types appear in this form when you view them from another language, such as C# or Visual Basic, or when you are using a tool that is not aware of F# constructs. The Tuple types were introduced in .NET Framework 4. If you are targeting an earlier version of the .NET Framework, the compiler uses versions of System.Tuple from the 2.0 version of the F# Core Library. The types in this library are used only for applications that target the 2.0, 3.0, and 3.5 versions of the .NET Framework. Type forwarding is used to ensure binary compatibility between .NET Framework 2.0 and .NET Framework 4 F# components.

So it seems, that the fact, that Tuples, happen to be System.Tuple is really just an implementation detail at which point, the lack of IComparison makes somewhat sense.

like image 188
Daniel Fabian Avatar answered Sep 22 '22 17:09

Daniel Fabian


Definitely some weirdness going on. FWIW, it works if you construct a System.Tuple<_, _> explicitly, so that might be a workaround:

let x (y : IComparable) = y
let t = (2, 3)

x (Tuple<_,_> t)
like image 20
latkin Avatar answered Sep 19 '22 17:09

latkin