Search code examples
scalashapelesspath-dependent-typesingleton-type

Build, using Shapeless, generic default instances for case classes with parameters defining a common createValue method


I'm trying to achieve the following - and using shapeless seems like a good path.

Given the current class model:

import shapeless._

object ShapelessTest {
  case class Definition[T](id: String) extends Typeable[T] {
    type V = Value[T]

    override def cast(t: Any): Option[T] = createValue(t.asInstanceOf[Option[T]]).value

    override def describe: String = s"$id"

    def createValue(value: Option[T]): V =
      Value[T](this, value)
  }

  case class Value[T](definition: Definition[T], value: Option[T])

  val DefA: Definition[Int] = Definition[Int]("defA")
  val DefB: Definition[String] = Definition[String]("defB")

  case class Instance(valA: DefA.V,
                      valB: DefB.V)

  def main(args: Array[String]): Unit = {
     val Empty: Instance = Instance(
       DefA.createValue(None),
       DefB.createValue(None)
     )
    println(s"Empty manual: $Empty")

    val emptyHl = Generic[Instance].from(DefA.createValue(None) :: DefB.createValue(None) :: HNil)
    println(s"Empty hlist: $emptyHl")
  }
}

I can create an empty instance as the Empty instance, by manually invoking the createValue methods on the correct Definitions, or by converting an HList using shapeless.

I'm trying to figure out if it's possible to programmatically create an Empty instance for every class having fields of type Value.

In other words, I'd like to be able to invoke

val Empty: Instance = empty[Instance]

and have the same result of the emptyHl or Empty instances.

This seems similar to the "8.3 Random Value Generator" example in the shapeless guide, but instead of generating random numbers, using functions for every type present in the case class, I'm trying to materialize the concrete Definition type for every parameter, and invoke the createValue(None) method on it.

I've been trying quite a bit without success.

Using a hlist.Mapper with a Poly1 defined over a Typeable, I'm able to get a list of the parameters, but I'm not able to invoke any methods on the typeable.

Any help would be greatly appreciated, thanks! :)


Update 9 Apr

I was able to come up with a very convoluted solution - unfortunately a lot of casting but does the job.

I would like to iterate over this and make it better. I tried using the natMapper: NatTRel but I wasn't able to make it work over Singleton Types. I'm sure this can be made a lot better though! Any suggestion is welcome.

import shapeless.ops.hlist
import shapeless.ops.hlist.{Comapped, Reify}
import shapeless.{Generic, HList, HNil}

object ShapelessTest2 {

  case class Definition[T](id: String) {
    type V = Value[this.type]

    def createValue(value: Option[T]) =
      new Value[this.type] {
        type NT = T
        override val valueT: Option[T] = value
        override val attrDef: Definition.this.type = Definition.this
      }
  }

  trait Value[D] {
    type NT
    val attrDef: D
    val valueT: Option[NT]
  }

  object DefA extends Definition[Int]("defA")
  object DefB extends Definition[Int]("defB")
  object DefC extends Definition[String]("defC")

  case class Instance(valA: DefA.V,
                      valB: DefB.V,
                      valC: DefC.V)

  // Compile safe
  val Inst1: Instance = Instance(
    DefA.createValue(Some(1)),
    DefB.createValue(Some(2)),
    DefC.createValue(Some("2"))
  )

  def main(args: Array[String]): Unit = {
    def empty[A <: Product] = new PartiallyApplied[A]

    class PartiallyApplied[A <: Product] {
      def apply[
          V <: HList,
          DL <: HList,
          RDL <: HList,
          H <: Definition[_],
          T <: HList
      ]()(
          implicit
          gen: Generic.Aux[A, V],
          comapped: Comapped.Aux[V, Value, DL],
          reify: Reify.Aux[DL, RDL],
          isHCons: hlist.IsHCons.Aux[RDL, H, T],
      ): A = {
        def getEmpties[L](list: RDL): V = {
          val hlist = list match {
            case HNil => HNil
            case _ => list.head.createValue(None) :: getEmpties(list.tail.asInstanceOf[RDL])
          }
          hlist.asInstanceOf[V]
        }

        val empties = getEmpties(reify.apply())
        gen.from(empties)
      }
    }

    val emptyInstance = empty[Instance]()
    println(s"Empty valA: ${emptyInstance.valA.attrDef} - ${emptyInstance.valA.valueT}")
    println(s"Empty valB: ${emptyInstance.valB.attrDef} - ${emptyInstance.valB.valueT}")
    println(s"Empty valC: ${emptyInstance.valC.attrDef} - ${emptyInstance.valC.valueT}")
  }
}

Correctly prints

Empty valA: Definition(defA) - None
Empty valB: Definition(defB) - None
Empty valC: Definition(defC) - None

Solution

  • I guess you somehow abuse Typeable. The idea of using Typeable is to have type-safe cast. But you come back to asInstanceOf.

    Typeable is a type class. So you should use your Definition as a type class. Make DefA, DefB, ... implicit.

    implicit val DefA: Definition[Int] = Definition[Int]("defA")
    implicit val DefB: Definition[String] = Definition[String]("defB")
    
    def empty[A <: Product] = new PartiallyApplied[A]
    
    class PartiallyApplied[A <: Product] {
      def apply[Vs <: HList, L <: HList, Ds <: HList]()(implicit
        gen: Generic.Aux[A, Vs],
        comapped: Comapped.Aux[Vs, Value, L],
        liftAll: LiftAll.Aux[Definition, L, Ds],
        natMapper: NatTRel[Ds, Definition, Vs, Value],
      ): A = {
        object createValueNatTransform extends (Definition ~> Value) {
          override def apply[T](definition: Definition[T]): Value[T] =
            definition.createValue(None)
        }
    
        gen.from(natMapper.map(createValueNatTransform, liftAll.instances))
      }
    }
    
    val Empty: Instance = empty[Instance]() 
    // Instance(Value(Typeable[defA],None),Value(Typeable[defB],None))
    

    Macro working with your original code

    def empty[A]: A = macro emptyImpl[A]
    
    def emptyImpl[A: c.WeakTypeTag](c: blackbox.Context): c.Tree = {
      import c.universe._
      val typA = weakTypeOf[A]
      val trees = typA.decls.filter(_.asTerm.isVal).map(_.infoIn(typA) match {
        case TypeRef(pre, _, _) => q"${pre.termSymbol}.createValue(_root_.scala.None)"
      })
      q"new $typA(..$trees)"
    }
    
    val Empty: Instance = empty[Instance]
    
    //Warning:scalac: performing macro expansion App.empty[App.Instance]
    //Warning:scalac: new App.Instance(DefA.createValue(_root_.scala.None),
    //                                 DefB.createValue(_root_.scala.None))
    
    // Instance(Value(Typeable[defA],None),Value(Typeable[defB],None))
    

    Regarding your new code make Value covariant and use Mapper (for properly defined Poly) instead of NatTRel or runtime recursion

    trait Value[+D] {
      type NT
      val attrDef: D
      val valueT: Option[NT]
    }
    
    object createValuePoly extends Poly1 {
      implicit def cse[D <: Definition[T] with Singleton, T](implicit
        ev: D <:< Definition[T]): Case.Aux[D, Value[D]] = at(_.createValue(None))
    }
    
    def empty[A <: Product] = new PartiallyApplied[A]
    
    class PartiallyApplied[A <: Product] {
      def apply[
          V <: HList,
          DL <: HList,
      ]()(
          implicit
          gen: Generic.Aux[A, V],
          comapped: Comapped.Aux[V, Value, DL],
          reify: Reify.Aux[DL, DL],
          mapper: Mapper.Aux[createValuePoly.type, DL, V]
      ): A = gen.from(mapper(reify()))
    }
    
    val emptyInstance = empty[Instance]()
    println(s"Empty valA: ${emptyInstance.valA.attrDef} - ${emptyInstance.valA.valueT}") 
    //Empty valA: Definition(defA) - None
    println(s"Empty valB: ${emptyInstance.valB.attrDef} - ${emptyInstance.valB.valueT}") 
    //Empty valB: Definition(defB) - None
    println(s"Empty valC: ${emptyInstance.valC.attrDef} - ${emptyInstance.valC.valueT}") 
    //Empty valC: Definition(defC) - None