由于以下行为,我遇到了一个令人讨厌的错误:
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 standard )。然而,对于一个集合,我真的认为这是一个奇怪的惯例。以上所有收集确实包含有效数字;这些数字有明确的最大值和最小值。在我看来,集合的最大数量的概念不是数字是一个矛盾,因为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&#34;实际上是错的。事实上,我认为它更难看:
scala> List(1.0, Double.NaN).min
res1: Double = NaN
scala> List(Double.NaN, 1.0).min
res2: Double = 1.0
答案 0 :(得分:5)
免责声明:我会在问题中添加自己的答案,以防万一其他人仍然对此事的更多细节感兴趣。
我觉得这个问题比我想象的要复杂得多。正如阿列克谢·罗曼诺夫已经指出的那样,无法比较的概念要求最大/最小函数采取部分顺序。不幸的是,Alexey也是正确的,基于部分顺序的一般最大/最小函数没有意义:想想部分排序只定义某些组内的关系的情况,但这些组本身完全独立于彼此(例如,元素{a,b,c,d}只有两个关系a&lt; b和c&lt; d;我们将有两个max / min)。在这方面,人们甚至可能会争辩说,正式的最大值/最小值应始终返回两个值,NaN 和各自的有效最小值/最大值,因为NaN本身也是其中的极值。自己的关系组。
因此,由于部分订单过于笼统/复杂,最小/最大函数需要Ordering
。不幸的是,总订单不允许无法比较的概念。检查总订单的三个定义属性使得很明显&#34;忽略NaN&#34;在形式上是不可能的:
因此,当试图提出Ordering
的实现以实现我们期望的最小/最大行为时,很明显我们必须违反某些事情(并承担后果)。 min
中max
/ minBy
/ maxBy
/ TraversableOnce
的实施遵循模式(min
):
reduceLeft((x, y) => if (cmp.lteq(x, y)) x else y)
对于gteq
变体,和max
。这让我想到了这样的偏见&#34;比较,即:
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
由此产生的这种&#34;左偏见&#34;订购看起来像这样:
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
履行总订单属性。基于此结果the reasoning behind java.lang.Double.compare
变得更加清晰:将NaN放在总订单的上端只是避免了任何矛盾!false
,而对于偏差顺序,它取决于位置。由于两者都导致矛盾,因此很难看出哪些可能导致更严重的问题。现在我们的实际问题是min / max函数。对于OrderingDerivedFromCompare
,现在我们必须明白我们必须获得的内容 - NaN只是最大的值,因此无论列表中的元素如何排列,它都可以清楚地获得它:/ p>
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
)。 &#34;左偏置&#34;显然可以解决这个问题,从而产生一致的结果:
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
答案 1 :(得分:2)
如何将隐式隐藏到范围内,以便在列表中使用新的最小/最大方法。
类似的东西:
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
答案 2 :(得分:1)
有关
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
答案 3 :(得分:1)
这个答案仅仅是为了解释这个问题,@ monkjack的回答可能提供了最好的实用解决方案。
Scala中的既然Scala提供了隐含地传递这种顺序的可能性,那么通过排序的自然愿望并不是这样,它可以处理“无法比较”#34;根据我们的要求
Ordering
仅代表总顺序,即所有元素都具有可比性的顺序。有一个PartialOrdering[T]
:http://www.scala-lang.org/api/2.10.3/index.html#scala.math.PartialOrdering,但有几个问题:
它实际上并未在标准库中的任何位置使用。
如果您尝试实施max
/ maxBy
/ etc。在PartialOrdering
/ Float
之类的情况下,Double
快速地看到除了之外通常可能 {{1}}与任何东西都不具有可比性的元素,其余 彼此相似(你可以决定忽略无比的元素)。