rau
rau

Reputation: 107

why does scala's implicit lookup ignore companion object of nested class

I was playing around with the following piece of code:

class A
class B
class C

trait Codecs[L] {
    case class Codec[R](val code: L => R, val decode: R => L)
    object Codec

    def code[R](foo: L)(implicit codec: Codec[R]): R = codec.code(foo)
    def decode[R](bar: R)(implicit codec: Codec[R]): L = codec.decode(bar)
}

object Codecs {
    implicit object ACodecs extends Codecs[A] {
        object Codec {
            implicit val ab: Codec[B] = new Codec(_ => new B, _ => new A)
            implicit val ac: Codec[C] = new Codec(_ => new C, _ => new A)
        }
    }
}

object test extends App {
    val codecs = implicitly[Codecs[A]]
    codecs.code[B](new A)
} 

It won't compile, as the compiler is unable to find an implicit value of type Codecs.Codec[B]. As I understand, the two values ab and ac are of type Acodecs.Codec[_](or something like that), which isn't exactly what the compiler is looking for. I am also aware that moving the case class Codec[_] and its companion outside of the trait solves the problem (after making it take 2 type params). If an implicit value is required, the compiler should include the companion object of the required type in the implicit scope. My questions are:

  1. How do I point the compiler at the companion of a path-dependent subtype, more specifically:
  2. Is it possible to alter the signature of the two methods of the trait (ideally alter the type signature of the implicit param) to make this compile? How would one refer to the type Acodecs.Codec[_] from inside the trait Codecs[_]?
  3. Like, how do you do this typeclass thing on a nested type?

  4. Is there like a pattern or something dealing with this sort of problem?

Upvotes: 3

Views: 427

Answers (1)

Joe K
Joe K

Reputation: 18424

The issue is that your type is bound to a specific instance since it's an inner class. And the compiler doesn't know that implicitly[Codecs[A]] is giving the exact same instance as what it's finding implicitly on the next line. For instance, if you pass it explicitly:

codecs.code[B](new A)(Codecs.ACodecs.Codec.ab)

You get this error message:

type mismatch;
 found   : Codecs.ACodecs.Codec[B]
 required: codecs.Codec[B]

So it believes the enclosing instances to be possibly different, and so different types.

I've never really seen this specific kind of nesting of implicits - i.e. an implicit typeclass with path-dependent implicit typeclasses within it. So I doubt there's a pattern for dealing with it and would in fact kind of recommend against it. It seems overly complicated. Here's how I would personally treat this case:

case class Codec[L, R](val code: L => R, val decode: R => L)

trait Codecs[L] {
  type LocalCodec[R] = Codec[L, R]

  def code[R](foo: L)(implicit codec: LocalCodec[R]): R = codec.code(foo)
  def decode[R](bar: R)(implicit codec: LocalCodec[R]): L = codec.decode(bar)
}

object Codecs {
  implicit object ACodecs extends Codecs[A] {
    implicit val ab: LocalCodec[B] = new LocalCodec(_ => new B, _ => new A)
    implicit val ac: LocalCodec[C] = new LocalCodec(_ => new C, _ => new A)
  }
}

object test extends App {
  import Codecs.ACodecs._
  val codecs = implicitly[Codecs[A]]    
  codecs.code[B](new A)
}

You still get the benefit of a "half-narrowed" type to work with but it's just a type alias so there's no path-dependency issues.

Upvotes: 2

Related Questions