13

I stumbled upon some "odd behaviour". I was using the F# interactive to test some code and wrote

Seq.zip "ACT" "GGA" |> Seq.map ((<||) compare)
// val it : seq<int> = seq [-1; -1; 1]

Then I wanted to make a function out of it and wrote

let compute xs ys = Seq.zip xs ys |> Seq.map ((<||) compare)
// val compute : xs:seq<'a> -> xs:seq<'a> -> seq<int> when 'a : comparison

That generalized the first snippet of code and I thought that was a good thing... until I tried to use it

compute "ACT" "GGA"
// val it : seq<int> = seq [-6; -4; 19]

So somehow compare acts differently for the "same thing" when there is a different "point of view" (explicit type vs generics)

I know how to solve it: either by making the type explicit

let compute (xs: #seq<char>) // ... or char seq or string

Or keeping the type generic and composing with the sign function

let compute (* ... *) ((<||) compare >> sign)

tl;dr the question is where does the difference in behavior come from exactly?

Ringil
  • 6,277
  • 2
  • 23
  • 37
Sehnsucht
  • 5,019
  • 17
  • 27

1 Answers1

11

This is an intricate interplay between F# compiler optimization and .NET standard library optimization.

First, F# tries hard to optimize your program. When the types are known at compile time, and the types are primitive, and comparable, then the call to compare gets compiled to just straight up comparison. So comparing the characters in your example would look like if 'A' < 'G' then -1 elif 'A' > 'G' then 1 else 0.

But when you wrap the thing in a generic method, you take away the type information. The types are generic now, the compiler doesn't know that they are char. So the compiler is forced to fall back to calling HashCompare.GenericComparisonIntrinsic, which in turn calls IComparable.CompareTo on the arguments.

And now guess how IComparable is implemented on the char type? It simply subtracts the values and returns the result. Seriously, try this in C#:

Console.WriteLine( 'A'.CompareTo('G') ); // prints -6

Note that such implementation of IComparable is not technically a bug. According to the documentation, it doesn't have to return only [-1,0,+1], it can return any value so long as its sign is correct. My best guess would be that this is also done for optimization.

F# documentation for compare doesn't specify this at all. It just says "result of the comparison" - go figure what that's supposed to be :-)


If you want your compute function to return only [-1,0,+1], that can be easily achieved by making the function inline:

let inline compute xs ys = Seq.zip xs ys |> Seq.map ((<||) compare)

Now it will get expanded at call site, where the types are known, and the optimized code can be inserted. Keep in mind though that, since [-1,0,+1] behavior is not guaranteed in the docs, it may disappear in the future. So I would rather not rely on it.

Fyodor Soikin
  • 78,590
  • 9
  • 125
  • 172
  • 1
    Great answer. As an aside, if the C# impl is an optimization, and the F# impl could simply reuse it, does that mean the F# one is *actively* inferior? – Dax Fohl Aug 17 '16 at 18:46
  • Nice answer ; I had lookep up about how `System.Char` had implemented IComparable so I knew about subtracting values. The information I didn't know was mostly the first part (about how F# optimize things away). I think I will stick with the added call to `sign` ; at least that is a defined behaviour (for no perceptible cost) – Sehnsucht Aug 17 '16 at 18:57
  • 1
    @DaxFohl I think I would have to agree that F# is actively inferior. But maybe not. It is definitely shorter to subtract than to jump around with conditional branching, but then the subtraction also comes with a function call (`CompareTo`). So I'd say it's a toss up. An experiment is required. – Fyodor Soikin Aug 17 '16 at 21:56
  • BTW, one of the reasons why it's more efficient to subtract than to branch is because with subtraction, you cannot ever suffer a [branch prediction failure](http://stackoverflow.com/a/11227902/2314532). Note that the answer I just linked to is probably **the** best answer on all of Stack Overflow! If you haven't seen it before, *definitely* read it. And even if you have seen it before, read it again for the sheer joy of seeing a question answered *so well*. – rmunn Aug 30 '16 at 00:42