FP Design Patterns
@volpegabriel87
@gvolpe
@volpegabriel87
@gvolpe
@volpegabriel87
@gvolpe
Practical FP in Scala
A hands-on approach
<< SUGAR2019 >>
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] =
for {
u <- mkUsername("gvolpe").liftTo[IO](EmptyError)
n <- mkName("George").liftTo[IO](EmptyError)
e <- mkEmail("123").liftTo[IO](InvalidEmail)
_ <- putStrLn(showNameV(u, n, e))
} yield ()
Smart Constructors
Let's do better!
@volpegabriel87
@gvolpe
λ > root[ERROR] meetup.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)
).mapN {
case (u, n, e) =>
putStrLn(
showNameV(u.copy(value = ""), n, e)
)
}
Ouch!
A neat trick
@volpegabriel87
@gvolpe
Sealed abstract case class
sealed abstract case class UserNameP(value: String)
object UserNameP {
def make(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 make(value: String): Option[EmailP] =
if (value.contains("@")) new EmailP(value) {}.some else None
}
val program: IO[Unit] =
(
UserNameP.make("jr"),
NameP.make("Joe Reef"),
EmailP.make("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)
).mapN {
case (u, n, e) =>
putStrLn(
showNameT(u, n, e)
)
}
putStrLn(
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}.
"""
Refinement Types
@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
@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.value}! Your username is ${username.value.value}
and your email is ${email.value.value}.
"""
val program: IO[Unit] =
putStrLn(
showNameTR(
UserName("jr"),
Name("John"),
Email("foo@bar.com")
)
)
State management
@volpegabriel87
@gvolpe
val makeRef: IO[Ref[IO, Int]] =
Ref.of[IO, Int](0)
Creation of mutable state
val program =
for {
r <- makeRef
_ <- r.update(_ + 10)
r <- makeRef
_ <- r.update(_ + 20)
n <- r.get
_ <- putStrLn(n)
} yield ()
PRINTS 20
State management
@volpegabriel87
@gvolpe
def incrByOne(ref: Ref[IO, Int]): IO[Unit] =
putStrLn("Increasing counter by one") *>
ref.update(_ + 1)
def incrByTwo(ref: Ref[IO, Int]): IO[Unit] =
putStrLn("Increasing counter by two") *>
ref.update(_ + 2)
val program: IO[Unit] =
Ref.of[IO, Int](0).flatMap { ref =>
incrByOne(ref) >> incrByTwo(ref) >> ref.get.flatMap(putStrLn)
}
PRINTS 100
Shared state
val program: IO[Unit] =
Ref.of[IO, Int](0).flatMap { ref =>
incrByOne(ref) >> incrByTwo(ref) >>
// We can access the Ref and alter its state which may be undesirable
ref.get.flatMap(n => if (n % 3 == 0) ref.set(100) else IO.unit) >>
ref.get.flatMap(putStrLn)
}
State management
@volpegabriel87
@gvolpe
trait Counter[F[_]] {
def incr: F[Unit]
def get: F[Int]
}
object Counter {
def make[F[_]: Sync]: F[Counter[F]] =
Ref.of[F, Int](0).map { ref =>
new Counter[F] {
def incr: F[Unit] =
ref.update(_ + 1)
def get: F[Int] =
ref.get
}
}
}
Encapsulating state
State management
@volpegabriel87
@gvolpe
val program: IO[Unit] =
Counter.make[IO].flatMap { c =>
c.incr >> c.get.flatMap(putStrLn)
}
Encapsulating state
def incrByTen(counter: Counter[IO]): IO[Unit] =
counter.incr.replicateA(10).void
val program: IO[Unit] =
Counter.make[IO].flatMap { c =>
// Sharing state only via the TF algebra
c.incr >> incrByTen(c) >> c.get.flatMap(putStrLn)
}
¡Muchas gracias!
@volpegabriel87
@gvolpe
FP Design Patterns
By Gabriel Volpe
FP Design Patterns
FP Design Patterns in Scala
- 2,548