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
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