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