Massaging case classes with shapeless
Agenda
- Context - DDD
- HList
- Generic
- LabelledGeneric
- More complex transformations
- Chimney
DDD in one slide
Domain
Translation
HTTP API
Database
Translation
List
val xs: List[Int] = List(1, 2, 3)
HList
val hlist: String :: Int :: Boolean :: HNil =
"yeah" :: 123 :: true :: HNil
Generic
"yeah" :: 123 :: true :: HNil
case class DomainModel(
a: String, b: Int, c: Boolean)
case class ApiRepresentation(
x: String, y: Int, z: Boolean)
Generic
scala> import shapeless._
import shapeless._
scala> val model = DomainModel("yeah", 123, true)
model: DomainModel = DomainModel(yeah,123,true)
scala> val hlist = Generic[DomainModel].to(model)
hlist: String :: Int :: Boolean :: shapeless.HNil =
yeah :: 123 :: true :: HNil
scala> val apiRepr =
| Generic[ApiRepresentation].from(hlist)
apiRepr: ApiRepresentation =
ApiRepresentation(yeah,123,true)
LabelledGeneric
("a" ->> "yeah") :: ("b" ->> 123) :: ("c" ->> true) :: HNil
case class DomainModel(
a: String, b: Int, c: Boolean)
case class ApiRepresentation(
x: String, y: Int, z: Boolean)
LabelledGeneric
scala> LabelledGeneric[DomainModel].to(model)
res0:
String with shapeless.labelled.KeyTag[
Symbol with shapeless.tag.Tagged[String("a")],
String
] ::
Int with shapeless.labelled.KeyTag[
Symbol with shapeless.tag.Tagged[String("b")],
Int
] ::
Boolean with shapeless.labelled.KeyTag[
Symbol with shapeless.tag.Tagged[String("c")],
Boolean
] ::
shapeless.HNil =
yeah :: 123 :: true :: HNil
... crazy type signature ...
LabelledGeneric
scala> LabelledGeneric[DomainModel].to(model)
res0:
String with shapeless.labelled.KeyTag[
Symbol with shapeless.tag.Tagged[String("a")],
String
] ::
Int with shapeless.labelled.KeyTag[
Symbol with shapeless.tag.Tagged[String("b")],
Int
] ::
Boolean with shapeless.labelled.KeyTag[
Symbol with shapeless.tag.Tagged[String("c")],
Boolean
] ::
shapeless.HNil =
yeah :: 123 :: true :: HNil
More complex transformations
- Removing fields
- Types don't exactly match
Removing fields
case class FiveFields(a: String,
b: Int,
c: Boolean,
d: String,
e: Double)
case class FourFields(a: String,
b: Int,
c: Boolean,
e: Double)
("a"->>"yep")::("b"->>1)::("c"->>true)::("d"->>"wow")::("e"->>4.5)::HNil
("a"->>"yep")::("b"->>1)::("c"->>true):: ("e"->>4.5)::HNil
Removing fields
scala> val fiveFields =
| FiveFields("yeah", 123, true, "wow", 4.5)
fiveFields: ... = FiveFields(yeah,123,true,wow,4.5)
scala> val fiveFieldsHList =
| LabelledGeneric[FiveFields].to(fiveFields)
fiveFieldsHList: ...
= yeah :: 123 :: true :: wow :: 4.5 :: HNil
scala> import shapeless.record._
import shapeless.record._
scala> val fourFieldsHList = fiveFieldsHList - 'd
fourFieldsHList: ...
= yeah :: 123 :: true :: 4.5 :: HNil
Types don't match
case class DomainModel(a: String,
b: Int,
c: Boolean)
case class ApiRepr(a: String,
b: Option[Int],
c: Option[Boolean])
("a"->>"yep") :: ("b"->>1) :: ("c"->>true) :: HNil
("a"->>"yep") :: ("b"->>Some(1)) :: ("c"->>Some(true)) :: HNil
Types don't match
String :: Int :: Boolean :: HNil
String :: Option[Int] :: Option[Boolean] :: HNil
(do nothing)
(wrap in Some)
Polymorphic function
object WrapWithOptionIfNecessary extends Poly1 {
// Reflexively convert any field to itself
// by doing nothing
implicit def refl[A]: Case.Aux[A, A] =
at[A](Predef.identity)
// Convert a field `x: A` into a field `x: Option[A]`
// by wrapping it in Some()
implicit def wrapWithOption[K, V]:
Case.Aux[FieldType[K, V], FieldType[K, Option[V]]] =
at[FieldType[K, V]](x => field[K](Some(x)))
}
Massage
/**
* Massages an HList of type `From`
* into an HList of type `To`
* using `WrapWithOptionIfNecessary`
* to convert elements appropriately.
*/
trait Massage[From <: HList, To <: HList] {
def apply(from: From): To
}
Inductive proof
\forall n \in \mathbb{N} .f(n)
∀n∈N.f(n)
If we can prove the base case:
f(0)
f(0)
and the inductive step:
\forall n \in \mathbb{N} .f(n) \Rightarrow f(n+1)
∀n∈N.f(n)⇒f(n+1)
then we have proved it for all natural numbers:
Inductive proof
If we can prove the base case:
and the inductive step:
then we have proved it for some type From <: HList
there is a Massage instance for HNil
if there is a Massage instance for Tail
and we can convert Head
then there is a Massage instance for Head :: Tail
Chimney
Chimney
import io.scalaland.chimney._
implicit def wrapWithOption[A]: Transformer[A, Option[A]] =
new Transformer[A, Option[A]] {
def transform(a: A): Option[A] = Some(a)
}
Chimney
import io.scalaland.chimney.dsl._
val domainModel = DomainModel("yeah", 123, true)
val apiRepr = domainModel.into[ApiRepr].transform
Questions?
Massaging case classes with shapeless
By Chris Birchall
Massaging case classes with shapeless
- 3,123