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("<", "<").replaceAll(">", ">")
}
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
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
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.
- 821