Toby Hobson

Copying fields using Shapeless - Part 3

banner.png
Copying fields using Shapeless - Part 3
Estimated reading time: 2 minutes

In my second post I explained how shapeless can add, remove and align fields from case classes to match another type. The code works, but it’s pretty clunky. In this post I will clean it up a bit to make it less verbose

To follow along it’s best to check out the accompanying source code from my Github repo and checkout the 9-cleanup tag

Automatically dropping unused fields

In the current implementation I need to manually drop the isDiesel flag from the Vehicle as it’s not needed by the Reservation case class. Shapeless can do this for us by comparing the expected fields and dropping anything that is not used. The type class is called Intersection and it’s usage is similar to the Align type class we already looked at. We will expand the ShapelessOps.genericAligner to use the new class:

implicit def genericAligner[ARepr <: HList, Unaligned <: HList, B, BRepr <: HList](
  implicit
  bGen    : LabelledGeneric.Aux[B, BRepr],
  inter   : Intersection.Aux[ARepr, BRepr, Unaligned], // NEW
  align   : Align[Unaligned, BRepr]
): Aligner[ARepr, B] = new Aligner[ARepr, B] {
  def apply(a: ARepr): B = bGen.from(align(inter(a)))
}

Intersection.Aux[ARepr, BRepr, Unaligned] basically means

take an intersection of ARepr and BRepr and expose a new type as Unaligned

We can then summon an Align instance to align Unaligned to BRepr. It’s a bit messy but Shapeless is about pushing the boundaries of the compiler. Some may even say we’re cheating the compiler!

Cleanup the code

We can also add a new implicit class to avoid the need for repeated LabelledGeneric[X] calls:

implicit class GenericOps[A, ARepr <: HList](val a: A)(implicit gen: LabelledGeneric.Aux[A, ARepr]) {
  def repr: ARepr = LabelledGeneric[A].to(a)
}

Our final code now looks much cleaner:

object Main {

  import ShapelessOps._
  import shapeless.syntax.singleton._

  def main(args: Array[String]): Unit = {
    val location = Location(pickup = "Malaga Airport", dropOff = "Malaga Airport", from = LocalDate.of(2018,8,1), to = LocalDate.of(2018,8,10))
    val vehicle = Vehicle(vehicleCategory = "Economy", automatic = false, numDoors = 4, isDiesel = true)
    val driver = Driver(driverAge = 35, nationality = "British")

    val misalignedReservationRepr = location.repr ++ vehicle.repr ++ driver.repr :+ ('confirmed ->> true)
    /*_*/ val reservation = misalignedReservationRepr.as[Reservation] /*_*/
    println(s"from date: ${reservation.from}")
  }

}
comments powered by Disqus

Need help with your project?

Do you need some help or guidance with your project? Reach out to me (email is best)