Search code examples
scalashapelesstype-members

Does shapeless play well with type members?


I'm trying to use shapeless to derive a Generic for a type member defined in a trait, but am not having luck. I have made as simple of a reproduction of the issue as I can think of while keeping it close enough to the original code. I'm taking inspiration from this blog post, and trying to expand (bastardize) it to be a bit more generic. It probably won't make sense why I have code that looks like this from this example alone, but hopefully that doesn't take away from this question :)

I have a trait that declares a type member, a case class representing some common set of fields, and another wrapper case class that combines an instance of both:

object A {
    trait TheTrait {
        type TheType
    }

    case class CommonFields(height: Double, isTall: Boolean)
    case class Wrapper[T <: TheTrait](commonFields: CommonFields, t: T#TheType)
}

I also have an implementation of the trait:

trait Obj extends TheTrait
object Obj extends Obj {
    case class Source(name: String, other: Int)

    override type TheType = Source 
}

My goal is to be able to take a tuple with values for both CommonFields and TheTrait#TheType for some instance of TheTrait, and to use shapeless to turn that into an instance of a Wrapper. So for the example so far, I'd like to go from something like (5.1, false, "sub", 10) to Wrapper[Obj](CommonFields(5.1, false), Source("other", 10)). Here's what I've come up with:

object Test {
    class Constructor[T <: TheTrait] {
        // take a tuple of all the fields of CommonFields and T#Trait and produce an instance of each in a A.Wrapper
        def apply[In <: Product, All <: HList, ORep <: HList, CRep <: HList, N <: Nat](in: In)(implicit
            cGen: Generic.Aux[A.CommonFields, CRep], // generic for CommonFields
            cLen: Length.Aux[CRep, N], // the length of the CommonFields generic HList
            trGen: Generic.Aux[T#TheType, ORep], // generic for T#TheType
            iGen: Generic.Aux[In, All], // generic for input tuple
            split: Split.Aux[All, N, CRep, ORep] // the input tuple, split at N, produces HLists for the CommonFields generic rep as well as for the T#TheType generic rep
        ): A.Wrapper[T] = {
            val all = iGen.to(in)
            val (cFields, tFields) = split(all)
            val com = cGen.from(cFields)
            val tr = trGen.from(tFields)
            
            A.Wrapper(com, tr)
        }
    }
    def construct[T <: TheTrait] = new Constructor[T]
    
    println(construct[Obj](5.1, false, "sub", 10))
}

Unfortunately, the correct implicits cannot be found, in particular I see the following error: No implicit arguments of type: hlist.Split.Aux[Double :: Boolean :: String :: Int :: HNil, Succ[Succ[shapeless.nat._0]], Double :: Boolean :: HNil, HNil] It seems like it is finding the right generic representation for CommonFields (by the appearances of Double :: Boolean :: HNil in the error), but cannot tell what the TheType should be. Is this asking too much of shapeless/the scala compiler? Can I give more type hints somewhere? Is there another way to achieve something like this? I can try to expand on why I have created a type structure like this if that would be helpful. Any ideas are appreciated!

EDIT:

Just to experiment, I made a variation using path dependent typing instead of the type projection, but still was unable to get it to work:

object Test {
    import A._
    class Constructor[T <: TheTrait] {
        // take a tuple of all the fields of CommonFields and T#Trait and produce an instance of each in a A.Wrapper
        def apply[In <: Product, All <: HList, ORep <: HList, CRep <: HList, N <: Nat](in: In, t: T /* <=== now takes a `T` instance */)(implicit
            cGen: Generic.Aux[CommonFields, CRep], // generic for CommonFields
            cLen: Length.Aux[CRep, N], // the length of the CommonFields generic HList
            trGen: Generic.Aux[t.TheType, ORep], // generic for T#TheType <==== no more type projection
            iGen: Generic.Aux[In, All], // generic for input tuple
            split: Split.Aux[All, N, CRep, ORep] // the input tuple, split at N, produces HLists for the CommonFields generic rep as well as for the T#TheType generic
        ): Wrapper[T] = {
            val all = iGen.to(in)
            val (cFields, tFields) = split(all)
            val com = cGen.from(cFields)
            val tr = trGen.from(tFields)
            
            Wrapper(com, tr)
        }
    }
    def construct[T <: TheTrait] = new Constructor[T]

    println(
        construct[Obj]((5.1, false, "sub", 10), Obj) // <== passing a `TheTrait` instance
    )
}

But still seeing the error

No implicit arguments of type: hlist.Split.Aux[Double :: Boolean :: String :: Int :: HNil, Succ[Succ[shapeless.nat._0]], Double :: Boolean :: HNil, HNil]

EDIT 2: Rearranging the implicits has helped a tad. Instead of the compiler believing that ORep is HNil, it is now at least looking for it to match String :: Int :: HNil:

    class Constructor[T <: TheTrait] {
        // take a tuple of all the fields of CommonFields and T#Trait and produce an instance of each in a A.Wrapper
        def apply[In <: Product, All <: HList, ORep <: HList, CRep <: HList, N <: Nat](in: In, t: T)(implicit
            cGen: Generic.Aux[CommonFields, CRep], // generic for CommonFields
            cLen: Length.Aux[CRep, N], // the length of the CommonFields generic HList
            iGen: Generic.Aux[In, All], // generic for input tuple
            split: Split.Aux[All, N, CRep, ORep], // the input tuple, split at N, produces HLists for the CommonFields generic rep as well as for the T#TheType generic
            trGen: Generic.Aux[t.TheType, ORep] // generic for T#TheType
        ): Wrapper[T] = {
            val all = iGen.to(in)
            val (cFields, tFields) = split(all)
            val com = cGen.from(cFields)
            val tr = trGen.from(tFields)

            Wrapper(com, tr)
        }
    }
    def construct[T <: TheTrait] = new Constructor[T]

The error now is No implicit arguments of type: Generic.Aux[B.Obj.TheType, String :: Int :: HNil], which feels like progress to me.

I can now actually get the program to compile by explicitly creating an instance of the Generic it is looking for and making it available implicitly:

    implicit val objTypeGen: Generic.Aux[Obj.TheType, String :: Int :: HNil] = Generic.instance[Obj.TheType, String :: Int :: HNil](
        t => t.name :: t.other :: HNil,
        {
            case n :: o :: HNil => Obj.Source(n, o)
        }
    )

But now I have removed all of the ergonomics I had originally set out to build. Hopefully there's enough hints here though for someone to figure out how to not need to explicitly pass a TheTrait instance or define the Generic manually?


Solution

  • Your original code compiles as soon as you move override type TheType = Source from object Obj to trait Obj. Note that construct[Obj](..) refers to the type (trait) Obj, not the singleton type corresponding to the object Obj, therefore in your code Obj#TheType cannot be resolved to a particular type, since it remains abstract.

    If you really need override type TheType = Source in object Obj, the invocation construct[Obj.type](..) will also compile.

    Edit. Full code:

    import shapeless.ops.hlist.{Length, Split}
    import shapeless.{Generic, HList, Nat}
    
    object Programme {
    
      object A {
        trait TheTrait {
          type TheType
        }
    
        case class CommonFields(height: Double, isTall: Boolean)
        case class Wrapper[T <: TheTrait](commonFields: CommonFields, t: T#TheType)
      }
    
      import A.TheTrait
    
      trait Obj extends TheTrait {
        override type TheType = Obj.Source
      }
    
      object Obj extends Obj {
        case class Source(name: String, other: Int)
      }
    
      object Test {
        class Constructor[T <: TheTrait] {
          def apply[In <: Product, All <: HList, ORep <: HList, CRep <: HList, N <: Nat](in: In)(implicit
              cGen: Generic.Aux[A.CommonFields, CRep],
              cLen: Length.Aux[CRep, N],
              trGen: Generic.Aux[T#TheType, ORep],
              iGen: Generic.Aux[In, All],
              split: Split.Aux[All, N, CRep, ORep]
          ): A.Wrapper[T] = {
            val all = iGen.to(in)
            val (cFields, tFields) = split(all)
            val com = cGen.from(cFields)
            val tr = trGen.from(tFields)
    
            A.Wrapper(com, tr)
          }
        }
        def construct[T <: TheTrait] = new Constructor[T]
      }
    
      import Test.construct
    
      def main(args: Array[String]): Unit = {
        println(construct[Obj](5.1, false, "sub", 10))
      }
    }