If I have these two case classes:
case class Address(street : String, zip : Int)
case class Person(name : String, address : Address)
and an instance:
val person = Person("Jane", Address("street address", 12345))
Is there a way in shapeless to automatically convert person
to an extensible record?
I am interested in both shallow and deep conversions.
The shallow copy would be something like:
'name ->> "Jane" :: 'address ->> Address("street address", 12345) :: HNil
In the deep conversion, the nested case class also becomes a record:
'name ->> "Jane" :: 'address ->> ('street ->> "street address" :: 'zip ->> 12345 :: HNil) :: HNil
I am also interested in converting records back to case classes.
Suppose we've got the following setup:
import shapeless._, shapeless.labelled.{ FieldType, field }
case class Address(street: String, zip: Int)
case class Person(name: String, address: Address)
val person = Person("Jane", Address("street address", 12345))
type ShallowPersonRec =
FieldType[Witness.`'name`.T, String] ::
FieldType[Witness.`'address`.T, Address] :: HNil
type DeepPersonRec =
FieldType[Witness.`'name`.T, String] ::
FieldType[
Witness.`'address`.T,
FieldType[Witness.`'street`.T, String] ::
FieldType[Witness.`'zip`.T, Int] :: HNil
] :: HNil
Shapeless's LabelledGeneric
supports the shallow case directly:
val shallow: ShallowPersonRec = LabelledGeneric[Person].to(person)
Or if you want a generic helper method:
def shallowRec[A](a: A)(implicit gen: LabelledGeneric[A]): gen.Repr = gen.to(a)
val shallow: ShallowPersonRec = shallowRec(person)
And you can go back with from
:
scala> val originalPerson = LabelledGeneric[Person].from(shallow)
originalPerson: Person = Person(Jane,Address(street address,12345))
The deep case is trickier, and as far as I know there's no convenient way to do this with the type classes and other tools provided by Shapeless, but you can adapt my code from this question (which is now a test case in Shapeless) to do what you want. First for the type class itself:
trait DeepRec[L] extends DepFn1[L] {
type Out <: HList
def fromRec(out: Out): L
}
And then a low-priority instance for the case where the head of the record doesn't itself have a LabelledGeneric
instance:
trait LowPriorityDeepRec {
type Aux[L, Out0] = DeepRec[L] { type Out = Out0 }
implicit def hconsDeepRec0[H, T <: HList](implicit
tdr: Lazy[DeepRec[T]]
): Aux[H :: T, H :: tdr.value.Out] = new DeepRec[H :: T] {
type Out = H :: tdr.value.Out
def apply(in: H :: T): H :: tdr.value.Out = in.head :: tdr.value(in.tail)
def fromRec(out: H :: tdr.value.Out): H :: T =
out.head :: tdr.value.fromRec(out.tail)
}
}
And then the rest of the companion object:
object DeepRec extends LowPriorityDeepRec {
def toRec[A, Repr <: HList](a: A)(implicit
gen: LabelledGeneric.Aux[A, Repr],
rdr: DeepRec[Repr]
): rdr.Out = rdr(gen.to(a))
class ToCcPartiallyApplied[A, Repr](val gen: LabelledGeneric.Aux[A, Repr]) {
type Repr = gen.Repr
def from[Out0, Out1](out: Out0)(implicit
rdr: Aux[Repr, Out1],
eqv: Out0 =:= Out1
): A = gen.from(rdr.fromRec(eqv(out)))
}
def to[A](implicit
gen: LabelledGeneric[A]
): ToCcPartiallyApplied[A, gen.Repr] =
new ToCcPartiallyApplied[A, gen.Repr](gen)
implicit val hnilDeepRec: Aux[HNil, HNil] = new DeepRec[HNil] {
type Out = HNil
def apply(in: HNil): HNil = in
def fromRec(out: HNil): HNil = out
}
implicit def hconsDeepRec1[K <: Symbol, V, Repr <: HList, T <: HList](implicit
gen: LabelledGeneric.Aux[V, Repr],
hdr: Lazy[DeepRec[Repr]],
tdr: Lazy[DeepRec[T]]
): Aux[FieldType[K, V] :: T, FieldType[K, hdr.value.Out] :: tdr.value.Out] =
new DeepRec[FieldType[K, V] :: T] {
type Out = FieldType[K, hdr.value.Out] :: tdr.value.Out
def apply(
in: FieldType[K, V] :: T
): FieldType[K, hdr.value.Out] :: tdr.value.Out =
field[K](hdr.value(gen.to(in.head))) :: tdr.value(in.tail)
def fromRec(
out: FieldType[K, hdr.value.Out] :: tdr.value.Out
): FieldType[K, V] :: T =
field[K](gen.from(hdr.value.fromRec(out.head))) ::
tdr.value.fromRec(out.tail)
}
}
(Note that the DeepRec
trait and object must be defined together to be companioned.)
This is messy, but it works:
scala> val deep: DeepPersonRec = DeepRec.toRec(person)
deep: DeepPersonRec = Jane :: (street address :: 12345 :: HNil) :: HNil
scala> val originalPerson = DeepRec.to[Person].from(deep)
originalPerson: Person = Person(Jane,Address(street address,12345))
The to
/ from
syntax for the conversion back to the case class is necessary because any given record could correspond to a very large number of potential case classes, so we need to be able to specify the target type, and since Scala doesn't support partially-applied type parameter lists, we have to break up the operation into two parts (one of which will have its types specified explicitly, while the type parameters for the other will be inferred).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With