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)
nN.f(n)\forall n \in \mathbb{N} .f(n)

If we can prove the base case:

f(0)
f(0)f(0)

and the inductive step:

\forall n \in \mathbb{N} .f(n) \Rightarrow f(n+1)
nN.f(n)f(n+1)\forall n \in \mathbb{N} .f(n) \Rightarrow 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,086