Jaap
Jaap

Reputation: 3220

ambiguous implicit values when using contravariant generic type

I've run into a problem with inferImplicitValue in a scala macro. I was playing around with a macro for play's json libary Format[T]. I could narrow it down to a problem how Writes[T] is sometimes implemented with OWrites[T]. Together with an explicit type declaration on an implicit val, this led to the following compiler error.

[error] ambiguous implicit values:
[error]  both value xm in object TestTest of type => OMaterializer[X]
[error]  and value tm in object TestTest of type => Materializer[T]
[error]  match expected type Materializer[X]
[error] one error found
[error] (root/compile:compile) Compilation failed

Let's look at the code (the SBT project can be found here, https://github.com/q42jaap/scala-macro-inferImplicitValue )

// models play json lib's Writes
trait Materializer[-T]

// models play json lib's OWrites
trait OMaterializer[T] extends Materializer[T]

trait T
case class X() extends T

object TestTest {
  // The OMaterializer trait here is the second part of the problem
  implicit val xm = new OMaterializer[X] {}
  // the explicit `tm: Materializer[T]` type declaration here is first part of the problem
  implicit val tm: Materializer[T] = Macro.genMaterializer[T, X]
}

object Macro {
  def genMaterializer[T, M]: Materializer[T] = macro MacroImpl.genMaterializer[T, M]
}

object MacroImpl {
  def genMaterializer[T: c.WeakTypeTag, M: c.WeakTypeTag](c: blackbox.Context): c.Expr[Materializer[T]] = {
    val tMaterializerTpe = c.universe.appliedType(c.typeOf[Materializer[_]], c.weakTypeOf[M])
    c.inferImplicitValue(tMaterializerTpe)
    c.universe.reify {
      new Materializer[T] {}
    }
  }
}

Note the explicit type declaration for tm, removing it, fixes the issue. When xm is Materializer[X] instead of OMaterializer[X], it also works.

inferImplicitValue considers both tm and xm when looking for a Materializer[X]. When xm is of the type Materializer[X] and tm has the type Materializer[T] the inferer prefers xm over tm, because it's an exact match. But when xm is OMaterializer[X] the compiler cannot decide anymore which one is better and throws an error.

As I said removing the explicit type declaration from tm fixes the problem, because at the time when the macro is executed only xm's type is known.

Can I solve this problem inferImplicitValue has? The silent option is already true (by default).

In my real use case, I have multiple implementations of T (X, Y, Z) and pass then with a union type (like reactivemongo does) to the Macro:

genMaterializer[T, Union[X \/ Y \/ Z]]

So I have to use the inferImplicitValue to find a Materializer for X, Y and Z.

Note that I have further simplified this case to

object ThisDoesntWorkToo {
  implicit val xm = new OMaterializer[X] {}

  implicit val tm: Materializer[T] = withoutMacro[X]

  def withoutMacro[A](implicit m: Materializer[A]): Materializer[A] = m
}

which doesn't use macros, but has the same compiler error:

[error] TestTest.scala:15: ambiguous implicit values:
[error]  both value xm in object ThisWorks of type => OMaterializer[X]
[error]  and value tm in object ThisWorks of type => Materializer[T]
[error]  match expected type Materializer[X]
[error]   implicit val tm: Materializer[T] = withoutMacro[X]

This simplifies the case here, but still leaves me with the problem that a implementation of an implicit val can refer to itself. The easy and obvious in the latter case is to explicitly provide the implicit value, but as argumented in the final version of the macro I need to use inferImplicitValue because I have a list of types for which I have to find a Materializer.

Upvotes: 0

Views: 675

Answers (1)

som-snytt
som-snytt

Reputation: 39577

I'm not sure what you mean by "fixes" or "it works", but this is just overload resolution at work.

When both are Materializer, tm wins because Mat[T] <:< Mat[X].

If the use case is to introduce the implicit tm as you show, in that scope, but pick up xm implicitly, then this is the only trick I could come up with:

  implicit val tm: Materializer[T] = {
    val tm = 0
    Macro.genMaterializer[T, X]
  }

which works by simply eliminating the implicit tm from the explicit scope.

Maybe an ugly macro could generate that block automatically, from which the real macro is expanded. That breaks locality.

Usually, you eliminate an implicit by making it ambiguous, but you want the implicit in this scope. So shadowing removes it from the nested scope only.

This doesn't help, but is natural:

object X {
  implicit val xm: OMaterializer[X] = new OMaterializer[X] {}
}

Upvotes: 1

Related Questions