Implicit ordering from Numeric

In building orderings, I find the resolution of implicits rather inconsistent:

class Test[T : Numeric] {

  class X(val t: T)

  val o1: Ordering[X] = Ordering.by(_.t)

  val o2: Ordering[X] = o1.reverse

  // doesn't work
  //val o3: Ordering[X] = Ordering.by(_.t).reverse

  val o4: Ordering[X] = Ordering[T].reverse.on(_.t)

  // doesn't work
  //val o5: Ordering[X] = Ordering[T].on(_.t).reverse
}

The o3 definition is rejected, even though o2 works. The definition of o5, as an alternative to o4, is also rejected. In both cases, the error seems to be a missing implicit Ordering[T], but the other definitions need it too and still work.

What am I missing here?

This has nothing to do with implicit resolution but rather type inference.

I really do not understand why do you expect your commented examples to compile, but let me try to explain what is the problem.

val o3: Ordering[X] = Ordering.by(_.t).reverse

So according to the docs by expects a A => B and given an Ordering[B] it provides an Ordering[A] (so in plain English, it is quite simple, if I know how to order something and I know how to get that something from something else, then I know how to order that something else by something).
Here the compiler has zero information about what is the type of the input of the function, so it can not infer the output of the function so it does not type-check. It works in o1 because it can use the expected return type of of the whole expression to infer the type of the input; in this case, since there is another call after by the inference can not use that information.
So you need to help the compiler know what is your input type:

// You do not even need the explicit return type
val o3 = Ordering.by[X, T](_.t).reverse
// Or
val o3 = Ordering.by((x: X) => x.t).reverse

The same happens with o5

You can see them running here.


One may argue that the compile could try harder with the type inference by seeing that reverse returns the same type… I know Scala 3 will improve its type inference, but I do not know if this would work, flowing types on more complex expressions may be pretty slow and maybe not be that useful.

No need to be mean. I understand what you’re saying, but arguing that one needs to known the inner workings of the compiler’s type inference to not be surprised is not fair. Suppose a better Scala 4 manages to compile the expression. Does my incorrect assumption that Scala 2 already had that capability make me stupid? For good or bad, I suspect code that breaks by replacing a val name by the expression it refers to will always catch me unguarded.

Try type ascription : X

val o3: Ordering[X] = Ordering.by((_: X).t).reverse

val o5: Ordering[X] = Ordering[T].on((_: X).t).reverse

Using more advanced language is always a trade-off. Either you need a language with more features (more advanced type system, which allows you to express your business logic with those more advanced types, but that’s not for free, type inference is worse, you should be ready to help compiler providing hints more often) or a language with less features is enough for your goals (simpler type system, easier type inference, no need or minimal need to provide hints; or even no static types at all).

There are languages with more advanced type systems than in Scala (Idris, Agda, Coq etc.), you’ll have to specify types there explicitly even more often.

Maybe for example Java or JavaScript can be enough.

I understand. And I knew how to resolve the problem with more type information. I just was puzzled by why the call to reverse would matter (from my limited understanding of Scala’s type inference), and I was barking at the wrong tree thinking the resolution of implicits had anything to do with it. I’m all set now, thanks.

1 Like

Oh sorry, it was not my intention to sound mean. It is just that I really do not understand how you expect the expression _.t to be inferred by the compiler. But yeah, it may be just my way of seeing things. Anyways, let me apologize again.

I would not call this inner working of the compiler, as a matter of fact, I have no idea of how the compiler works.
But, I would say that one does need to have a basic understanding of how type inference works; since it can help you fix bugs. However, I may give you the point that it keeps surprising me so it is a matter of experience.

Again, not my intention.

Being fair, you have a point here.
But again, I would say it is fair for the compiler to fail at some point, given more and more complex expressions it has to give up or be extremely slow (without taking into consideration that it may end up with many alternatives that work).

It’s okay. No big deal.

Maybe I’m a bit spoiled from having programmed in SML for many years. Also, IntelliJ keeps flagging missing implicits that are not missing, which adds to my confusion.

I tend to think of Scala’s type inference as flowing from left to right. If L is known to be of type List[A], I can write expressions like L.map(...).filter(...)... without too many type problems.

So, when I see Ordering[T].on(_.t) correctly typed, I expect it to continue into Ordering[T].on(_.t).reverse without troubles. But the [T] information is insufficient, and Ordering[T].on(_.t) only works because of the specified type Ordering[X] of variable o. With a call to reverse, value o is a totally different expression. One could reason that if Ordering[T].on(_.t).reverse has type Ordering[X], then so does Ordering[T].on(_.t), but this is not so much left-to-right anymore.

Maybe I should use more lambdas (with types) and fewer partial applications. It tends to make code more readable anyway.

Doesn’t it have an option to fill implicits? Or is that the one that is failing?
Off-topic, but have you tried metals? It has a smaller feature set but it will always be correct about compile errors.

Yeah, the main difference is that this is really a contramap so the left value does not help on the inference.

Being honest this is kind of an exception, I can not think on many methods that have a right-to-left inference.
However, I do prefer named (not really typed, but named) lambdas over the _ syntax most of the time; but that is all subjective.

Anyways, I am glad the problem is solved.