Why types matter

@volpegabriel87

@gvolpe

April 2020

Scala Love Conf

@volpegabriel87

@gvolpe

About me

A motivating example

@volpegabriel87

@gvolpe

def showName(username: String, name: String, email: String): String =
  s"""
    Hi $name! Your username is $username
    and your email is $email.
   """
val program: IO[Unit] =
  putStrLn(
    showName("gvolpe@github.com", "12345", "foo")
  )

Dealing with Strings

A motivating example

@volpegabriel87

@gvolpe

@volpegabriel87

@gvolpe

Introducing types

case class UserNameV(value: String) extends AnyVal
case class NameV(value: String) extends AnyVal
case class EmailV(value: String) extends AnyVal
val program: IO[Unit] = 
  putStrLn(
    showNameV(
      UserNameV("gvolpe@github.com"),
      NameV("12345"),
      EmailV("foo")
    )
  )
def showNameV(username: UserNameV, name: NameV, email: EmailV): String =
  s"""
    Hi ${name.value}! Your username is ${username.value}
    and your email is ${email.value}.
   """

Value classes

Let's do better!

@volpegabriel87

@gvolpe

def mkUsername(value: String): Option[UserNameV] =
  if (value.nonEmpty) UserNameV(value).some else None

def mkName(value: String): Option[NameV] =
  if (value.nonEmpty) NameV(value).some else None

// Let's pretend we validate it properly
def mkEmail(value: String): Option[EmailV] =
  if (value.contains("@")) EmailV(value).some else None
val program: IO[Unit] =
  (
    mkUsername("gvolpe").liftTo[IO](EmptyError),
    mkName("George").liftTo[IO](EmptyError),
    mkEmail("123").liftTo[IO](InvalidEmail)
  ).parMapN(showNameT)
    .flatMap(putStrLn)

Smart Constructors

Let's do better!

@volpegabriel87

@gvolpe

λ > root[ERROR] TypesDemo$InvalidEmail$
root ... finished with exit code 1

Smart Constructors

RUNTIME VALIDATION

Case classes gotcha

@volpegabriel87

@gvolpe

Copy method

val program: IO[Unit] =
  (
    mkUsername("gjl").liftTo[IO](EmptyError),
    mkName("George").liftTo[IO](EmptyError),
    mkEmail("gjl@foo.com").liftTo[IO](InvalidEmail)
  ).parMapN {
    case (u, n, e) =>
      showNameV(u.copy(value = ""), n, e)
  }.flatMap(putStrLn)

Ouch!

A neat trick

@volpegabriel87

@gvolpe

Sealed abstract case class

sealed abstract case class UserNameP(value: String)
object UserNameP {
  def apply(value: String): Option[UserNameP] =
    if (value.nonEmpty) new UserNameP(value) {}.some else None
}

// same for name

sealed abstract case class EmailP(value: String)
object EmailP {
  def apply(value: String): Option[EmailP] =
    if (value.contains("@")) new EmailP(value) {}.some else None
}
val program: IO[Unit] =
  (
    UserNameP("jr"),
    NameP("Joe Reef"),
    EmailP("joe@bar.com")
  ).tupled.fold(IO.unit) {
    case (u, n, e) => putStrLn(showNameP(u, n, e))
  }

@volpegabriel87

@gvolpe

A better solution

@newtype case class UserNameT(value: String)
@newtype case class NameT(value: String)
@newtype case class EmailT(value: String)
val program: IO[Unit] = 
  putStrLn(
    showNameT(
      UserNameT("gvolpe@github.com"),
      NameT("12345"),
      EmailT("")
    )
  )
def showNameT(username: UserNameT, name: NameT, email: EmailT): String =
  s"""
    Hi ${name.value}! Your username is ${username.value}
    and your email is ${email.value}.
   """

Newtypes

@volpegabriel87

@gvolpe

A better solution

Newtypes

def mkUsername(value: String): Option[UserNameT] =
  if (value.nonEmpty) UserNameT(value).some else None

def mkName(value: String): Option[NameT] =
  if (value.nonEmpty) NameT(value).some else None

def mkEmail(value: String): Option[EmailT] =
  if (value.contains("@")) EmailT(value).some else None

@volpegabriel87

@gvolpe

A better solution

Newtypes

val program: IO[Unit] =
  (
    mkUsername("gvolpe").liftTo[IO](EmptyError),
    mkName("George").liftTo[IO](EmptyError),
    mkEmail("123").liftTo[IO](InvalidEmail)
  ).parMapN {
    case (u, n, e) =>
      showNameT(u, n, e)
  }.flatMap(putStrLn)
case (u, n, e) =>
  showNameT(UserName(""), n, e)

Refinement Types

@volpegabriel87

@gvolpe

import eu.timepit.refined._ // and more ...

type UserNameR = NonEmptyString
type NameR     = NonEmptyString
type EmailR    = String Refined Contains['@']

Because we can do better

Refinement Types

@volpegabriel87

@gvolpe

val program: IO[Unit] =
  putStrLn(
    showNameR("jr", "Joe", "123#com")
  )

Because we can do better

def showNameR(username: UserNameR, name: NameR, email: EmailR): String =
  s"""
    Hi ${name.value}! Your username is ${username.value}
    and your email is ${email.value}.
   """

@volpegabriel87

@gvolpe

COMPILE TIME VALIDATION

[error] /foo/one.scala:77:30: Predicate (!(1 == @) && !(2 == @)) did not fail.
[error]       showNameR("jr", "Joe", "123#com")
[error]                              ^
[error] one error found
[error] (Compile / compileIncremental) Compilation failed
[error] Total time: 2 s, completed Nov 29, 2019 5:12:33 PM

Refinement Types

Because we can do better

✔️

Refinement Types

@volpegabriel87

@gvolpe

Runtime validation

case class MyType(a: NonEmptyString, b: Int Refined Greater[5])

def validate(a: String, b: Int): Either[String, MyType] =
  for {
    x <- refineV[NonEmpty](a)
    y <- refineV[Greater[5]](b)
  } yield MyType(x, y)
validate("", 4)

Left(Predicate isEmpty() did not fail.)
def refineV[T, P](t: T)(
    implicit v: Validate[T, P]
): Either[String, Refined[T, P]]

Refinement Types

@volpegabriel87

@gvolpe

Runtime validation via Validated

case class MyType(a: NonEmptyString, b: Int Refined Greater[5])

// Round-trip Either-Validated conversion via Parallel
def validate(a: String, b: Int): Either[String, MyType] =
  (refineV[NonEmpty](a), refineV[Greater[5]](b))
    .parMapN(MyType.apply) 
validate("", 4)

Left(Predicate isEmpty() did not fail.Predicate failed: (4 > 5).)

Refinement Types

@volpegabriel87

@gvolpe

Runtime validation via Validated NEL

case class MyType(a: NonEmptyString, b: Int Refined Greater[5])

// Round-trip EitherNel-ValidatedNel conversion via Parallel
def validate(a: String, b: Int): EitherNel[String, MyType] =
  (refineV[NonEmpty](a).toEitherNel, refineV[Greater[5]](b).toEitherNel)
    .parMapN(MyType.apply)
validate("", 4)

Left(
  NonEmptyList(
    Predicate isEmpty() did not fail., 
    Predicate failed: (4 > 5).
  )
)

Refinement Types

@volpegabriel87

@gvolpe

  • Logical Predicates
    • Not, And, Or
  • Numeric Predicates
    • LessThan
    • GreaterThan
    • EqualTo
    • From, To, FromTo
    • Positive, Negative
  • Foldable Predicates
    • SizeEqualTo
    • NonEmpty

Newtypes + Refined

@volpegabriel87

@gvolpe

Because we can excel

@newtype case class UserName(value: NonEmptyString)
@newtype case class Name(value: NonEmptyString)
@newtype case class Email(value: String Refined Contains['@'])
import eu.timepit.refined._ // and more ...

type UserNameR = NonEmptyString
type NameR     = NonEmptyString
type EmailR    = String Refined Contains['@']

Newtypes + Refined

@volpegabriel87

@gvolpe

Because we can excel

def showNameTR(username: UserName, name: Name, email: Email): String =
  s"""
    Hi ${name.value}! Your username is ${username.value}
    and your email is ${email.value}.
   """
val program: IO[Unit] =
  putStrLn(
    showNameTR(
      UserName("jr"),
      Name("John"),
      Email("foo@bar.com")
    )
  )

Newtypes + Refined

@volpegabriel87

@gvolpe

Because we can excel

def program(u: String, n: String, e: String): IO[Unit] =
  putStrLn (
    (
      refineV[NonEmpty](u).toEitherNel.map(UserName.apply),
      refineV[NonEmpty](n).toEitherNel.map(Name.apply),
      refineV[Contains['@']](e).toEitherNel.map(Email.apply)
    ).parMapN(showNameTR)
  )
def program(u: String, n: String, e: String): IO[Unit] =
  putStrLn (
    (
      validate[UserName](u),
      validate[Name](n),
      validate[Email](e)
    ).parMapN(showNameTR)
  )

Newtypes + Refined

@volpegabriel87

@gvolpe

Because we can excel

final class NewtypeRefinedPartiallyApplied[A] {
  def apply[T, P](raw: T)(
      implicit c: Coercible[Refined[T, P], A],
      v: Validate[T, P]
  ): EitherNel[String, A] =
    refineV[P](raw).toEitherNel.map(_.coerce[A])
}

def validate[A]: NewtypeRefinedPartiallyApplied[A] = 
  new NewtypeRefinedPartiallyApplied[A]

Summary

@volpegabriel87

@gvolpe

  • Newtypes: zero-cost wrappers.
  • Refined: type-level predicates.
  • EitherNEL + Parallel: seamless runtime validation.

✔️ No boilerplate

✔️ No smart constructors

✔️ Compile-time validation

✔️ Strongly-typed functions

Questions?

@volpegabriel87

@gvolpe

Why types matter

By Gabriel Volpe

Why types matter

Why types matter

  • 2,630