Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Automatically convert a case class to an extensible record in shapeless?

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.

like image 250
mushroom Avatar asked Nov 22 '15 18:11

mushroom


1 Answers

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

like image 76
Travis Brown Avatar answered Nov 03 '22 01:11

Travis Brown