由于以下行为,我刚遇到一个令人讨厌的错误:
scala> List(1.0, 2.0, 3.0, Double.NaN).min
res1: Double = NaN
scala> List(1.0, 2.0, 3.0, Double.NaN).max
res2: Double = NaN
我理解,对于成对比较,有时可能更好 max(NaN, 0) = NaN
这可能是原因所在 java.lang.Double.compare
遵循这个惯例(似乎有一个 IEEE标准 为了那个原因)。然而,对于一个集合,我真的认为这是一个奇怪的约定。以上所有上述集合确实包含有效数字;这些数字有明确的最大值和最小值。在我看来,这个概念就是 最大数量 该系列是 不是数字 是一个矛盾,因为,NaN不是一个数字,所以它不能是一个集合的最大或最小“数字” - 除非根本没有有效数字;在这种情况下,最大的“不是数字”是完全合理的。语义上的 min
和 max
函数退化以检查集合是否包含NaN。由于有更合适的方法来检查NaN的存在(例如 collection.find(_.isNaN)
)在集合上保持语义上有意义的最小/最大值会很棒。
所以我的问题是:获得忽略NaN存在的行为的最佳方法是什么?我看到两种可能性:
在调用min / max之前过滤NaN。由于这需要在所有地方明确处理问题,并可能导致性能损失,我宁愿更容易。
有一种NaN忽略排序会很好,可以在必要时用作隐式排序。我尝试了以下方法:
object NanAwareOrdering extends Ordering[Double] {
def compare(x: Double, y: Double) = {
if (x.isNaN()) {
+1 // without checking x, return y < x
} else if (y.isNaN()) {
-1 // without checking y, return x < y
} else {
java.lang.Double.compare(x, y)
}
}
}
然而,这种方法似乎取决于我是否有兴趣找到最小值或最大值,即:
scala> List(1.0, 2.0, 3.0, Double.NaN).min(NanAwareOrdering)
res7: Double = 1.0
scala> List(1.0, 2.0, 3.0, Double.NaN).max(NanAwareOrdering)
res8: Double = NaN
这意味着我必须有两个NanAwareOrdering,具体取决于我是否需要最小值或最大值,这将禁止具有 implicit val
。因此,我的问题是:如何定义一个排序,以便同时处理两个案例?
更新:
为了完整起见:在分析问题的过程中,我意识到前提“退化为检查NaN”实际上是错误的。事实上,我认为它更难看:
scala> List(1.0, Double.NaN).min
res1: Double = NaN
scala> List(Double.NaN, 1.0).min
res2: Double = 1.0
免责声明:我会在问题中添加我自己的答案,以防万一其他人仍然对此事的更多细节感兴趣。
一些理论......
我觉得这个问题比我想象的要复杂得多。正如阿列克谢·罗曼诺夫已经指出的那样,无法比较的概念要求最大/最小功能采取部分顺序。不幸的是,Alexey也是正确的,基于部分顺序的一般最大/最小函数没有意义:想想部分排序仅定义某些组内的关系的情况,但这些组本身完全独立于彼此(例如,元素{a,b,c,d}只有两个关系a <b和c <d;我们将有两个max / min)。在这方面,人们甚至可能认为正式的最大/最小应该 总是 返回两个值,NaN 和 相应的有效最小值/最大值,因为NaN本身也是其自身关系组中的极值。
因此,由于部分订单过于笼统/复杂,最小/最大功能需要一个 Ordering
。不幸的是,总订单不允许无法比较的概念。回顾总订单的三个定义属性,很明显“忽略NaN”在形式上是不可能的:
- 如果a≤b且b≤a则a = b(反对称)
- 如果a≤b且b≤c则a≤c(传递性)
- a≤b或b≤a(总数)
......并且练习......
所以当试图想出一个实现 Ordering
为了实现我们理想的最小/最大行为,很明显我们必须违反某些事情(并承担后果)。实施 min
/max
/minBy
/maxBy
在 TraversableOnce
遵循模式(for min
):
reduceLeft((x, y) => if (cmp.lteq(x, y)) x else y)
和 gteq
为了 max
变种。这给了我“左偏”这个比较的想法,即:
x <comparison_operator> NaN is always true to keep x in the reduction
NaN <comparison_operator> x is always false to inject x into the reduction
由此产生的“左偏”排序实现如下:
object BiasedOrdering extends Ordering[Double] {
def compare(x: Double, y: Double) = java.lang.Double.compare(x, y) // this is inconsistent, but the same goes for Double.Ordering
override def lteq(x: Double, y: Double): Boolean = if (x.isNaN() && !y.isNaN) false else if (!x.isNaN() && y.isNaN) true else if (x.isNaN() && y.isNaN) true else compare(x, y) <= 0
override def gteq(x: Double, y: Double): Boolean = if (x.isNaN() && !y.isNaN) false else if (!x.isNaN() && y.isNaN) true else if (x.isNaN() && y.isNaN) true else compare(x, y) >= 0
override def lt(x: Double, y: Double): Boolean = if (x.isNaN() && !y.isNaN) false else if (!x.isNaN() && y.isNaN) true else if (x.isNaN() && y.isNaN) false else compare(x, y) < 0
override def gt(x: Double, y: Double): Boolean = if (x.isNaN() && !y.isNaN) false else if (!x.isNaN() && y.isNaN) true else if (x.isNaN() && y.isNaN) false else compare(x, y) > 0
override def equiv(x: Double, y: Double): Boolean = if (x.isNaN() && !y.isNaN) false else if (!x.isNaN() && y.isNaN) true else if (x.isNaN() && y.isNaN) true else compare(x, y) == 0
}
......分析:
目前我正试图找出:
- 此订单与默认订单的比较方式,
- 我们在哪里违反总订单属性,
- 什么是潜在的问题。
我将此与Scala的默认顺序进行比较 Ordering.Double
和以下直接派生的排序 java.lang.Double.compare
:
object OrderingDerivedFromCompare extends Ordering[Double] {
def compare(x: Double, y: Double) = {
java.lang.Double.compare(x, y)
}
}
Scala默认订单的一个有趣属性 Ordering.Double
是它通过语言的本机数值比较运算符覆盖所有比较成员函数(<
, <=
, ==
, >=
, >
所以比较结果是相同的,就像我们将直接与这些运算符进行比较一样。以下显示了NaN与三个排序的有效数字之间的所有可能关系:
Ordering.Double 0.0 > NaN = false
Ordering.Double 0.0 >= NaN = false
Ordering.Double 0.0 == NaN = false
Ordering.Double 0.0 <= NaN = false
Ordering.Double 0.0 < NaN = false
OrderingDerivedFromCompare 0.0 > NaN = false
OrderingDerivedFromCompare 0.0 >= NaN = false
OrderingDerivedFromCompare 0.0 == NaN = false
OrderingDerivedFromCompare 0.0 <= NaN = true
OrderingDerivedFromCompare 0.0 < NaN = true
BiasedOrdering 0.0 > NaN = true
BiasedOrdering 0.0 >= NaN = true
BiasedOrdering 0.0 == NaN = true
BiasedOrdering 0.0 <= NaN = true
BiasedOrdering 0.0 < NaN = true
Ordering.Double NaN > 0.0 = false
Ordering.Double NaN >= 0.0 = false
Ordering.Double NaN == 0.0 = false
Ordering.Double NaN <= 0.0 = false
Ordering.Double NaN < 0.0 = false
OrderingDerivedFromCompare NaN > 0.0 = true
OrderingDerivedFromCompare NaN >= 0.0 = true
OrderingDerivedFromCompare NaN == 0.0 = false
OrderingDerivedFromCompare NaN <= 0.0 = false
OrderingDerivedFromCompare NaN < 0.0 = false
BiasedOrdering NaN > 0.0 = false
BiasedOrdering NaN >= 0.0 = false
BiasedOrdering NaN == 0.0 = false
BiasedOrdering NaN <= 0.0 = false
BiasedOrdering NaN < 0.0 = false
Ordering.Double NaN > NaN = false
Ordering.Double NaN >= NaN = false
Ordering.Double NaN == NaN = false
Ordering.Double NaN <= NaN = false
Ordering.Double NaN < NaN = false
OrderingDerivedFromCompare NaN > NaN = false
OrderingDerivedFromCompare NaN >= NaN = true
OrderingDerivedFromCompare NaN == NaN = true
OrderingDerivedFromCompare NaN <= NaN = true
OrderingDerivedFromCompare NaN < NaN = false
BiasedOrdering NaN > NaN = false
BiasedOrdering NaN >= NaN = true
BiasedOrdering NaN == NaN = true
BiasedOrdering NaN <= NaN = true
BiasedOrdering NaN < NaN = false
我们可以看到:
- 只要
OrderingDerivedFromCompare
履行总订单属性。基于这个结果 背后的推理 java.lang.Double.compare
变得更加清晰:将NaN置于总顺序的上端简单地避免了任何矛盾!
- Scala的默认顺序和偏差顺序违反了许多总体条件。 Scala的默认订单始终返回
false
,而对于有偏见的顺序,它取决于位置。由于两者都导致矛盾,因此很难看出哪些可能导致更严重的问题。
现在我们手头的实际问题,最小/最大功能。对于 OrderingDerivedFromCompare
现在很清楚我们必须得到什么 - NaN只是最大的值,因此无论列表中的元素是如何排列的,都可以将其作为最大值获得:
OrderingDerivedFromCompare List(1.0, 2.0, 3.0, Double.NaN).min = 1.0
OrderingDerivedFromCompare List(Double.NaN, 1.0, 2.0, 3.0).min = 1.0
OrderingDerivedFromCompare List(1.0, 2.0, 3.0, Double.NaN).max = NaN
OrderingDerivedFromCompare List(Double.NaN, 1.0, 2.0, 3.0).max = NaN
现在来看Scala的默认排序。看到这种情况实际上比我的问题中提到的更复杂,我深感震惊:
Ordering.Double List(1.0, 2.0, 3.0, Double.NaN).min = NaN
Ordering.Double List(Double.NaN, 1.0, 2.0, 3.0).min = 1.0
Ordering.Double List(1.0, 2.0, 3.0, Double.NaN).max = NaN
Ordering.Double List(Double.NaN, 1.0, 2.0, 3.0).max = 3.0
事实上,元素的顺序变得相关(作为返回的结果 false
对于每一次比较 reduceLeft
)。 “左偏”显然解决了这个问题,导致了一致的结果:
BiasedOrdering List(1.0, 2.0, 3.0, Double.NaN).min = 1.0
BiasedOrdering List(Double.NaN, 1.0, 2.0, 3.0).min = 1.0
BiasedOrdering List(1.0, 2.0, 3.0, Double.NaN).max = 3.0
BiasedOrdering List(Double.NaN, 1.0, 2.0, 3.0).max = 3.0
不幸的是,我仍然无法完全回答所有问题。剩下的一些要点是:
为什么Scala的默认排序按照它的方式定义?目前处理NaNs似乎存在很大缺陷。一个非常危险的细节 Ordering.Double
是那个 compare
函数实际上委托给 java.lang.Double.compare
,而比较成员是基于语言的本机比较实现的。这显然会导致不一致的结果,例如:
Ordering.Double.compare(0.0, Double.NaN) == -1 // indicating 0.0 < NaN
Ordering.Double.lt (0.0, Double.NaN) == false // contradiction
有什么潜在的缺点 BiasedOrdering
,除了直接评估任何矛盾的比较?快速检查 sorted
给出了以下结果,但没有发现任何问题:
Ordering.Double List(1.0, 2.0, 3.0, Double.NaN).sorted = List(1.0, 2.0, 3.0, NaN)
OrderingDerivedFromCompare List(1.0, 2.0, 3.0, Double.NaN).sorted = List(1.0, 2.0, 3.0, NaN)
BiasedOrdering List(1.0, 2.0, 3.0, Double.NaN).sorted = List(1.0, 2.0, 3.0, NaN)
Ordering.Double List(Double.NaN, 1.0, 2.0, 3.0).sorted = List(1.0, 2.0, 3.0, NaN)
OrderingDerivedFromCompare List(Double.NaN, 1.0, 2.0, 3.0).sorted = List(1.0, 2.0, 3.0, NaN)
BiasedOrdering List(Double.NaN, 1.0, 2.0, 3.0).sorted = List(1.0, 2.0, 3.0, NaN)
暂时我会选择这种左偏序。但由于问题的性质不允许完美的通用解决方案:小心使用!
更新
在基于monkjack建议的隐式类的解决方案方面,我非常喜欢以下内容(因为它根本没有混淆(有缺陷的?)总订单,但内部转换为一个干净的完全有序的域):
implicit class MinMaxNanAware(t: TraversableOnce[Double]) {
def nanAwareMin = t.minBy(x => if (x.isNaN) Double.PositiveInfinity else x)
def nanAwareMax = t.maxBy(x => if (x.isNaN) Double.NegativeInfinity else x)
}
// and now we can simply use
val goodMin = list.nanAwareMin
如何将隐式带入允许您在列表中使用新的最小/最大方法的范围。
就像是:
object NanAwareMinOrdering extends Ordering[Double] {
def compare(x: Double, y: Double) = {
if (x.isNaN()) {
+1 // without checking x, return y < x
} else if (y.isNaN()) {
-1 // without checking y, return x < y
} else {
java.lang.Double.compare(x, y)
}
}
}
object NanAwareMaxOrdering extends Ordering[Double] {
....
}
implicit class MinMaxList(list:List[Double]) {
def min2 = list.min(NanAwareMinOrdering)
def max2 = list.max(NanAwareMaxOrdering)
}
List(1.0, 2.0, 3.0, Double.NaN).min2
对于
val a = List(1.0, 2.0, 3.0, Double.NaN)
把它分类,
a.sortWith {_ >_ }
res: List[Double] = List(3.0, 2.0, 1.0, NaN)
所以 NaN
价值降级,因此最大,
a.sortWith {_ >_ }.head
res: Double = 3.0
同样
a.sortWith {_ < _ }
res: List[Double] = List(1.0, 2.0, 3.0, NaN)
所以对于min,
a.sortWith {_ < _ }.head
res: Double = 1.0