比较涉及泛型的不同工作方式
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]
行为,因此它可能会在将来消失。所以我宁愿不依赖它。
我偶然发现了一些 "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]
行为,因此它可能会在将来消失。所以我宁愿不依赖它。