scalashapeless

Get case class parameter types as a HList


I'm trying to generate instances of case class using shapeless

This works for generating instances of Foo

case class Foo(x: Int, y: String)

class Context {
  val random = new Random()
}

def genInt(context: Context): Int = {
  context.random.nextInt()
}

def genString(context: Context): String = {
  context.random.nextString(16)
}

object ClassesToGenerators extends Poly1 {
  implicit def caseInt = at[Class[Int]](_ => genInt(_))
  implicit def caseString = at[Class[String]](_ => genString(_))
}


val gen = Generic[Foo]
val context = new Context()
val classes = classOf[Int] :: classOf[String] :: HNil // can't figure out how to get this hlist programmatically
val generators = classes.map(ClassesToGenerators)

gen.from(generators.zipApply(generators.mapConst(context)))

However, I'm aiming to write something reusable like

def newInstance[T] -> T:
  ???

which could generate instances of any case classes that takes only int and string parameters.

As mentioned in the code snippet, I'm stuck at getting a hlist of case class attribute types i.e. would like to convert case class Foo(x: Int, y: String) to classOf[Int] :: classOf[String] :: HNil. Any other approaches to this problem are also very appreciated but I'm not looking for a generic way of generating random instances of cases classes (as my use-case is different and used random generator just as an example)


Solution

  • IMHO, Shapeless is better used when you forget about all the fancy stuff and just focus on simple typeclass derivation using HList like this:

    import shapeless.{Generic, HList, HNil, :: => :!:}
    import scala.util.Random
    
    trait Context {
      def random: Random
    }
    object Context {
      object implicits {
        implicit final val global: Context = new Context {
          override final val random: Random = new Random() 
        }
      }
    }
    
    trait Generator[A] {
      def generate(context: Context): A
    }
    object Generator {
      final def apply[A](implicit ev: Generator[A]): ev.type = ev
      
      final def generate[A](implicit ev: Generator[A], ctx: Context): A =
        ev.generate(ctx)
      
      implicit final val IntGenerator: Generator[Int] =
        new Generator[Int] {
          override def generate(context: Context): Int =
            context.random.nextInt()
        }
      
      implicit final val StringGenerator: Generator[String] =
        new Generator[String] {
          override def generate(context: Context): String =
            context.random.nextString(16)
        }
      
      implicit final def auto[P <: Product](implicit ev: GeneratorGen[P]): Generator[P] = ev
    }
    
    sealed trait GeneratorRepr[R <: HList] extends Generator[R]
    object GeneratorRepr {
      implicit final val HNilGeneratorRepr: GeneratorRepr[HNil] =
        new GeneratorRepr[HNil] {
          override def generate(context: Context): HNil =
            HNil
        }
      
      implicit final def HConsGeneratorRepr[E, T <: HList](
        implicit ev: Generator[E], tail: GeneratorRepr[T]
      ): GeneratorRepr[E :!: T] =
        new GeneratorRepr[E :!: T] {
          override def generate(context: Context): E :!: T =
            ev.generate(context) :: tail.generate(context)
        }
    }
    
    sealed trait GeneratorGen[P <: Product] extends Generator[P]
    object GeneratorGen {
      implicit final def instance[P <: Product, R <: HList](
        implicit gen: Generic.Aux[P, R], ev: GeneratorRepr[R]
      ): GeneratorGen[P] = new GeneratorGen[P] {
        override def generate(context: Context): P =
          gen.from(ev.generate(context))
      }
    }
    

    Which can be used like this:

    import Context.implicits.global
    
    final case class Foo(x: Int, y: String)
    
    val result = Generator.generate[Foo]
    // result: Foo = Foo(-2127375055, "鞰Ϗƨ⹼沺㗝䚮Ⴍ욏ꖱꬮӝ闉믃雦峷")
    

    You can see the code running here.