Why are extra imports necessary for Numeric/Ordering operators?

It’s natural to expect to use operators in

def double[A: Numeric](x: A) = x + x

However, currently this requires adding an import

import scala.math.Numeric.Implicits._

which isn’t even listed in Scaladoc. Same for Ordering. Scaladoc there just says

You can import scala.math.Ordering.Implicits to gain access to other implicit orderings

What are arguments against adding these implicits to Predef? I can think of 2:

  1. Not wanting Predef to depend on scala.math. This doesn’t seem strong to me, since it already depends on scala.collection.immutable and on scala.reflect.

  2. Would it break any existing programs? I thought there could be a conflict because the implicits are also defined on the Numeric/Ordering instances, but I tried

    import scala.math.Ordering
    import scala.math.Ordering.Implicits._
    
    object Foo {
    def foo[A](x: A)(implicit ev: Ordering[A]) = { import ev._; x<x }
      def main(args: Array[String]) = println(foo(0))
    }
    

and it still compiles. Even if it didn’t, this could be done by deprecating the instance-implicits in one release and removing them in the next.

So what am I missing?

I don’t think you’re missing much. There’s some general resistance against putting too much stuff into Predef, especially implicits which will then always be in scope for everybody. Historically a lot of stuff was put in there, but now the trend is rather to move stuff out of Predef if possible. But I don’t know why those implicits weren’t added to Predef in the good old days when it wasn’t frowned upon yet…

The good news is that if the new extension methods mechanism of dotty gets accepted, the Right Thing will happen by default without having to put everything in Predef:

scala> trait Ord[T] { 
     |   def (self: T) < (other: T): Boolean 
     | }
// defined trait Ord

scala> implicit object StringOrd extends Ord[String] { 
     |   def (self: String) < (other: String) = self.compare(other) < 0 
     | }
// defined object StringOrd

scala> def isLT[A: Ord](a: A, b: A) = a < b             
def isLT[A](a: A, b: A)(implicit evidence$1: Ord[A]): Boolean

scala> isLT("bar", "foo")
val res1: Boolean = true
1 Like

The basic answer to your question is, people more or less agree nowadays that implicit conversions, especially ops-style stuff which add ‘syntactic sugar’ operations to generic methods, should be imported explicitly. We don’t want to pay the import tax to get the actual implicit instances, but we do to get the operations.

So, given this philosophy, I more or less agree with what’s going on here, although I do think it’s a bit more complex than was really necessary. I mean, I get why all those implicits were defined as traits and then as instance objects, but I still think it’s overkill. And there was just no need to create a separate Implicits object just to house a single implicit method, which should have been an implicit class in the first place to provide the same functionality.

So you would have been able to do this:

import scala.math.Numeric.Ops
...

… Where Ops would be the implicit class that provides the operators.

This actually conveys a useful meaning, it actually reads like a meaningful sentence. With the current style you see you’re importing Numeric implicits but it’s kinda hard to understand why you’d need to do that from the naming alone, because the implicit instances should already be in scope.

1 Like