Implicits resolution

Not a big discussion of implicits in general, but a behavior that is mysterious to me:

object Base {

  import scala.language.implicitConversions

  implicit def conv[A, B](x: Base[A])(implicit f: A => B): Base[B] = new C[B]()
}

abstract class Base[A] {

  def +(n: A): Base[A] = this
  def +(that: Base[A]): Base[A]
}

case class C[A]() extends Base[A] {
  override def +(that: Base[A]): Base[A] = that
}

The idea is to lift an implicit conversion from A to B into one from C[A] to C[B]. Let A be Int and B be BigInt:

  import Base._

  val x = new C[Int]
  val X = new C[BigInt]

The expression X + x compiles fine: argument x is lifted into a C[BigInt]. However, x + X does not (target x is not lifted).

What’s mysterious to me is that x + X does work fine if:

  • the first + method is removed (no overloading), OR
  • the second + method is given an implementation in Base instead of begin abstract.

My question is this: What does the presence of overloading OR the fact that an overridden method is abstract or not have to do with how implicits are resolved? In my actual implementation, I cannot avoid the overloading, but I can certainly add a dummy implementation to override. But I’d like to know why I need to.

MC

I can’t really say in particular what is wrong here, but that the semantics you desire require walking back from selecting a method, and you’re lucky that it works in the mysterious cases you note at all.

scala> reify(x + X)
res3: reflect.runtime.universe.Expr[charpentier.Base[BigInt]] =
Expr[charpentier.Base[scala.BigInt]](Base.conv(Tests.x)({
  ((i) => BigInt.int2bigInt(i))
}).$plus(Tests.X))

Once scalac reaches some point in method selection, it won’t walk backwards and try to implicit-convert the receiver anymore, so you are stuck.

For example, in Scalaz, we had to encourage everyone to use the cata enrichment method on Option as an alternative name to fold, because when scala-library introduced curried fold in 2.10, it broke our uncurried fold added by implicit.

I don’t understand that. What point needs to be reached? In my case, no + method on x fits types for the expression x + X and thus none can be selected. Why shouldn’t this trigger the conversion conv(x) + X that your reify command seems to produce?

I’m saying that it’s weird that it works in the two bullet-point cases. It’s not weird where it doesn’t work.

I don’t understand “once scalac reaches some point in method selection”. What point does it need to reach to not be able to convert x into conv(x)? What method has been selected at that point? The other + method? But since it doesn’t type-check, how can it prevent implicit conversions?

Here,

scala> implicit class OA[A](val o: Option[A]) {def fold[Z](some: A => Z, none: => Z): Z = o.fold(none)(some)}
defined class OA

scala> (??? : Option[Int]).fold(a => a + 1, 0)
<console>:13: error: too many arguments (2) for method fold: (ifEmpty: => (<error> => <error>, Int))(f: Int => (<error> => <error>, Int))(<error> => <error>, Int)
       (??? : Option[Int]).fold(a => a + 1, 0)
                                            ^

even though the built-in curried fold is totally wrong, and this code could compile if it applied the OA conversion, it won’t, because it saw some fold so it’s too late.

I don’t know what the exact point is, but whatever it is, it’s too late for the fold example to work, as well as the example you posted at top-thread.

But what this illustrates is that “doesn’t type-check because of the signature being wrong” gets different behavior from “doesn’t type-check because no method with that name is present”; these two are not treated the same. The fold is an example of the former: even though it doesn’t type-check, it still prevents the implicit conversion OA.

And this is an example of an asymmetry in the rules of thumb for implicits: while it is true that an implicit conversion will never be applied if the code compiles without one, it is not true that an implicit conversion will always be applied if it makes non-compiling code compile.

I’m still unclear as to what the criteria for allowing/disallowing an implicit conversion are. You seem to say that the presence of a method by the same name, even if it doesn’t type-check, is enough to prevent the conversion. That seems too restrictive.

In Programming in Scala (3rd ed.) by Odersky et al., page 454, an example of implicit conversion is:

1 + oneHalf

where oneHalf is an instance of a user-defined rational class. The presence of a + method in class Int does not prevent the conversion intToRational(1) to happen. Indeed, the book states: the Scala compiler first tries to type check the expression 1 + oneHalf as it is. This fails because Int has several + methods, but none that takes a Rational argument.

Then the book describes Scala wrongly, or at least the scalac implementation. If that description applied in general, then the fold example above would work.

This is not your run-of-the-mill crappy Scala book and it’s a recent edition. I’d be curious to hear from Odersky/Spoon/Venners on this. Either the resolution algorithm is part of the language specification or it’s not. And if it is, it’d be nice to know what it does exactly.