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. Combining Refinement Types with Type Class Derivation
    Lawrence Carvalho

    View full-size slide

  2. Lawrence Carvalho
    Motivation
    • Deserialize and validate input
    • Goal
    – correctness
    – "make illegal states unrepresentable”
    • Challenge
    – boilerplate

    View full-size slide

  3. 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

    View full-size slide

  4. 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] = ???
    }

    View full-size slide

  5. 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))

    View full-size slide

  6. 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])

    View full-size slide

  7. 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)

    View full-size slide

  8. 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

    View full-size slide

  9. 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

    View full-size slide

  10. 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)

    View full-size slide

  11. 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(_)))

    View full-size slide

  12. 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]

    View full-size slide

  13. Lawrence Carvalho
    Existing implementations
    • JSON decoding with Circe
    • Typesafe configuration decoding with pureconfig
    • Command line argument parsing with scopt
    • Deriving Scalacheck Arbitrary instances

    View full-size slide

  14. 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)

    View full-size slide

  15. 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)

    View full-size slide

  16. 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

    View full-size slide

  17. 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

    View full-size slide