# Scala Schema ## with Shapeless --- ## Disclaimers -- ### All the code does actually compile #### Thanks to mdoc (kudo to @olafurpg & @jvican) -- > This is a work of fiction. Any resemblance to actual persons, living or dead, or actual events is purely coincidental. --- ### What this talk is not about ? Recursion Schemes <!-- .element: class="fragment" --> -- ### What this talk about ? Manipulating constructs at the type level <!-- .element: class="fragment" --> Note: - ground-up introduction - realworld/concrete example -- ## Agenda - motivation - schema - case classes manipulation at the type level --- ### The project ##### relatively modern stack <!-- .element: class="fragment" --> - microservices <!-- .element: class="fragment" --> - JSON/REST APIs <!-- .element: class="fragment" --> - NoSQL database <!-- .element: class="fragment" --> - Kafka for events <!-- .element: class="fragment" --> -- ### The project ![](ms-diagram.png) Note: - lots of communication - lots of (de)serialization -- ### User - first / last name - age - creation date - permissions -- ### User ```scala case class User( firstName: String, lastName: String, age: Int, creationDate: Long, permissions: List[String] ) ``` -- #### Read from DB / Write to API ```scala import play.api.libs.json._ val userWrites = new Writes[User] { override def writes(u: User): JsValue = Json.obj( "firstName" -> u.lastName, "lastName" -> u.firstName, "age" -> u.creationDate, "creationDate" -> u.creationDate, "permisions" -> u.permissions ) } ``` -- ```scala import java.time.Instant.now val donald = User("Donald", "Duck", 85, now.toEpochMilli, Nil) ``` ### Oops <!-- .element: class="fragment" data-fragment-index="1" --> ```scala println(Json.prettyPrint(userWrites.writes(donald))) // { // "firstName" : "Duck", // "lastName" : "Donald", // "age" : 1651670154078, // "creationDate" : 1651670154078, // "permisions" : [ ] // } ``` <!-- .element: class="fragment" data-fragment-index="1" --> -- ### Generated Writes ```scala val userAutoWrites = Json.writes[User] ``` ```scala println(Json.prettyPrint(userAutoWrites.writes(donald))) // { // "firstName" : "Donald", // "lastName" : "Duck", // "age" : 85, // "creationDate" : 1651670154078, // "permissions" : [ ] // } ``` <!-- .element: class="fragment" data-fragment-index="1" --> -- #### What if we remove a field from the code ? ##### We can break compatibility <!-- .element: class="fragment" --> -- #### What if we stored our passwords #### in our user ? -- No problem ```scala case class UserWithPassword( firstName: String, lastName: String, age: Int, creationDate: Long, permissions: List[String], password: String ) ``` -- ```scala val mickey = UserWithPassword("Mickey", "Mouse", 91, now.toEpochMilli, Nil, "azerty") val userWPWAutoWrites = Json.writes[UserWithPassword] ``` #### Oops <!-- .element: class="fragment" data-fragment-index="1" --> ```scala println(Json.prettyPrint(userWPWAutoWrites.writes(mickey))) // { // "firstName" : "Mickey", // "lastName" : "Mouse", // "age" : 91, // "creationDate" : 1651670154536, // "permissions" : [ ], // "password" : "azerty" // } ``` <!-- .element: class="fragment" data-fragment-index="1" --> -- #### Automatically generated code is evil ! ##### Or is it ? <!-- .element: class="fragment" --> -- ### Coupling is evil ![](decoupling-diagram.png) <!-- .element: class="fragment" --> -- ### Maybe we need a schema ? --- ## Schema -- > The word schema comes from the Greek word σχήμα (skhēma), which means shape, or more generally, plan. -- ### In databases > In a relational database, the schema defines the tables, the fields in each table, and the relationships between fields and tables. -- ### Schema So, let's simplify, a schema is: - A way to define data with constraints - A way to define relationships in our data -- #### With types, we can - model and constraint our data - case classes - base types (String, Int, Boolean, ...) - define relationships - inheritances - composition -- #### Types are a way to define schema ![](decoupling-scala-diagram.png) <!-- .element: class="fragment" --> -- ### Schema Evolution (in Avro) -- #### Operations - add (optional) fields <!-- .element: class="fragment" --> - delete (optional) field <!-- .element: class="fragment" --> - modify (optional) field <!-- .element: class="fragment" --> -- #### Backward compatible > Backward compatibility means that consumers using the new schema can read data produced with the previous schema. -- #### Backward compatible We can automatically find a function v1 => v2 -- #### Backward compatible operations - delete fields - add, delete or modify optional fields -- #### Forward compatible > Forward compatibility means that data produced with a new schema can be read by consumers using the previous schema -- #### Forward compatible compatible We can automatically find a function v2 => v1 -- #### Forward compatible operations - add field - add, delete or modify optional fields -- ### Decoupling Business and API ```scala case class UserDomain( firstName: String, lastName: String, age: Int, creationDate: Long, permissions: List[String], password: String ) ``` <!-- .element: id="left" --> ```scala case class GetUserAPI( firstName: String, lastName: String, age: Int, creationDate: Long, permissions: List[String] ) ``` <!-- .element: id="right" --> -- ```scala def toUserDomain(u: UserDomain): GetUserAPI = GetUserAPI( u.firstName, u.lastName, u.age, u.creationDate, u.permissions ) val UAPIWrites = Json.writes[GetUserAPI] ``` -- ```scala val minnie = UserDomain( "Minnie", "Mouse", 91, now.toEpochMilli, Nil, "azerty" ) ``` ```scala println( Json.prettyPrint(UAPIWrites.writes(toUserDomain(minnie))) ) // { // "firstName" : "Minnie", // "lastName" : "Mouse", // "age" : 91, // "creationDate" : 1651670154558, // "permissions" : [ ] // } ``` -- ### It works ! #### with tons of boilerplate <!-- .element: class="fragment" --> -- ![](sad-panda.png) -- #### Why can't the compiler do that for us ? --- ## Transforming Data with shapeless -- ### Goal - Done at compile time - Typesafe - Minimal amount of boilerplate -- #### Compile time "runtimes" in scala - macros <!-- .element: class="fragment" --> - implicits <!-- .element: class="fragment" --> -- ### Implicits - context - extension method - composition -- ### Recursion -- #### List ```scala sealed trait MyList[T] case object MyNil extends MyList[Nothing] case class MyCons[T](h: T, t: MyList[T]) extends MyList[T] ``` -- #### List Length ```scala def length[T](l: List[T]): Int = l match { case Nil => 0 case h :: t => 1 + length(t) } ``` ```scala length(List(1, 2, 3)) ``` <!-- .element: class="fragment" data-fragment-index=1 --> ```scala 1 + length(List(2, 3)) ``` <!-- .element: class="fragment" data-fragment-index=3 data-code-focus="3" data-code-block="1" --> ```scala 1 + 1 + length(List(3)) ``` <!-- .element: class="fragment" data-fragment-index=3 data-code-focus="3" data-code-block="1" --> ```scala 1 + 1 + 1 + length(Nil) ``` <!-- .element: class="fragment" data-code-focus="2" data-code-block="1" --> ```scala val r = 1 + 1 + 1 + 0 // r: Int = 3 ``` <!-- .element: class="fragment" --> -- #### Can we do it at compile time ? Note: - no we can't, not like this - the compiler does not have enough information -- #### Statically sized list ```scala sealed trait SList[+H] case object SNil extends SList[Nothing]{ def ::[H](h: H) = new ::(h, this) } case class ::[H, T <: SList[H]](h: H, t: T) extends SList[H]{ def ::(h: H) = new ::(h, this) } ``` ```scala 1 :: 2 :: 3 :: SNil // res10: Int :: Int :: Int :: SNil.type = 1 :: 2 :: 3 :: SNil ``` <!-- .element: class="fragment" --> ```scala 1 :: 2 :: 3 :: Nil // res11: List[Int] = List(1, 2, 3) ``` <!-- .element: class="fragment" --> -- #### Typelevel length ```scala case class Size[T](size: Int) object Size { def apply[T](implicit s: Size[T]) = s } ``` -- ##### Typelevel length ```scala implicit val SNilSize: Size[SNil.type] = Size(0) ``` ```scala implicit def sconsSize[H, T <: SList[H]]( implicit l: Size[T] ): Size[H :: T] = Size(1 + l.size) ``` ```scala Size[Int :: Int :: Int :: SNil.type].size // res12: Int = 3 ``` <!-- .element: class="fragment" --> ```scala sconsSize[Int, Int :: Int :: SNil.type]( Size[Int :: Int :: SNil.type] ) ``` <!-- .element: class="fragment" data-code-block="2" data-code-focus="1-4" --> ```scala sconsSize[Int, Int :: Int :: SNil.type]( sconsSize(Size[Int :: SNil.type]) ) ``` <!-- .element: class="fragment" data-code-block="2" data-code-focus="1-4" --> ```scala sconsSize(sconsSize(sconsSize(Size[SNil.type]))) ``` <!-- .element: class="fragment" data-code-block="2" data-code-focus="1-4" --> ```scala sconsSize(sconsSize(sconsSize(SNilSize))) ``` <!-- .element: class="fragment" data-code-block="1" data-code-focus="1" --> -- #### Implicits composition behaves like recursion and pattern matching on types ![](exploding-brain.gif) <!-- .element: class="fragment" --> -- ### Transform Typeclass ```scala trait Transform[A, B] { def to(a: A): B } object Transform{ def apply[A, B](implicit t: Transform[A, B]) = t } ``` -- ### Let's start simple - Only field deletions - Same field types -- ### How would we do it at runtime ? -- ### Let's define our types ```scala sealed trait RType case object RInt extends RType case object RString extends RType case object RBoolean extends RType type RTuple = List[RType] ``` -- ### Now let's generate our fake transform ```scala def to(a: RTuple, b: RTuple): RTuple = (a, b) match { case (_, Nil) => Nil case (ha :: ta, hb :: tb) if ha == hb => ha :: to(ta, tb) case (ha :: ta, b) => to(ta, b) } ``` 1. Base case, b is complete <!-- .element: class="fragment" data-code-focus="2" --> 2. Heads do match <!-- .element: class="fragment" data-code-focus="3" --> 2. Heads do not match <!-- .element: class="fragment" data-code-focus="4" --> -- ### Does it work ? ```scala to(RInt :: Nil, RInt :: Nil) // res18: RTuple = List(RInt) to(RInt :: RString :: Nil, RInt :: Nil) // res19: RTuple = List(RInt) ``` ```scala to(RString :: Nil, RInt :: Nil) // scala.MatchError: (List(),List(RInt)) (of class scala.Tuple2) // at repl.MdocSession$App17.to(scala-schema-wshapeless.md:330) // at repl.MdocSession$App17.to(scala-schema-wshapeless.md:333) // at repl.MdocSession$App17$$anonfun$30.apply(scala-schema-wshapeless.md:350) // at repl.MdocSession$App17$$anonfun$30.apply(scala-schema-wshapeless.md:350) ``` <!-- .element: class="fragment" --> #### Yes ! <!-- .element: class="fragment" --> -- ### Let's do this at the type level ? -- ### HList to the rescue ```scala sealed trait HList case class ::[H, T <: MyHList](head: H, tail: T) extends HList case object HNil extends HList ``` ```scala import shapeless._ 1 :: "hello" :: true :: HNil // res20: Int :: String :: Boolean :: HNil = 1 :: "hello" :: true :: HNil Generic[(Int, String, Boolean)] .to((1, "hello", true)) // res21: Int :: String :: Boolean :: HNil = 1 :: "hello" :: true :: HNil ``` <!-- .element: class="fragment" --> -- ### Our base case ```scala import shapeless._ implicit def HNilTransform[T]: Transform[T, HNil] = _ => HNil ``` ```scala Transform[String :: Int :: HNil, HNil] .to("hello" :: 1 :: HNil) // res22: HNil = HNil ``` <!-- .element: class="fragment" --> -- ### Head matches ```scala implicit def HeadMatchTransform[ Head, TailOfA <: HList, TailOfB <: HList ]( implicit t: Transform[TailOfA, TailOfB] ): Transform[Head :: TailOfA, Head :: TailOfB] = a => a.head :: t.to(a.tail) ``` ```scala Transform[String :: Int :: HNil, String :: Int :: HNil] .to("Bob" :: 42 :: HNil) // res23: String :: Int :: HNil = "Bob" :: 42 :: HNil ``` <!-- .element: class="fragment" --> -- ### Head do not match ```scala import shapeless._ implicit def HeadDoNotMatchTransform[ HeadOfA, TailOfA <: HList, HeadOfB, TailOfB <: HList ]( implicit t: Transform[TailOfA, HeadOfB :: TailOfB] ): Transform[HeadOfA :: TailOfA, HeadOfB :: TailOfB] = a => t.to(a.tail) ``` ```scala Transform[String :: Int :: HNil, Int :: HNil] .to("Bob" :: 42 :: HNil) // res24: Int :: HNil = 42 :: HNil ``` <!-- .element: class="fragment" --> -- ### Great ! But what about case classes ? -- #### Well they are not so special ```scala case class Tag(tag: String, t: RType) extends RType ``` ```scala to( Tag("age", RInt) :: Tag("name", RString) :: Nil, Tag("name", RString) :: Nil ) // res25: RTuple = List(Tag("name", RString)) ``` <!-- .element: class="fragment" --> ```scala to( Tag("age", RInt) :: Tag("name", RString) :: Nil, Tag("nam", RString) :: Nil ) // scala.MatchError: (List(),List(Tag(nam,RString))) (of class scala.Tuple2) // at repl.MdocSession$App17.to(scala-schema-wshapeless.md:330) // at repl.MdocSession$App17.to(scala-schema-wshapeless.md:333) // at repl.MdocSession$App17.to(scala-schema-wshapeless.md:333) // at repl.MdocSession$App17$$anonfun$38.apply(scala-schema-wshapeless.md:451) // at repl.MdocSession$App17$$anonfun$38.apply(scala-schema-wshapeless.md:449) ``` <!-- .element: class="fragment" --> -- ## Labelled Generics ```scala case class Labelled(param1: Int, param2: String) LabelledGeneric[Labelled] .to(Labelled(1, "Hello")) // res26: Int with labelled.KeyTag[Symbol with tag.Tagged[param1], Int] :: String with labelled.KeyTag[Symbol with tag.Tagged[param2], String] :: HNil = 1 :: "Hello" :: HNil ``` -- ### Case classes transformation ```scala implicit def ccTransform[A, ReprA, B, ReprB]( implicit genA: LabelledGeneric.Aux[A, ReprA], genB: LabelledGeneric.Aux[B, ReprB], transform: Transform[ReprA, ReprB] ): Transform[A, B] = a => genB.from(transform.to(genA.to(a))) ``` -- ```scala case class UserDomain( firstName: String, lastName: String, age: Int, creationDate: Long, permissions: List[String], password: String ) ``` <!-- .element: id="left" --> ```scala case class GetUserAPI( firstName: String, lastName: String, age: Int, creationDate: Long, permissions: List[String] ) ``` <!-- .element: id="right" --> -- ```scala import java.time.Instant.now val donald = UserDomain( "Donald", "Duck", 85, now.toEpochMilli, Nil, "azerty" ) ``` ```scala Transform[UserDomain, GetUserAPI].to(donald) // res27: GetUserAPI = GetUserAPI("Donald", "Duck", 85, 1651670154690L, List()) ``` -- ### Automatic wraping / unwraping ```scala implicit def optionTransform[A]: Transform[A, Option[A]] = a => Option(a) case class UserId(value: String) implicit val userIdTransform: Transform[UserId, String] = a => a.value ``` -- ### Automatic wraping / unwraping ```scala implicit def HeadMatchWTransform[ HeadOfA, TailOfA <: HList, HeadOfB, TailOfB <: HList, ]( implicit hT: Transform[HeadOfA, HeadOfB], tT: Transform[TailOfA, TailOfB] ): Transform[HeadOfA :: TailOfA, HeadOfB :: TailOfB] = a => hT.to(a.head) :: tT.to(a.tail) ``` ```scala Transform[String :: Int :: HNil, Option[Int] :: HNil] .to("hello" :: 1 :: HNil) // res28: Option[Int] :: HNil = Some(1) :: HNil ``` --- ## So right to production ? -- ### With this you will have... - a compiler to keep you warm in winter <!-- .element: class="fragment" data-fragment-index="1" --> - overhead going to and from HLists <!-- .element: class="fragment" data-fragment-index="2" --> - definitely helpful error messages <!-- .element: class="fragment" data-fragment-index="3" --> ```scala case class UserMisorderedBis( age: Int, lastName: String, creationDate: Long, ) Transform[User, UserMisorderedBis] // error: not found: type User // Transform[User, UserMisorderedBis] // ^^^^ ``` <!-- .element: class="fragment" data-fragment-index="3" --> -- ### What about if we mess with field order ```scala case class UserMisordered( age: Int, lastName: String, creationDate: Long, permissions: List[String] ) Transform[UserDomain, UserMisordered].to(donald) // error: could not find implicit value for parameter t: App2.this.Transform[App2.this.UserDomain,App2.this.UserMisordered] // Transform[UserDomain, UserMisordered].to(donald) // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` <!-- .element: class="fragment" --> -- ### Use chimney ! -- ```scala case class UserMisorderedBis( age: Int, fullName: String, creationDate: Long, legacyUser: Boolean ) ``` ```scala import io.scalaland.chimney.dsl._ donald.transformInto[UserMisorderedBis] // error: Chimney can't derive transformation from App2.this.UserDomain to App2.this.UserMisorderedBis // // repl.MdocSession.App2.UserMisorderedBis // fullName: java.lang.String - no accessor named fullName in source type repl.MdocSession.App2.UserDomain // legacyUser: scala.Boolean - no accessor named legacyUser in source type repl.MdocSession.App2.UserDomain // // Consult https://scalalandio.github.io/chimney for usage examples. // // // donald.transformInto[UserMisorderedBis] // ^ ``` <!-- .element: class="fragment" --> -- ```scala import io.scalaland.chimney.dsl._ donald.into[UserMisorderedBis] .withFieldComputed( _.fullName, u => u.firstName + " " + u.lastName ) .withFieldConst(_.legacyUser, true) .transform // res32: UserMisorderedBis = UserMisorderedBis( // 85, // "Donald Duck", // 1651670154690L, // true // ) ``` --- ### Wrap Up - Avoid coupling <!-- .element: class="fragment" --> - Write more types <!-- .element: class="fragment" --> - Derive all the things ! <!-- .element: class="fragment" --> --- ### Going Further - [The Type Astronaut's Guide to Shapeless](https://books.underscore.io/shapeless-guide/shapeless-guide.html) - [Massaging case classes with shapeless](https://www.signifytechnology.com/blog/2018/10/massaging-case-classes-with-shapeless-by-chris-birchall-at-scala-in-the-city) - [Chimney](https://scalalandio.github.io/chimney/) --- ## Thank you ! Any questions ? ###### Slides: [bit.ly/2TL6QJu](https://bit.ly/2TL6QJu) Kévin Rauscher • [`@tomahna`](https://twitter.com/tomahna) --- ### Bonus -- ### Handling field order ```scala import shapeless.ops.hlist.Selector implicit def ContainTransform[ A <: HList, HeadOfB, TailOfB <: HList ]( implicit select: Selector[A, HeadOfB], t: Transform[A, TailOfB] ): Transform[A, HeadOfB :: TailOfB] = a => select(a) :: t.to(a) ``` -- ```scala case class UserMisordered( age: Int, lastName: String, creationDate: Long, permissions: List[String] ) Transform[UserDomain, UserMisordered].to(donald) // res33: UserMisordered = UserMisordered(85, "Duck", 1651670154690L, List()) ``` ## Thank you ! Any questions ? ###### Slides: [bit.ly/2TL6QJu](https://bit.ly/2TL6QJu) Kévin Rauscher • [`@tomahna`](https://twitter.com/tomahna) ---