Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Lawrence Carvalho - Combining Refined Types with Type Class Derivation in Scala

Shannon
October 21, 2018

Lawrence Carvalho - Combining Refined Types with Type Class Derivation in Scala

Lawrence Carvalho talk from Scala in the City

Shannon

October 21, 2018
Tweet

More Decks by Shannon

Other Decks in Technology

Transcript

  1. Lawrence Carvalho Motivation • Deserialize and validate input • Goal

    – correctness – "make illegal states unrepresentable” • Challenge – boilerplate
  2. Lawrence Carvalho Motivation case class Movie(id: String, title: String, credits:

    List[Credit], productionYear: Int, languages: List[String], imageUrl: String) case class Credit(firstName: String, lastName: String, role: Role) UUID Cannot be empty Cannot be empty Greater than or equal to 1889 From predefined set ISO 639-2 URL Cannot be empty
  3. Lawrence Carvalho Ways to “make illegal states unrepresentable” • assert/require

    – Unsafe case class Movie(id: String, title: String, credits: List[Credit], productionYear: Int, languages: List[String], imageUrl: String) { require(id.nonEmpty) } • Smart constructors case class Movie private (id: String, title: String, credits: List[Credit], productionYear: Int, languages: List[String], imageUrl: String) { def copy(id: String = id, title: String = title, credits: List[Credit] = credits, productionYear: Int = productionYear, languages: List[String] = languages, imageUrl: String = imageUrl): Either[Invalid, Movie] = Movie(id, title, credits, productionYear, languages, imageUrl) } object Movie { def apply(id: String, title: String, credits: List[Credit], productionYear: Int, languages: List[String], imageUrl: String): Either[Invalid, Movie] = ??? }
  4. Lawrence Carvalho Manually decoding JSON implicit val stringJsonDecoder: JsonDecoder[String] =

    { case JsonString(str) => Right(str) case _ => Left("Not a string!") } implicit val intJsonDecoder: JsonDecoder[Int] = { case JsonInt(int) => Right(int) case _ => Left("Not an int!") } implicit def listJsonDecoder[T](implicit decoder: JsonDecoder[T]): JsonDecoder[List[T]] = { case JsonArray(jsons) => jsons.traverse(decoder(_)) case _ => Left("Not an array!") } implicit val uuidJsonDecoder: JsonDecoder[UUID] = stringJsonDecoder(_).flatMap(s => Either.catchNonFatal(UUID.fromString(s)).leftMap(_.getMessage))
  5. Lawrence Carvalho Manually decoding JSON implicit val movieJsonDecoder: JsonDecoder[Movie] =

    json => for { id <- json.get[String]("id") title <- json.get[String]("title") credits <- json.get[List[Credit]]("credits") productionYear <- json.get[Int]("productionYear") languages <- json.get[List[String]]("languages") imageUrl <- json.get[String]("imageUrl") } yield Movie(id, title, credits, productionYear, languages, imageUrl) implicit val creditJsonDecoder: JsonDecoder[Credit] = json => for { firstName <- json.get[String]("firstName") lastName <- json.get[String]("lastName") role <- json.get[Role]("role") } yield Credit(firstName, lastName, role) implicit val roleJsonDecoder: JsonDecoder[Role] = json => json.decode[Actor].orElse(json.decode[Writer])
  6. Lawrence Carvalho Refined • Scala library for adding predicates to

    types • Port of the refined Haskell library type NonEmptyStr = String Refined NonEmpty type PosInt = Int Refined Positive type MoreThanTen = Int Refined Greater[W.`10`.T] import eu.timepit.refined.auto._ val r1: NonEmptyStr = "" val e1: PosInt = -1 val f1: MoreThanTen = 9 //compilation fails! val r2: Either[String, Refined[String, NonEmptyStr]] = refineV[NonEmptyStr]("") val e2: Either[String, Refined[Int, PosInt]] = refineV[PosInt](-1) val f2: Either[String, Refined[Int, MoreThanTen]] = refineV[MoreThanTen](9)
  7. Lawrence Carvalho Refined JSON Decoder type `ISO639-2` = String Refined

    MatchesRegex[W.`"^[a-zA-Z]{3}([/][a-zA-Z]{3})?$"`.T] case class Movie(id: UUID, title: String Refined NonEmpty, credits: NonEmptyList[Credit], productionYear: Int Refined GreaterEqual[W.`1889`.T], languages: List[`ISO639-2`], imageUrl: String Refined Url) case class Credit(firstName: String, lastName: String, role: Role) implicit def refinedJsonDecoder[T, P, F[_, _]](implicit jsonDecoder: JsonDecoder[T], validate: Validate[T, P], refType: RefType[F]): JsonDecoder[F[T, P]] = json => for { t <- jsonDecoder(json) refined <- refType.refine[P](t) } yield refined
  8. Lawrence Carvalho Shapeless type class derivation • Heterogeneous lists (HLists)

    • Coproducts • Generic case class Movie(id: UUID, title: String Refined NonEmpty, credits: NonEmptyList[Credit], productionYear: Int Refined GreaterEqual[W.`1889`.T], languages: List[`ISO639-2`], imageUrl: String Refined Url) type GenericMovie = UUID :: Refined[String, NonEmpty] :: NonEmptyList[Credit] :: Refined[Int, GreaterEqual[W.`1889`.T]] :: List[ `ISO639-2`] :: Refined[String, Url] :: HNil sealed trait Role case class Actor(characterName: String Refined NonEmpty) extends Role case class Writer(characters: List[String Refined NonEmpty]) extends Role type GenericRole = Actor :+: Writer :+: CNil
  9. Lawrence Carvalho Shapeless type class derivation for products implicit def

    hlistJsonDecoder[H, T <: HList]( implicit hDecoder: Lazy[JsonDecoder[H]], tDecoder: JsonDecoder[T] ): JsonDecoder[H :: T] = json => for { h <- hDecoder.value(json) t <- tDecoder(json) } yield h +: t implicit val hnilJsonDecoder: JsonDecoder[HNil] = _ => Right(HNil) implicit def genericJsonDecoder[A, R]( implicit gen: Generic.Aux[A, R], hEncoder: Lazy[JsonDecoder[R]] ): JsonDecoder[A] = json => hEncoder.value(json).map(gen.from)
  10. Lawrence Carvalho Shapeless type class derivation for coproducts implicit val

    cnilJsonDecoder: JsonDecoder[CNil] = _ => throw new Exception("cannot happen") implicit def coproductJsonDecoder[H, T <: Coproduct](implicit hJsonDecoder: Lazy[JsonDecoder[H]], tJsonDecoder: JsonDecoder[T]): JsonDecoder[H :+: T] = json => hJsonDecoder .value(json) .map(Inl(_)) .orElse(tJsonDecoder(json).map(Inr(_)))
  11. Lawrence Carvalho Automatically decoding JSON case class Movie(id: UUID, title:

    String Refined NonEmpty, credits: NonEmptyList[Credit], productionYear: Int Refined GreaterEqual[W.`1889`.T], languages: List[`ISO639-2`], imageUrl: String Refined Url) case class Season(id: UUID, title: String Refined NonEmpty, seasonNumber: Int Refined Positive, credits: NonEmptyList[Credit]) case class ContentWatched(contentId: String Refined NonEmpty, userId: UUID) val decodedMovie: Either[String, Movie] = json.decode[Movie] val decodedSeries: Either[String, Series] = json.decode[Series] val decodedContentWatched: Either[String, ContentWatched] = json.decode[ContentWatched]
  12. Lawrence Carvalho Existing implementations • JSON decoding with Circe •

    Typesafe configuration decoding with pureconfig • Command line argument parsing with scopt • Deriving Scalacheck Arbitrary instances
  13. Lawrence Carvalho JSON decoding with Circe import io.circe.parser._ import io.circe.generic.auto._

    case class Movie(id: String Refined Uuid, title: String Refined NonEmpty, credits: NonEmptyList[Credit], productionYear: Int Refined GreaterEqual[W.`1889`.T]) val jsonStr = """ |{ | "id": "5f5e36aa-a4fd-40df-9363-2ce71f2ad262" | "title": "Venom" | "credits": [ | { | "firstName": "Tom" | "lastName": "Hardy" | "role": "actor" | } | ] | "productionYear": 2017 |} """.stripMargin val decoded: Either[circe.Error, Movie] = decode[Movie](jsonStr)
  14. Lawrence Carvalho Typesafe configuration decoding with pureconfig case class AppConfig(port:

    Int Refined Positive, schemaRegistryUrl: String Refined Url) val config = ConfigFactory.parseString( """ |{ | port = 8080, | schema-registry-url = "http://schema-registry.sky.uk" |} """.stripMargin ) val validatedConfig: Either[ConfigReaderFailures, AppConfig] = loadConfig[AppConfig](config)
  15. Lawrence Carvalho Conclusions • Refined – Define constraints (predicates) on

    types. – Make the incorrect impossible – Types cannot lie! • Type class derivation – Let the compiler write your boilerplate! – Evolve domain model – Support deserialization of more data structures • Drawbacks – Fully automatic derivation will create more objects at runtime – Compilation time
  16. Lawrence Carvalho Questions? • Refined: https://github.com/fthomas/refined • Decorate your types

    with refined: https://www.youtube.com/watch?v=zExb9x3fzKs • Shapeless: https://github.com/milessabin/shapeless • Shapeless guide: https://github.com/underscoreio/shapeless-guide • Myself! – https://www.linkedin.com/in/lacarvalho/ – https://github.com/lacarvalho91