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,818