Demystifying Functional Programming

with some abstractions

Cristian Spinetta| @cspinetta | Diego Parra | @dpsoft

Disclaimer

Typeclass

trait HtmlWriter[A] {
  def write(value: A): String
}
implicit val intWriter:HtmlWriter[Int] =
  new HtmlWriter[Int] {
    override def write(value: Int): String =
      value.toString
  }

implicit val stringWriter:HtmlWriter[String] =
  new HtmlWriter[String] {
    override def write(value: String): String =
      value.replaceAll("<", "&lt;").replaceAll(">", "&gt;")
  }
implicit def boxWriter[A]:HtmlWriter[Box[A]] =
  new HtmlWriter[Box[A]] {
    override def write(value: Box[A]): String = ???
  }
The type class itself is a trait with a single type parameter
Type class instances
Extra type class instances

What are typeclasses? Scala | Haskell

Semigroup

A semigroup is a binary associative operation

trait Semigroup[A] {
 /**
   * Associativity means:
   * combine(x, combine(y, z)) = combine(combine(x, y), z)
   */
  def combine(x: A, y: A): A // append | compose | whatever
}

implicit val intAdditionSemigroup: Semigroup[Int] =
  new Semigroup[Int] {
    override def combine(x: Int, y: Int): Int = 
      x + y
  }
Semigroup.combineAll[Int](100, 50) //150
List(1, 2, 3).foldLeft(0)(Semigroup.combineAll) //6
def reduce[A](as: List[A])(implicit semigroup: Semigroup[A]): A =
    as.foldLeft(/* ??? */)(semigroup.combine) // We can't generalize the previous function
def combineAll[A](x: A, y: A)(implicit semigroup: Semigroup[A]): A = 
  semigroup.combine(x, y)

Monoid

A monoid is a binary associative operation with an identity

trait Monoid[A] extends Semigroup[A] {
  /**
    * Identity means:
    * combine(x, empty) = combine(empty, x) = x
    */
  def empty: A
}

implicit val intMonoid:Monoid[Int] =
  new Monoid[Int] {
    def combine(x: Int, y: Int): Int =  x + y
    def empty: Int = 0
  }
def combineAll[A](as: List[A])(implicit monoid: Monoid[A]): A =
    as.foldLeft(monoid.empty)(monoid.combine)
Monoid.combineAll(List(1,2,3))
case class Times(data: Map[String, FiniteDuration])

implicit val timesMonoid: Monoid[Times] = new Monoid[Times] {
  def combine(x: Times, y: Times): Times = Times(x.data ++ y.data)
  def empty: Times = Times(Map.empty)
}

Example

Functor

trait Functor[F[_]] {

  /**
    * Identity: Option(identity(1)) == identity(Option(1))
    * Composition: F[f o g] = F[f] o F[g]
    */
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

implicit val optionFunctor: Functor[Option] =
  new Functor[Option] {
    def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa match {
      case None    => None
      case Some(a) => Some(f(a))
    }
  }

A functor is a way to apply a function over or around some structure that we don’t want to alter it

def mapOption[A, B](as: Option[A])
                   (f: A => B)
                   (implicit functor: Functor[Option]): Option[B] =
    functor.map(as)(f)

Functor

Functor.map(Some(5))(_ + 3)

Example

Functor.map(None)(_ + 3)

Functor

Functor.map(List(2, 8, 12))(_ + 3)

Using a List

implicit val listFunctor: Functor[List] =
  new Functor[List] {
    def map[A, B](fa: List[A])(f: A => B): List[B] = fa match {
      case Nil => Nil
      case a :: as => f(a) :: map(as)(f)
    }
  }

Example

Applicative

Applicative is a way to apply a wrapped function over or around some structure that we don’t want to alter it

trait Applicative[F[_]] extends Functor[F] {

  def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]

  def pure[A](a: A): F[A]

  def map[A, B](fa: F[A])(f: A => B): F[B] = ap(pure(f))(fa)
}
implicit val optionApplicative = new Applicative[Option] {
  def ap[A, B](ff: Option[A => B])(fa: Option[A]): Option[B] =
    (ff, fa) match {
      case (Some(f), Some(a)) => Some(f(a))
      case _ => None
    }

  def pure[A](a: A): Option[A] = Option(a)
}
def apOption[A, B](opt: Option[A])
                     (f: Option[A => B])
                     (implicit functor: Applicative[Option]): Option[B] =
    optionApplicative.ap(f)(opt)

Applicative

apOption(Some(2))(Some(i => s"The number is: ${i.toString}")) // Some(The number is: 2)

Example

apOption(None)(Some(i => s"The number is: ${i.toString}")) // None

Monad

Monad introduces a way to append two structure sequentially and the results of previous computations may influence what computations to run next

trait Monad[F[_]] extends Applicative[F] {

  def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]

  def ap[A, B](ff: F[A => B])(fa: F[A]): F[B] =
    flatMap(ff)(x => map(fa)(x))
}
val optionMonad = new Monad[Option] {
  def flatMap[A, B](fa: Option[A])(f: A => Option[B]): Option[B] =
    fa match {
      case None => None
      case Some(a) => f(a)
    }

  def pure[A](a: A): Option[A] = Some(a)
}

Monad

implicit class OptionMonad[A](opt: Option[A]) {
  def flatMap[B](f: A => Option[B]): Option[B] =
    optionMonad.flatMap(opt)(f)
}

Example

def half(number: Int): Option[Int] = {
  if (number % 2 == 0) Some(number / 2)
  else None
}
val result = Some(6)
  .flatMap(half)
  .flatMap(half)
  .flatMap(half)

Monads Everywhere

  • Option
  • Future
  • List
  • Try
  • Either

Monad Transformers

def compose[M[_]: Monad, N[_]: Monad]: Monad[M[N[_]]] =

Monads do not compose

val greetingFuture: Future[String] = Future.successful("Hello")
val firstNameOption: Option[String] = Some("John")
val lastNameOption: Option[String] = Some("Doe")

for {
  g <- greetingFuture
  f <- firstNameOption
  l <- lastNameOption
} yield s"$g $f $l"
val greeting: Option[String] = Some("Hello")
val firstName: Option[String] = Some("John")
val lastName: Option[String] = Some("Doe")

for {
  g <- greetingOption
  f <- firstNameOption
  l <- lastNameOption
} yield s"$g $f $l"

Same monads

Different monads

Monad Transformers

  • Monads do not compose generically.

Instead of nesting

We have a stack

  • Gives us a way to stack two monads on top of each other and have them act a single monad.
Future[Either[Throwable, String]] => EitherT[Future, Throwable, String]

There exists a transformer equivalent for every monad instance

Monad Transformers Scala | Haskell

EitherT[F[_], A, B]
          ^  
          |__ ANY MONAD!

Monad Transformers

Example

val greetingEither: Future[Either[Throwable, String]] = Future.successful(Either.right("Hello"))
val firstNameFuture: Future[String] = Future.successful("Jane")
val lastNameOption: Option[String] = Some("Doe")

def greet: Future[Either[Throwable, String]] = {
  val result: EitherT[Future, Throwable, String] = for {
    g <- EitherT(greetingEither)
    f <- EitherT.liftF(firstNameFuture)
    l <- EitherT.fromOption[Future](lastNameOption, new RuntimeException("lastName not found"))
  } yield s"$g $f $l"
  result.value // => EitherT[Future, Throwable, String] => Future[Either[Throwable, String]]
}
case class AwesomeError(msg: String)

type ResultT[F[_], A] = EitherT[F, AwesomeError, A]
type FutureResult[A] = ResultT[Future, A]
def getUser(id:String):FutureResult[User]
def canBeUpdated(user:User):FutureResult[User]
def update(user:User):FutureResult[User]
def updateUser(user: User):FutureResult[User] = for {
  u           <- getUser(user.id)
  _           <- canBeUpdated(u)
  updatedUser <- updateUser(u)
} yield updatedUser

Better?

Show me the Money!

Monad!

trait UserStore {
  def findUser(id: Long): Future[Either[Throwable, User]]
  def canBeUpdated(user: User): Future[Either[Throwable, Boolean]]
  def updateUser(user: User)(pointsToAdd: Int): Future[Either[Throwable, User]]
}

class LoyaltyPoints(store: UserStore) {
  def addPoints(userId: Long, pointsToAdd: Int): Future[Either[Throwable, User]] = {
    val result: Future[Either[Throwable, User]] = for {
      user        <- store.findUser(userId)
      _           <- user.right.map(store.canBeUpdated).left.map(Future.failed).merge
      updatedUser <- user.right.map(store.updateUser(_)(pointsToAdd)).left.map(Future.failed).merge
    } yield updatedUser
    result
  }
}

Api V0

Show me the Monad!

trait UserStore {
  def findUser(id: Long): Future[Either[Throwable, User]]
  def canBeUpdated(user: User): Future[Either[Throwable, Boolean]]
  def updateUser(user: User)(pointsToAdd: Int): Future[Either[Throwable, User]]
}

class LoyaltyPoints(store: UserStore) {
  def addPoints(userId: Long, pointsToAdd: Int): Future[Either[Throwable, User]] = {
    val result: EitherT[Future, Throwable, User] = for {
      user        <- EitherT(store.findUser(userId))
      _           <- EitherT(store.canBeUpdated(user))
      updatedUser <- EitherT(store.updateUser(user)(pointsToAdd))
    } yield updatedUser

    result.value // EitherT[Future, Throwable, User] => Future[Either[Throwable, User]]
  }
}

Api V1

So far so good, but what about description and interpretation?

Show me the Monad!

trait UserStoreAlg[F[_]] {
  def findUser(id: Long): F[Either[Throwable, User]]
  def canBeUpdated(user: User): F[Either[Throwable, Boolean]]
  def updateUser(user: User)(pointsToAdd: Int): F[Either[Throwable, User]]
}

class LoyaltyPoints[F[_]](store: UserStoreAlg[F])(implicit M: Monad[F]) {
  def addPoints(userId: Long, pointsToAdd: Int): F[Either[Throwable, User]] = {
    val result: EitherT[F, Throwable, User] = for {
      user        <- EitherT(store.findUser(userId))
      _           <- EitherT(store.canBeUpdated(user))
      updatedUser <- EitherT(store.updateUser(user)(pointsToAdd))
    } yield updatedUser

    result.value // EitherT[F, Throwable, User] => F[Either[Throwable, User]]
  }
}

Api v2

so, it smells like we have to put EitherT everywhere,

doesn't it?

Show me the Monad!

case class ServiceError(msg: String)

type ResultOrError[F[_], A] = EitherT[F, ServiceError, A]

trait UserStoreAlg[F[_]] {
  def findUser(id: Long): ResultOrError[F, User]
  def canBeUpdated(user: User): ResultOrError[F, Boolean]
  def updateUser(user: User)(pointsToAdd: Int): ResultOrError[F, User]
}

class LoyaltyPoints[F[_]](store: UserStoreAlg[F])(implicit M: Monad[F]) {
  def addPoints(userId: Long, pointsToAdd: Int): F[Either[ServiceError, User]] = {
    val result: ResultOrError[F, User] = for {
      user        <- store.findUser(userId)
      _           <- store.canBeUpdated(user)
      updatedUser <- store.updateUser(user)(pointsToAdd)
    } yield updatedUser
    result.value // EitherT[F, ServiceError, User] => F[Either[ServiceError, User]]
  }
}

Api v3

and now to run the code...

Show me the Monad!

trait FutureInterpreter extends UserStoreAlg[Future] {
  override def findUser(id: Long): EitherT[Future, ServiceError, User] =
    EitherT(Future.successful(Either.right(User(1L, "awesome@fp.com", 1000))))

  override def canBeUpdated(user: User): ResultOrError[Future, Boolean] =
    EitherT(Future.successful(Either.right(true)))

  override def updateUser(user: User)(pointsToAdd: Int): ResultOrError[Future, User] =
    EitherT(Future.successful(
      Either.right(user.copy(loyaltyPoints = user.loyaltyPoints + pointsToAdd))))
}

The Interpreter

val interpreter:FutureInterpreter = new FutureInterpreter{}
val result: Future[Either[ServiceError, User]] = new LoyaltyPoints(interpreter)
                                                   .addPoints(1, 100)
// wait the future
Await.result(result, 1 second).foreach(println) // User(1,awesome@fp.com,1100)

FP in the JVM

Scalaz - scala

Cats- scala

Arrow- kotlin

Cyclops-react - java

vavr.io - java

Resources

Questions?

Demystifying Functional Programming

By Diego Parra

Demystifying Functional Programming

Functional programming with abstractions.

  • 743