In my first post I explained how shapeless can
- Transform a case class to an Hlist
- Concatenate hlists
- Transform an hlist back to another case class
There are some serious limitations to our previous implementation:
We need to ensure the case class fields are ordered correctly. If the order of the fields change the code will either not compile (with LabelledGeneric); or worse it may compile and run but return the wrong date. We really shouldn’t need to worry about the ordering of case class fields
If the Location, Vehicle and Driver case classes contain too many fields it also won’t compile. Equally, we assume all the information needed to build a Reservation instance is contained in the 3 case classes we merge. If a field is missing it won’t compile
The updated ADTs
6-updated-adts
tagIn this post I will address all these shortcomings. Let’s assume that our location class now looks like this (note how I swapped the order of the last two fields):
case class Location(pickup: String, dropOff: String, to: LocalDate, from: LocalDate)
Our Car now includes some additional fields not needed to build a reservation:
case class Vehicle(vehicleCategory: String, automatic: Boolean, numDoors: Int, isDiesel: Boolean)
Finally we’ll add an additional field to our Reservation class:
case class Reservation(
pickup: String,
dropOff: String,
from: LocalDate,
to: LocalDate,
vehicleCategory: String,
automatic: Boolean,
numDoors: Int,
driverAge: Int,
nationality: String,
confirmed: Boolean // this is new
)
We can see our previous code now doesn’t even compile, we’ll fix it step by step.
Dropping a field
7-pending-alignment
tagsLet’s drop the isDiesel
field from the Vehicle as we dont need it:
First we need an additional import import shapeless.record._
, Once we have this we can drop the isDiesel
field
from the generated vehicle hlist:
val (_, vehicleRepr) = VehicleGen.to(vehicle).remove('isDiesel)
remove
returns the element that was removed and the updated hlist as a tuple. Notice how shapeless uses a symbol
and this operation is also checked at compile time. If we try to remove a non existent field the compiler will complain:
val (_, vehicleRepr) = VehicleGen.to(vehicle).remove('isPetrol)
Error:(20, 66) No field Symbol with shapeless.tag.Tagged[String("isPetrol")] in record ...
Adding a field
We also need to add the confirmed
flag to the final hlist before converting to a Reservation instance. Again we start
with another import: import shapeless.syntax.singleton._
Ad hoc fields are created using a (fieldName ->> value)
syntax. Once we have our field we can add it to the
hlist:
val misalignedReservationRepr = locationRepr ++ vehicleRepr ++ driverRepr :+ ('confirmed ->> true)
At this point we have all the fields we need to create a Reservation but they’re in the wrong order.
Aligning the types
8-aligned-fields
tagsWe can ask Shapeless to align the fields so the hlist is in the correct order to build a Reservation. Conceptually this is quite simple but we need to do a few tricks to let the compiler infer the HList types for us.
Shapeless includes a type class called Align
which is parameterized on the source and target HList types. i.e. to
summon an instance we should use Align[A, B]
where A is the type of misalignedReservationRepr
and B is
ReservationGen.Repr
. But what is “the type of misalignedReservationRepr”? We know what is is, but we don’t have
a static type or alias for it. We’ll use a trick to infer this for us:
object ShapelessOps {
import shapeless._
import shapeless.ops.hlist
// 1
implicit class AlignerOps[ARepr <: HList](a: ARepr) {
def as[B](implicit aligner: Aligner[ARepr, B]): B = aligner.apply(a)
}
// 2
trait Aligner[A, B] {
def apply(a: A): B
}
// 3
implicit def genericAligner[ARepr <: HList, B, BRepr <: HList](
implicit
bGen : LabelledGeneric.Aux[B, BRepr],
align : hlist.Align[ARepr, BRepr]
): Aligner[ARepr, B] = new Aligner[ARepr, B] {
def apply(a: ARepr): B = bGen.from(align.apply(a))
}
}
Let’s decompose this:
We create an implicit class to “pimp” an HList, adding an
as[B]
method. This method requires an implicitAligner
instance that we defineOur Aligner trait simply says convert from A to B. So far there’s nothing interesting happening. The real magic happens in our
genericAligner
method:Our genericAligner method uses a shapeless
Align
instance to perform the alignment and therefore build anAligner
. We have the source and target types (ARepr, BRepr and B) so we can summon an instance ofAlign
We can now transform our misalignedReservationRepr
to a Reservation by calling .as[Reservation]
on it
The final implementation
So our final implementation looks like this:
object ShapelessOps {
import shapeless._
import shapeless.ops.hlist
// 1
implicit class AlignerOps[ARepr <: HList](a: ARepr) {
def as[B](implicit aligner: Aligner[ARepr, B]): B = aligner.apply(a)
}
// 2
trait Aligner[A, B] {
def apply(a: A): B
}
// 3
implicit def genericAligner[ARepr <: HList, B, BRepr <: HList](
implicit
bGen : LabelledGeneric.Aux[B, BRepr],
align : hlist.Align[ARepr, BRepr]
): Aligner[ARepr, B] = new Aligner[ARepr, B] {
def apply(a: ARepr): B = bGen.from(align.apply(a))
}
}
object Main {
import java.time.LocalDate
import shapeless.LabelledGeneric
import shapeless.record._
import shapeless.syntax.singleton._
def main(args: Array[String]): Unit = {
import ShapelessOps._
// Our case class representations of the data
// See the updated case class declaration for Location
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 LocationGen = LabelledGeneric[Location]
val VehicleGen = LabelledGeneric[Vehicle]
val DriverGen = LabelledGeneric[Driver]
val ReservationGen = LabelledGeneric[Reservation]
val locationRepr = LocationGen.to(location)
val (_, vehicleRepr) = VehicleGen.to(vehicle).remove('isDiesel)
val driverRepr = DriverGen.to(driver)
val misalignedReservationRepr = locationRepr ++ vehicleRepr ++ driverRepr :+ ('confirmed ->> true)
// closer!
/*_*/ val reservation: Reservation = misalignedReservationRepr.as[Reservation] /*_*/
println(s"from date: ${reservation.from}")
}
}
There’s still much more we can do. The code is not as generic as it could be and it’s pretty verbose. In the final post in this series I will clean up the code