比较涉及泛型的不同工作方式

compare working differently with generics involved

我偶然发现了一些 "odd behaviour"。我正在使用 F# interactive 来测试一些代码并编写了

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

然后我想用它做一个函数并写

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

这概括了第一段代码,我认为这是一件好事...直到我尝试使用它

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

所以当存在不同的 "point of view" 时 compare 对 "same thing" 的行为有所不同(显式类型与泛型)

我知道如何解决它:要么通过明确类型

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

或者保持类型通用并与sign函数组合

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

tl;dr 问题是行为差异究竟来自哪里?

这是 F# 编译器优化和 .NET 标准库优化之间错综复杂的相互作用。

首先,F# 努力优化您的程序。当类型在编译时已知,并且类型是原始的且可比较的,那么对 compare 的调用将被编译为直接比较。所以比较你的例子中的字符看起来像 if 'A' < 'G' then -1 elif 'A' > 'G' then 1 else 0.

但是当你用泛型方法包装东西时,你就带走了类型信息。这些类型现在是通用的,编译器不知道它们是 char。因此,编译器被迫回退到调用 HashCompare.GenericComparisonIntrinsic,后者又对参数调用 IComparable.CompareTo

现在猜猜 IComparable 是如何在 char 类型上实现的?它只是减去值和 returns 结果。说真的,在 C# 中试试这个:

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

请注意,IComparable 的这种实现在技术上不是错误。根据 the documentation,它不必 return 仅 [-1,0,+1],它可以 return 任何值,只要其符号正确即可。我最好的猜测是,这也是为了优化。

F# documentation for compare 根本没有指定这一点。它只是说 "result of the comparison" - 想想那应该是什么 :-)


如果您希望 compute 函数仅 return [-1,0,+1],可以通过使函数 inline:

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

现在它将在已知类型的调用点展开,并且可以插入优化代码。请记住,由于文档中不保证 [-1,0,+1] 行为,因此它可能会在将来消失。所以我宁愿不依赖它。