Problems with `Numeric` and `Ordering`

Currently (Scala 2.12.4 and 2.13.0-M3), there are some problems with Ordering and Numeric, particularly with regards to Float and Double (not BigDecimal).

The current default Orderings for Float and Double are internally inconsistent (see scala/bug#10511) with compare behaving the same as j.l.{Float,Double}.compare, the comparison methods (lt, gteq, equiv, etc.) being consistent with IEEE spec, and extrema methods (min and max) behaving the same as math.min and math.max (which is mostly consistent with IEEE spec). For Orderings, it is confusing for many methods to be inconsistent with a total ordering.

The default Numerics for Float and Double inherit from the default Orderings for them. For Numeric, it is absolutely expected that their behaviour conforms to IEEE spec as much as possible (which is why their behaviour was changed to the way it is in the first place). In addition, Ordering.min(x, y) behaves the same as x min y for Float and Double (violating this is extremely unexpected). However, this still leaves the Ordering internally inconsistent, as it is impossible to implement Ordering.compare in a way that complies with IEEE spec.

The root problem is that IEEE floating point values do not have a total ordering; only a partial ordering. This leaves a few solutions:

  1. Leave Ordering for Float and Double as is; it is mostly compliant with IEEE spec, but internally inconsistent.
  2. Revert the behaviour of the default Orderings for Float and Double to be consistent with their compare implementations (and a total ordering), while no longer being consistent with IEEE spec. At the same time, the old (current) Orderings can be used for the default implementations of their respective Numerics; the behaviour of Numeric[Float] and Numeric[Double] will remain as is.
  3. Change Numeric to inherit from PartialOrdering instead of Ordering. Ordering[Float] and Ordering[Double] are changed to be consistent with a total ordering, and Numeric[Float] and Numeric[Double] are defined with PartialOrderings consistent with IEEE spec. There may additionally be an Extrema typeclass which Numeric extends, which defines min and max methods (which would be consistent with math.min and math.max). A deprecated implicit conversion from Numeric to Ordering can perhaps be added to reduce breakage.
  4. Something else?

See scala/scala#6323 for a longer discussion of the issues here. A huge thanks to @Ichoran for the discussion and great points raised there.

To me at least, changing Numeric to inherit from PartialOrdering is the most “correct”, and likely to be least confusing in the long term (despite causing breakage in the immediate future).

When considering changing Numeric to inherit from PartialOrdering instead of Ordering, I considered what methods make sense for a Numeric.

All the comparison methods make sense. But reverse doesn’t. It’s perfectly sensible to reverse an Ordering or PartialOrdering - it means that the less-than and greater-than relationships get reversed. What does it mean to reverse a Numeric though? That doesn’t make any sense.

Perhaps, instead of inheriting from Ordering (or PartialOrdering; whatever the case may be), it should contain an Ordering. What is a Numeric? An object defining the behaviour of a class of numbers? Is the behaviour of a type of number itself an ordering for that type, or should it just have that ordering? To me, the latter seems more natural. It doesn’t make sense to reverse a Numeric, but it makes sense to get the Ordering of a Numeric type, and reverse that.

Perhaps we should change option ‘3’ above to be:

trait Numeric[T] {
  def ordering: PartialOrdering[T]
  // ... rest of trait
}

Do all Numerics have even a partial ordering though?

Take complex numbers. Surely, complex numbers are numbers; therefore they ought to be able to have a Numeric. But complex numbers don’t have any meaningful order. Is 6 + 3i greater than 4 + 7i, or even 3 - 2i? You could define a PartialOrdering that, when either the real or imaginary parts are equal, compares the other part of the two numbers, but would that be useful or meaningful? The vast majority of numbers would still not be comparable, and it’s not clear that such an ordering is anything but arbitrary.

If you are talking about natural ordering, then possibly no. But the question is what you call numeric and whether do you like an idea of having several type classes for a single type (instead of having, for instance, tagged types with a single type class).

It depends. scala.math.Numeric is not only just a number, but a thing that can be converted to Double, Float, Int and Long. Maybe, the name is not really the best.

If we were considering a number to be only those that contain all operations of fields, then current Numeric type class should have be splitted to two: one with field-like operations, one with convertions. I’m sure, it is done cleanly in libraries like scalaz.

That’s a very good point. I had forgotten about that.

It doesn’t help that Numeric (and Integral and Fractional) has no documentation at all.

Hello,

Welcome to Scala 2.12.4 (OpenJDK 64-Bit Server VM, Java 1.8.0_151).Type in
expressions for evaluation. Or try :help.
scala> Double.NaN.longValue
res0: Long = 0

Best, Oliver

1 Like

I am not sure it is possible to guess what people are thinking when they say something like xs.sorted, and it is surely a problem if people are thinking IEEE rules and get a total order, or vice versa. To me this suggests that

  1. Numeric can still extend Ordering, so long as

  2. There is an Ordering which is total that collides.

Then people would have to manually disambiguate the two. I think we can do this by having not just Ordering[Double] in scope but a RealOrdering[Double]. Then people have to manually override the ordering, and the error message can probably tell them what to plug in. Or docs can refer to Double.totalOrder vs Double.ieeeOrder.

I’ve got nothing.

The sad thing is, it happens in Java too. I really don’t even know at this point.

This seems like the simplest solution. It still has a few sharp edges, but it’s not clear to me that anything else would be better.

Ideally, I think Numeric and family should be composed of Orderings, rather than extending them, as several methods (reverse, on, orElse, etc.) don’t make sense for Numeric; however, after some experimentation, it is too late to make that change. It breaks most code that uses Numeric.

How do you get both implicits in scope, and customize the message for ambiguous implicits? Or do you just hope they see the two of them, and check their documentation?

Personally I think that in “ideal world” Numeric should not either extend or contain Ordering just because they are about different things. I think that if some method needs both number operations (like addition and multiplication) and ordering, if should be declared like

def f[T:Numeric:Ordering](ss: Seq[T]) = // e.g. multiply the smallest and the biggest number in the sequence and add the median.

This allows to use e.g. Double’s operations to be used with non-standard ordering. Now, if you need to change the ordering for a method declared like

def g[T:Numeric](ss: Seq[T]) = // whatever using both ordering and numeric operations.

you need to declare another Numeric with the same numeric operations but different ordering operations. This seems to be weird, however depends on whether you accept an idea of having several typeclasses for a single type.

1 Like

You raise a good point about typeclass composition. IMHO, inheritance is usually the wrong way to compose them. We don’t really want to be stating that we have something that is at the same time an ordering over a numeric type and integral field operators over that numeric type. We want some way to conveniently bundle up the fact that both typeclasses should be available in implicit scope, and that those two typeclasses promise to obey some mutual consistency laws.

This is a general issue with how typeclass latices are modelled in scala. The approach of modelling the latice through a trait inheritance latice doesn’t serve us well IMHO. However, I don’t have an obvious fix.

That idea ended up not really working.

The original problem listed in this thread has been fixed here.