3
votes

I have this code:

trait Context {
  implicit val e: Encoder

  trait Encoder {
    def write(): Unit = {
      println("Test")
    }
  }

}

trait AsyncEncoders {
  this: Context =>

  class AsyncEncoder extends Encoder {
  }

  implicit val e = new AsyncEncoder()
}

class ConcreteContext extends Context with AsyncEncoders {
}

When I use it like this (case 1):

object Main extends App {
  implicit val c = new ConcreteContext()

  import c._

  implicitly[Encoder].write()
}

then it compiles and prints Test.

But when I try to call the same code inside singleton object (case 2):

object TestObject {
  def apply()(implicit c: ConcreteContext): Unit = {
    import c._
    implicitly[Encoder].write()
  }
}

object Main extends App {
  implicit val c = new ConcreteContext()

  TestObject()
}

compilation fails with:

path/to/Main.scala:29: could not find implicit value for parameter e: c.Encoder implicitly[c.Encoder].write()

If I change (case 3):

implicit val e = new AsyncEncoder()

to

implicit val e: Encoder = new AsyncEncoder()

then it compiles and runs as expected.

But for some reason this is not acceptable for me.

Why does compilation fail in the above case?

2
I don't have an answer for you, but just to be clear, you're fighting against Scala here. Things would be much smoother, and the type inference paths more well-documented, if you implemented Encoder as a typeclass instead of a path-dependent type.Yawar
Seems to be having trouble with path-dependent types that come in the form of implicit parameters.Michael Zajac
I'm not 100% sure what is wrong here, but this does compile in Scala 2.12.0.Jasper-M
@Jasper-M Yes, it does compile in Scala 2.12.0. Thanks! So it's Scala 2.11.* compiler issue.mixel
Hmm, your Encoder trait is SAM and they've changed treatment of SAMs in 2.12. Not sure if this is the root cause why it compiles in 2.12 and errors in 2.11, but this is my first intuition.Piotr Krzemiński

2 Answers

0
votes

I would argue that the problem is not the fact that you use object, but that you are accepting ConcreteContext, as an argument: ConcreteContext.e's type is AsyncEncoder, not Encoder.

Many times I observed, that, when it comes to Scala, it is better to treat arguments as invariants unless specified otherwise (e.g. macwire often fails if you don't cast impl type to interface type - it is not completely predicatable though, most of the time it works).

As you observed setting e type explicitly to Encoder would solve the issue. So would changing ConcreteContext to Context. My guess is that it is either a matter of invariance or limitation of a compiler's type inference engine.

0
votes

As it said in comments there is no issue in Scala 2.12.0.

For Scala 2.11.8 I used the following workaround assuming that Encoder will have only one method:

trait Context {
  implicit val e: Encoder

  type BaseEncoder = () => Unit

  type Encoder <: BaseEncoder
}

trait AsyncEncoders {
  this: Context =>

  type Encoder = AsyncEncoder

  class AsyncEncoder extends BaseEncoder {
    override def apply(): Unit = {
      println("Test")
    }
  }

  implicit val e = new AsyncEncoder()
}

class ConcreteContext extends Context with AsyncEncoders {
}

object TestObject {
  def apply()(implicit c: ConcreteContext): Unit = {
    import c._
    implicitly[Encoder].apply()
  }
}

object Main extends App {
  implicit val c = new ConcreteContext()

  TestObject()
}