Ответ 1
Отказ от ответственности: я добавлю свой собственный ответ на вопрос на всякий случай, если кто-либо еще интересуется более подробными сведениями по этому вопросу.
Некоторая теория...
Похоже, эта проблема сложнее, чем я ожидал. Как уже отмечал Алексей Романов, понятие несравнимости потребовало бы, чтобы функции max/min выполняли частичный порядок. К сожалению, Алексей также прав в том, что общая функция max/min, основанная на частичном порядке, не имеет смысла: подумайте о случае, когда частичный порядок определяет только отношения внутри определенных групп, но сами группы полностью независимы от (например, элементы {a, b, c, d} только с двумя отношениями a < b и c < d; мы имели бы два max/min). В этом отношении можно даже утверждать, что формально max/min всегда должен возвращать два значения: NaN и соответствующий действительный min/max, поскольку NaN сама по себе также является экстремальным значением в своей собственной группе отношений.
Таким образом, из-за того, что частичные порядки являются слишком общими/сложными, функции min/max принимают Ordering
. К сожалению, общий порядок не допускает понятия несравнимости. Рассмотрение трех определяющих свойств полного порядка делает довольно очевидным, что "игнорирование NaN" формально невозможно:
- Если a ≤ b и b ≤ a, то a = b (антисимметрия)
- Если a ≤ b и b ≤ c, то a ≤ c (транзитивность)
- a ≤ b или b ≤ a (совокупность)
... и практика...
Поэтому, пытаясь придумать реализацию Ordering
для выполнения нашего желаемого поведения min/max, ясно, что мы должны что-то нарушать (и нести последствия). Реализация min
/max
/minBy
/maxBy
в TraversableOnce
следует шаблону (для 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
, тогда как для смещенного порядка это зависит от позиции. Поскольку оба ведут к противоречиям, трудно понять, что может привести к более серьезным проблемам.
Теперь, к нашей актуальной проблеме, функции min/max. Для OrderingDerivedFromCompare
теперь ясно, что мы должны получить - NaN - просто самое большое значение, поэтому ясно, что он получает его как max, независимо от того, как упорядочены элементы в списке:
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
Теперь
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 по умолчанию определяется так, как он есть? В настоящее время обработка NaN кажется довольно ошибочной. Очень опасная деталь
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