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

后端 未结 1 1128
不知归路
不知归路 2021-02-06 09:19

If I have these two case classes:

case class Address(street : String, zip : Int)
case class Person(name : String, address : Address)

and an ins

相关标签:
1条回答
  • 2021-02-06 10:06

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

    0 讨论(0)
提交回复
热议问题