milessabin / shapeless

Generic programming for Scala

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Inconsistency in handling of case classes with private vals

travisbrown opened this issue · comments

This is related to #768 but the problem is different. Suppose we have some case classes:

case class Foo(private val a: Int)
case class Bar(private val a: Int, b: Int)
case class Baz(a: Int, private val b: String)
case class Qux(private val a: Int, b: String)

We get LabelledGeneric instances for all but the last:

scala> LabelledGeneric[Foo]
res0: shapeless.LabelledGeneric[Foo]{type Repr = Int with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("a$access$0")],Int] :: shapeless.HNil} = shapeless.LabelledGeneric$$anon$1@eba39b2

scala> LabelledGeneric[Bar]
res1: shapeless.LabelledGeneric[Bar]{type Repr = Int with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("b")],Int] :: Int with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("a$access$0")],Int] :: shapeless.HNil} = shapeless.LabelledGeneric$$anon$1@685c20ab

scala> LabelledGeneric[Baz]
res2: shapeless.LabelledGeneric[Baz]{type Repr = Int with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("a")],Int] :: String with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("b$access$1")],String] :: shapeless.HNil} = shapeless.LabelledGeneric$$anon$1@16da3286

scala> LabelledGeneric[Qux]
                      ^
       error: could not find implicit value for parameter lgen: shapeless.LabelledGeneric[Qux]

The generalisation seems to be that if the first member is private, and there is another member with a different type, the instance isn't available. The same pattern holds for Generic.

This inconsistency was showing up in Circe's semiauto derivation and was reported by @ohze there.

I also think it's related to #768, specifically the logic of alignFieds is probably tripping on this:

def alignFields(tpe: Type, args: List[(TermName, Type)]): Option[List[(TermName, Type)]] = for {
fields <- Option(fieldsOf(tpe))
if fields.size == args.size
if fields.zip(args).forall { case ((fn, ft), (an, at)) =>
(fn == an || at.typeSymbol == definitions.ByNameParamClass) && ft =:= unByName(at)
}
} yield fields

Some background:

  • Why can't we just lookup the fields by name?
    We want to support byname parameters like this:

    class Foo(bar0: => String) {
      lazy val bar: String = bar0
    }
  • Why does it have to be so strict?
    We want to prevent unsound cases where we have a mismatch in the order of fields with the same type.

But in the case of synthetic accessor methods, the names are different.

Actually it happens even earlier:

tpe.decls.sorted collect {

    /** Sorts the symbols included in this scope so that:
     *    1) Symbols appear in the linearization order of their owners.
     *    2) Symbols with the same owner appear in same order of their declarations.
     *    3) Synthetic members (e.g. getters/setters for vals/vars) might appear in arbitrary order.
     */
    def sorted: List[Symbol]